Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37e5b97507 | |||
| 63c9a272b5 | |||
| 411f7fd6d9 | |||
| 8fc4b236ac |
@@ -63,6 +63,12 @@ func main() {
|
||||
}
|
||||
args = append(args, rest[0])
|
||||
rest = rest[1:]
|
||||
if len(args) > 0 {
|
||||
// Once we have identified the subcommand, the remaining arguments
|
||||
// belong to it (including its own flags), so we stop parsing global flags.
|
||||
args = append(args, rest...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if *showVersion {
|
||||
@@ -81,7 +87,7 @@ func main() {
|
||||
fmt.Printf("configuration file already exists at %s\n", configPath)
|
||||
os.Exit(0)
|
||||
}
|
||||
fatalIf(saveDefaultConfig(configPath))
|
||||
fatalIf(saveDefaultConfig(configPath, DefaultConfigContent(getUsername())))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -96,7 +102,7 @@ func main() {
|
||||
runCmd(args)
|
||||
case "install":
|
||||
ensureRoot()
|
||||
fatalIf(installService())
|
||||
fatalIf(installService(args))
|
||||
case "uninstall":
|
||||
ensureRoot()
|
||||
fatalIf(uninstallService(slices.Contains(args, "--complete")))
|
||||
@@ -238,7 +244,7 @@ func runServer() {
|
||||
|
||||
meta.Register(api, mods)
|
||||
meta.RegisterHealth(api, sessions)
|
||||
meta.RegisterWhoami(api, sessions, roles, mods)
|
||||
meta.RegisterWhoami(api, sessions, tokenAuth, roles, mods)
|
||||
meta.RegisterUpdate(api, configPath)
|
||||
|
||||
auth.RegisterLogin(api, sessions, auditStore, cfg.SecureCookie())
|
||||
|
||||
+143
-27
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -13,26 +14,29 @@ import (
|
||||
"nadir/internal/config"
|
||||
)
|
||||
|
||||
const defaultConfigTemplate = `# Nadir configuration - config.yaml
|
||||
const configTemplateBase = `# Nadir configuration - config.yaml
|
||||
#
|
||||
# Single source of truth for runtime settings.
|
||||
#
|
||||
|
||||
server:
|
||||
secure_tls: true
|
||||
# trust_proxy: false
|
||||
# tls_cert: /etc/nadir/tls/cert.pem
|
||||
# tls_key: /etc/nadir/tls/key.pem
|
||||
hostname: 127.0.0.1
|
||||
port: 9999
|
||||
secure_tls: %s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
hostname: %s
|
||||
port: %d
|
||||
release_repo: https://tea.urania.dev/urania/nadir-agent
|
||||
|
||||
roles:
|
||||
admin:
|
||||
"*": ["*"]
|
||||
auditor:
|
||||
"*": ["read"]
|
||||
|
||||
assignments:
|
||||
%s: [admin]
|
||||
dashboard: [auditor]
|
||||
`
|
||||
|
||||
// resolveConfigPath returns the config file path: CONFIG_PATH env (with ~ expanded)
|
||||
@@ -101,7 +105,38 @@ const (
|
||||
// installService writes the systemd unit, enables it on boot, and starts it.
|
||||
// The unit pins the absolute executable and config paths captured now, so the
|
||||
// service doesn't depend on the working directory at boot.
|
||||
func installService() error {
|
||||
func installService(args []string) error {
|
||||
// Parse options
|
||||
fs := flag.NewFlagSet("install", flag.ContinueOnError)
|
||||
tlsOpt := fs.Bool("tls", false, "Generate persistent self-signed TLS cert/key and enable HTTPS")
|
||||
unsecureOpt := fs.Bool("unsecure", false, "Serve plaintext HTTP directly")
|
||||
trustProxyOpt := fs.Bool("trust-proxy", false, "Serve plaintext HTTP behind a trusted TLS-terminating reverse proxy")
|
||||
hostnameOpt := fs.String("hostname", "127.0.0.1", "Hostname to bind to")
|
||||
portOpt := fs.Int("port", 9999, "Port to bind to")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
optCount := 0
|
||||
if *tlsOpt {
|
||||
optCount++
|
||||
}
|
||||
if *unsecureOpt {
|
||||
optCount++
|
||||
}
|
||||
if *trustProxyOpt {
|
||||
optCount++
|
||||
}
|
||||
|
||||
if optCount > 1 {
|
||||
return fmt.Errorf("options --tls, --unsecure, and --trust-proxy are mutually exclusive")
|
||||
}
|
||||
|
||||
// Default to tls if nothing is specified
|
||||
isTLS := *tlsOpt || optCount == 0
|
||||
isUnsecure := *unsecureOpt
|
||||
isTrustProxy := *trustProxyOpt
|
||||
|
||||
// Provision the PAM service the server authenticates against, so it exists
|
||||
// before the unit starts rather than appearing on first login. Idempotent:
|
||||
// EnsurePAMService leaves an existing /etc/pam.d/nadir untouched. runServer
|
||||
@@ -130,14 +165,51 @@ func installService() error {
|
||||
return fmt.Errorf("create data directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate and save persistent self-signed TLS certificates if TLS mode
|
||||
certPath := filepath.Join("/var/lib/nadir/tls", "cert.pem")
|
||||
if isTLS {
|
||||
tlsDir := "/var/lib/nadir/tls"
|
||||
if err := os.MkdirAll(tlsDir, 0700); err != nil {
|
||||
return fmt.Errorf("create tls directory: %w", err)
|
||||
}
|
||||
keyPath := filepath.Join(tlsDir, "key.pem")
|
||||
if _, err := os.Stat(certPath); os.IsNotExist(err) {
|
||||
if err := generateAndSaveCert(certPath, keyPath, *hostnameOpt); err != nil {
|
||||
return fmt.Errorf("generate certificates: %w", err)
|
||||
}
|
||||
fmt.Printf("generated persistent self-signed TLS certificate at %s\n", certPath)
|
||||
}
|
||||
}
|
||||
|
||||
cfgPath, err := resolveConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Construct configuration template content based on installation options
|
||||
secureTLSVal := "true"
|
||||
trustProxyLine := "# trust_proxy: false"
|
||||
certLine := "tls_cert: /var/lib/nadir/tls/cert.pem"
|
||||
keyLine := "tls_key: /var/lib/nadir/tls/key.pem"
|
||||
|
||||
if isUnsecure {
|
||||
secureTLSVal = "false"
|
||||
trustProxyLine = "# trust_proxy: false"
|
||||
certLine = "# tls_cert: /var/lib/nadir/tls/cert.pem"
|
||||
keyLine = "# tls_key: /var/lib/nadir/tls/key.pem"
|
||||
} else if isTrustProxy {
|
||||
secureTLSVal = "false"
|
||||
trustProxyLine = "trust_proxy: true"
|
||||
certLine = "# tls_cert: /var/lib/nadir/tls/cert.pem"
|
||||
keyLine = "# tls_key: /var/lib/nadir/tls/key.pem"
|
||||
}
|
||||
|
||||
username := getUsername()
|
||||
configContent := fmt.Sprintf(configTemplateBase, secureTLSVal, trustProxyLine, certLine, keyLine, *hostnameOpt, *portOpt, username)
|
||||
|
||||
// Ensure default config file exists
|
||||
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
|
||||
if err := saveDefaultConfig(cfgPath); err != nil {
|
||||
if err := saveDefaultConfig(cfgPath, configContent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -182,6 +254,44 @@ WantedBy=multi-user.target
|
||||
fmt.Printf("created logrotate configuration %s\n", logrotatePath)
|
||||
}
|
||||
|
||||
// Generate a token for the dashboard if it doesn't exist
|
||||
var tokenStr string
|
||||
store, err := auth.NewTokenStore(tokenDBPath)
|
||||
if err == nil {
|
||||
defer store.Close()
|
||||
infos, err := store.List()
|
||||
hasDashboard := false
|
||||
if err == nil {
|
||||
for _, t := range infos {
|
||||
if t.Name == "dashboard" {
|
||||
hasDashboard = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasDashboard {
|
||||
tokenStr, _ = store.Create("dashboard")
|
||||
} else {
|
||||
tokenStr = "(already created; run 'nadir token add dashboard' to replace/generate a new one if lost)"
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("warning: failed to open token store: %v\n", err)
|
||||
}
|
||||
|
||||
// Output credentials to copy to the frontend
|
||||
fmt.Println("\n======================================================================")
|
||||
fmt.Println(" NADIR CLIENT CREDENTIALS (COPY THESE TO NADIR WEBUI)")
|
||||
fmt.Println("======================================================================")
|
||||
fmt.Printf("Token for \"dashboard\" (Bearer):\n %s\n", tokenStr)
|
||||
if isTLS {
|
||||
certBytes, err := os.ReadFile(certPath)
|
||||
if err == nil {
|
||||
fmt.Println("\nCA Certificate (Trust Store):")
|
||||
fmt.Println(string(certBytes))
|
||||
}
|
||||
}
|
||||
fmt.Println("======================================================================")
|
||||
|
||||
fmt.Printf("installed and started %s; follow logs with: %s logs\n", serviceName, filepath.Base(exe))
|
||||
return nil
|
||||
}
|
||||
@@ -393,21 +503,24 @@ func fatalIf(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultConfigContent returns the default configuration template filled with the username.
|
||||
func DefaultConfigContent(username string) string {
|
||||
return fmt.Sprintf(configTemplateBase, "true", "# trust_proxy: false", "# tls_cert: /var/lib/nadir/tls/cert.pem", "# tls_key: /var/lib/nadir/tls/key.pem", "127.0.0.1", 9999, username)
|
||||
}
|
||||
|
||||
// saveDefaultConfig writes the default configuration template to cfgPath.
|
||||
func saveDefaultConfig(cfgPath string) error {
|
||||
func saveDefaultConfig(cfgPath, content string) error {
|
||||
dir := filepath.Dir(cfgPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("create config directory: %w", err)
|
||||
}
|
||||
chownToSudoUser(dir)
|
||||
|
||||
username := getUsername()
|
||||
configContent := fmt.Sprintf(defaultConfigTemplate, username)
|
||||
if err := os.WriteFile(cfgPath, []byte(configContent), 0600); err != nil {
|
||||
if err := os.WriteFile(cfgPath, []byte(content), 0600); err != nil {
|
||||
return fmt.Errorf("write default config: %w", err)
|
||||
}
|
||||
chownToSudoUser(cfgPath)
|
||||
fmt.Printf("created default configuration file at %s (assigned admin role to user %q)\n", cfgPath, username)
|
||||
fmt.Printf("created default configuration file at %s\n", cfgPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -415,19 +528,22 @@ func usage(w io.Writer) {
|
||||
fmt.Fprint(w, `nadir - Linux system administration backend
|
||||
|
||||
Usage:
|
||||
nadir [run] [-d] [-f <path>] Start the server (-d / --detach: run in background)
|
||||
nadir --save-config [-f <path>] Save default configuration to path and exit
|
||||
nadir install [-f <path>] Install + enable the systemd service (starts on boot)
|
||||
nadir uninstall [--complete] Remove the service (keeps data/config; --complete wipes all)
|
||||
nadir start|stop|restart|status Control the running service
|
||||
nadir enable|disable Toggle start-on-boot without removing the unit
|
||||
nadir logs [clear] Follow (default) or clear server logs
|
||||
nadir token add <name> Mint a machine credential (Bearer token), shown once
|
||||
nadir token rm <name> Revoke a token (effective immediately, no restart)
|
||||
nadir token ls List token names and when they were created
|
||||
nadir update [--check|--force] Fetch the latest release from server.release_repo and restart
|
||||
(--check: report only; --force: re-download when already current)
|
||||
nadir help Show this help
|
||||
nadir [run] [-d] [-f <path>] Start the server (-d / --detach: run in background)
|
||||
nadir --save-config [-f <path>] Save default configuration to path and exit
|
||||
nadir install [--tls|--unsecure|--trust-proxy] Install + enable the systemd service (starts on boot)
|
||||
(--tls: enable HTTPS with self-signed certificate, default)
|
||||
(--unsecure: serve HTTP directly)
|
||||
(--trust-proxy: serve HTTP behind a reverse proxy)
|
||||
nadir uninstall [--complete] Remove the service (keeps data/config; --complete wipes all)
|
||||
nadir start|stop|restart|status Control the running service
|
||||
nadir enable|disable Toggle start-on-boot without removing the unit
|
||||
nadir logs [clear] Follow (default) or clear server logs
|
||||
nadir token add <name> Mint a machine credential (Bearer token), shown once
|
||||
nadir token rm <name> Revoke a token (effective immediately, no restart)
|
||||
nadir token ls List token names and when they were created
|
||||
nadir update [--check|--force] Fetch the latest release from server.release_repo and restart
|
||||
(--check: report only; --force: re-download when already current)
|
||||
nadir help Show this help
|
||||
|
||||
Most commands need root. Config path is specified via -f/--config or CONFIG_PATH (default ~/.config/config.yaml).
|
||||
`)
|
||||
|
||||
+77
-1
@@ -7,9 +7,11 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -39,7 +41,7 @@ func generateSelfSignedCert() (tls.Certificate, error) {
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{Organization: []string{"nadir-dev-local"}},
|
||||
Subject: pkix.Name{Organization: []string{"urania-nadir"}},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
@@ -55,3 +57,77 @@ func generateSelfSignedCert() (tls.Certificate, error) {
|
||||
}
|
||||
return tls.Certificate{Certificate: [][]byte{derBytes}, PrivateKey: priv}, nil
|
||||
}
|
||||
|
||||
// generateAndSaveCert generates a self-signed certificate and private key,
|
||||
// and saves them as PEM files.
|
||||
func generateAndSaveCert(certPath, keyPath string, hostname string) error {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{Organization: []string{"nadir-agent-tls"}},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(3650 * 24 * time.Hour), // 10 years
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
||||
}
|
||||
|
||||
if hostname != "" && hostname != "localhost" && hostname != "127.0.0.1" && hostname != "::1" {
|
||||
if ip := net.ParseIP(hostname); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, hostname)
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically add the machine's external IP addresses to SANs so it can be verified
|
||||
// correctly over the local network (e.g. Tailscale or Netbird).
|
||||
if addrs, err := net.InterfaceAddrs(); err == nil {
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
template.IPAddresses = append(template.IPAddresses, ipnet.IP)
|
||||
template.DNSNames = append(template.DNSNames, ipnet.IP.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certOut, err := os.Create(certPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer certOut.Close()
|
||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close()
|
||||
privBytes, err := x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+26
-7
@@ -15,6 +15,7 @@ import (
|
||||
// itself.
|
||||
type WhoamiInput struct {
|
||||
SessionID string `cookie:"nadir_session_id"`
|
||||
Auth string `header:"Authorization"`
|
||||
}
|
||||
|
||||
// WhoamiBody reports who the caller is and, per module, which permissions they
|
||||
@@ -30,7 +31,7 @@ type WhoamiOutput struct{ Body WhoamiBody }
|
||||
// RegisterWhoami adds the current-user endpoint. It resolves the caller's
|
||||
// concrete grants by asking the RBAC store about each module's permissions,
|
||||
// so "*" wildcards in roles are expanded for free.
|
||||
func RegisterWhoami(api huma.API, sessions *auth.SessionStore, roles *rbac.RBAC, mods []module.Module) {
|
||||
func RegisterWhoami(api huma.API, sessions *auth.SessionStore, tokens *auth.TokenAuth, roles *rbac.RBAC, mods []module.Module) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "whoami",
|
||||
Method: "GET",
|
||||
@@ -40,18 +41,35 @@ func RegisterWhoami(api huma.API, sessions *auth.SessionStore, roles *rbac.RBAC,
|
||||
"permissions the caller holds (wildcards resolved). Pair with " +
|
||||
"/api/_modules to render the full permission matrix.",
|
||||
Tags: []string{"Meta"},
|
||||
Errors: []int{401},
|
||||
Errors: []int{401, 429},
|
||||
}, func(ctx context.Context, in *WhoamiInput) (*WhoamiOutput, error) {
|
||||
sess, ok := sessions.GetByToken(in.SessionID)
|
||||
if !ok {
|
||||
return nil, huma.Error401Unauthorized("unauthorized")
|
||||
var username string
|
||||
|
||||
if raw, isBearer := auth.BearerToken(in.Auth); isBearer {
|
||||
if tokens == nil {
|
||||
return nil, huma.Error401Unauthorized("unauthorized")
|
||||
}
|
||||
name, ok, throttled := tokens.Verify(auth.ClientIP(ctx), raw)
|
||||
if throttled {
|
||||
return nil, huma.Error429TooManyRequests("too many failed token attempts; wait a minute")
|
||||
}
|
||||
if !ok {
|
||||
return nil, huma.Error401Unauthorized("unauthorized")
|
||||
}
|
||||
username = name
|
||||
} else {
|
||||
sess, ok := sessions.GetByToken(in.SessionID)
|
||||
if !ok {
|
||||
return nil, huma.Error401Unauthorized("unauthorized")
|
||||
}
|
||||
username = sess.Username
|
||||
}
|
||||
|
||||
held := make(map[string][]string)
|
||||
for _, m := range mods {
|
||||
var perms []string
|
||||
for _, p := range m.Permissions() {
|
||||
if roles.Can(sess.Username, m.ID(), p) {
|
||||
if roles.Can(username, m.ID(), p) {
|
||||
perms = append(perms, string(p))
|
||||
}
|
||||
}
|
||||
@@ -61,7 +79,8 @@ func RegisterWhoami(api huma.API, sessions *auth.SessionStore, roles *rbac.RBAC,
|
||||
}
|
||||
|
||||
out := &WhoamiOutput{}
|
||||
out.Body = WhoamiBody{Username: sess.Username, Permissions: held}
|
||||
out.Body = WhoamiBody{Username: username, Permissions: held}
|
||||
return out, nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"nadir/internal/auth"
|
||||
"nadir/internal/module"
|
||||
"nadir/internal/rbac"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
||||
"github.com/danielgtaylor/huma/v2/humatest"
|
||||
)
|
||||
|
||||
type dummyModule struct {
|
||||
id string
|
||||
perms []rbac.Permission
|
||||
}
|
||||
|
||||
func (m *dummyModule) ID() string { return m.id }
|
||||
func (m *dummyModule) Name() string { return m.id }
|
||||
func (m *dummyModule) Permissions() []rbac.Permission { return m.perms }
|
||||
func (m *dummyModule) Register(api huma.API) {}
|
||||
|
||||
func TestWhoami(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
sessions, err := auth.NewSessionStore(filepath.Join(tempDir, "sessions.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tokenStore, err := auth.NewTokenStore(filepath.Join(tempDir, "tokens.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tokenStore.Close()
|
||||
tokenAuth := auth.NewTokenAuth(tokenStore)
|
||||
|
||||
roles := rbac.New()
|
||||
roles.DefineRole(rbac.Role{
|
||||
Name: "admin-role",
|
||||
ModuleGrants: map[string][]rbac.Permission{
|
||||
"system": {rbac.Read},
|
||||
},
|
||||
})
|
||||
roles.AssignRole("admin", "admin-role")
|
||||
|
||||
mods := []module.Module{
|
||||
&dummyModule{
|
||||
id: "system",
|
||||
perms: []rbac.Permission{rbac.Read, rbac.Write},
|
||||
},
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
api := humatest.Wrap(t, humago.New(mux, huma.DefaultConfig("Test", "1.0.0")))
|
||||
RegisterWhoami(api, sessions, tokenAuth, roles, mods)
|
||||
|
||||
// 1. Unauthorized request (no token, no session)
|
||||
resp := api.Get("/api/whoami")
|
||||
if resp.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", resp.Code)
|
||||
}
|
||||
|
||||
// 2. Cookie session request
|
||||
sessToken, err := sessions.Create("admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp = api.Get("/api/whoami", "Cookie: nadir_session_id="+sessToken)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", resp.Code)
|
||||
}
|
||||
var body WhoamiBody
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Username != "admin" {
|
||||
t.Errorf("expected username admin, got %q", body.Username)
|
||||
}
|
||||
if !slices.Contains(body.Permissions["system"], "read") {
|
||||
t.Errorf("expected system read permission, got %v", body.Permissions["system"])
|
||||
}
|
||||
|
||||
// 3. Token request
|
||||
bearerToken, err := tokenStore.Create("api-user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
roles.DefineRole(rbac.Role{
|
||||
Name: "api-role",
|
||||
ModuleGrants: map[string][]rbac.Permission{
|
||||
"system": {rbac.Write},
|
||||
},
|
||||
})
|
||||
roles.AssignRole("api-user", "api-role")
|
||||
|
||||
resp = api.Get("/api/whoami", "Authorization: Bearer "+bearerToken)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", resp.Code)
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Username != "api-user" {
|
||||
t.Errorf("expected username api-user, got %q", body.Username)
|
||||
}
|
||||
if !slices.Contains(body.Permissions["system"], "write") {
|
||||
t.Errorf("expected system write permission, got %v", body.Permissions["system"])
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func TestOpenAPISchemaNoCollisions(t *testing.T) {
|
||||
}
|
||||
meta.Register(api, mods)
|
||||
meta.RegisterHealth(api, sessions)
|
||||
meta.RegisterWhoami(api, sessions, roles, mods)
|
||||
meta.RegisterWhoami(api, sessions, nil, roles, mods)
|
||||
auth.RegisterLogin(api, sessions, auditStore, true)
|
||||
auth.RegisterLogout(api, sessions, true)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user