5232fcb808
R-023: Zero-trust enforcement operationally wired. ACL enforcement (C-45 staged rollout): - acl.Check wired into all 5 daemon handlers (dispatch/jobs/nodes/tasks) - health endpoints exempt (liveness probes not gated) - ACL log-only mode default (config acl.enforce=false); enforce after bootstrap ACL verified - sshpush auth: ORCA_OIDC_TOKEN validated against JWKS before apply - txn apply: Authorize hook validates OIDC token before running pull - acl.json mode 0600 (was 0644) - flock on acl.json for concurrent grant/revoke - bootstrap ACL: init grants cluster-admin to orca-admins group + SVID Audit actor identity: - currentActor reads OIDC sub from credentials.json (was hardcoded "cli") - threaded through all audit.Record calls via context WebAuthn registration auth: - BeginRegistration/FinishRegistration require authenticated session - fail-closed 401 when no authFunc configured New files: internal/daemon/acl.go, internal/cli/authactor.go, internal/engine/actor.go, internal/identity/authtoken.go, internal/sshpush/auth.go, internal/txn/auth_test.go ---ci--- project: orca phase: 4 milestone: v0.13 status: complete requirements: covered: [153] ---/ci---
150 lines
5.1 KiB
Go
150 lines
5.1 KiB
Go
package webauthn
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestConnectorBuild verifies a Connector can be built with a valid
|
|
// RP ID + origin (C-38).
|
|
func TestConnectorBuild(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, err := NewStore(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
defer store.Close()
|
|
c, err := NewConnector(store, "cluster.example.com", "https://cluster.example.com")
|
|
if err != nil {
|
|
t.Fatalf("NewConnector: %v", err)
|
|
}
|
|
if c.RPID() != "cluster.example.com" {
|
|
t.Errorf("RPID = %q, want cluster.example.com", c.RPID())
|
|
}
|
|
}
|
|
|
|
// TestConnectorHealthz verifies the /orca/webauthn/healthz endpoint
|
|
// responds with the RP ID.
|
|
func TestConnectorHealthz(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, _ := NewStore(dbPath)
|
|
defer store.Close()
|
|
c, _ := NewConnector(store, "test.cluster", "https://test.cluster")
|
|
mux := c.Routes()
|
|
req := httptest.NewRequest("GET", "/orca/webauthn/healthz", nil)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("healthz status = %d, want 200", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "test.cluster") {
|
|
t.Errorf("healthz body should contain rp_id: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestConnectorBeginRegistrationNoUsername verifies the register
|
|
// endpoint rejects requests without a username when authenticated.
|
|
func TestConnectorBeginRegistrationNoUsername(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, _ := NewStore(dbPath)
|
|
defer store.Close()
|
|
c, _ := NewConnectorWithAuth(store, "test.cluster", "https://test.cluster",
|
|
func(r *http.Request) (bool, string, error) { return true, "admin", nil })
|
|
mux := c.Routes()
|
|
req := httptest.NewRequest("GET", "/orca/webauthn/register", nil)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("register without username (authed): %d, want 400", rec.Code)
|
|
}
|
|
}
|
|
|
|
// TestConnectorBeginRegistrationUnauthenticated (P04, T9, C-45)
|
|
// verifies the register endpoint rejects requests with no
|
|
// authenticated session — closing the hole where anyone could
|
|
// register a credential for any username.
|
|
func TestConnectorBeginRegistrationUnauthenticated(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, _ := NewStore(dbPath)
|
|
defer store.Close()
|
|
// No authFunc → fail-closed (401).
|
|
c, _ := NewConnector(store, "test.cluster", "https://test.cluster")
|
|
mux := c.Routes()
|
|
req := httptest.NewRequest("GET", "/orca/webauthn/register?username=admin", nil)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Errorf("register unauthenticated (no authFunc): %d, want 401", rec.Code)
|
|
}
|
|
|
|
// authFunc that returns false → 401.
|
|
c2, _ := NewConnectorWithAuth(store, "test.cluster", "https://test.cluster",
|
|
func(r *http.Request) (bool, string, error) { return false, "", nil })
|
|
mux2 := c2.Routes()
|
|
req2 := httptest.NewRequest("GET", "/orca/webauthn/register?username=admin", nil)
|
|
rec2 := httptest.NewRecorder()
|
|
mux2.ServeHTTP(rec2, req2)
|
|
if rec2.Code != http.StatusUnauthorized {
|
|
t.Errorf("register unauthenticated (authFunc=false): %d, want 401", rec2.Code)
|
|
}
|
|
}
|
|
|
|
// TestConnectorFinishRegistrationUnauthenticated (P04, T9) verifies
|
|
// the finish endpoint also requires authentication.
|
|
func TestConnectorFinishRegistrationUnauthenticated(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, _ := NewStore(dbPath)
|
|
defer store.Close()
|
|
c, _ := NewConnector(store, "test.cluster", "https://test.cluster")
|
|
mux := c.Routes()
|
|
req := httptest.NewRequest("POST", "/orca/webauthn/register/finish?username=admin", nil)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Errorf("finish unauthenticated: %d, want 401", rec.Code)
|
|
}
|
|
}
|
|
|
|
// TestConnectorBeginLoginNotRegistered verifies login for an
|
|
// unregistered user returns 404.
|
|
func TestConnectorBeginLoginNotRegistered(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, _ := NewStore(dbPath)
|
|
defer store.Close()
|
|
c, _ := NewConnector(store, "test.cluster", "https://test.cluster")
|
|
mux := c.Routes()
|
|
req := httptest.NewRequest("GET", "/orca/webauthn/login?username=ghost", nil)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("login unregistered: %d, want 404", rec.Code)
|
|
}
|
|
}
|
|
|
|
// TestStoreModeEnforced verifies the DB file is 0600 after creation.
|
|
func TestStoreModeEnforced(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "webauthn-creds.db")
|
|
store, err := NewStore(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
defer store.Close()
|
|
// Trigger a write so the DB file is created on disk.
|
|
store.PutCredential(&Credential{
|
|
UserID: "u",
|
|
CredentialID: []byte("c"),
|
|
PublicKey: []byte("p"),
|
|
})
|
|
info, err := os.Stat(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("stat db: %v", err)
|
|
}
|
|
if info.Mode().Perm()&0o077 != 0 {
|
|
t.Errorf("db mode = %o, want 0600", info.Mode().Perm())
|
|
}
|
|
}
|