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---
76 lines
2.5 KiB
Go
76 lines
2.5 KiB
Go
package txn
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
// TestApplyRefusesUnauthorized (P04, T4, C-44) verifies that Apply
|
|
// returns ErrUnauthorized when the Authorize hook returns an error.
|
|
// The apply path MUST refuse to run without a verified operator
|
|
// token.
|
|
func TestApplyRefusesUnauthorized(t *testing.T) {
|
|
tr := &authMockTransport{}
|
|
opts := ApplyOptions{
|
|
Namespace: "myapp",
|
|
Authorize: func(ctx context.Context) (string, error) {
|
|
return "", errors.New("ORCA_OIDC_TOKEN not set")
|
|
},
|
|
}
|
|
err := Apply(context.Background(), "T-deadbeefdeadbeef", "lead.example.com", tr, opts)
|
|
if err == nil {
|
|
t.Fatal("expected error when Authorize fails, got nil")
|
|
}
|
|
if !errors.Is(err, ErrUnauthorized) {
|
|
t.Errorf("expected ErrUnauthorized, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestApplyAuthorizesWithHook verifies that Apply proceeds when the
|
|
// Authorize hook returns nil, and that the actor is logged.
|
|
func TestApplyAuthorizesWithHook(t *testing.T) {
|
|
tr := &authMockTransport{execOut: []byte("applied\n")}
|
|
opts := ApplyOptions{
|
|
Namespace: "myapp",
|
|
Authorize: func(ctx context.Context) (string, error) {
|
|
return "oidc:operator@example.com", nil
|
|
},
|
|
}
|
|
err := Apply(context.Background(), "T-deadbeefdeadbeef", "lead.example.com", tr, opts)
|
|
// We expect a non-ErrUnauthorized error here because the mock
|
|
// transport's orca-pull.sh path doesn't exist; the point is that
|
|
// the apply got PAST the authorize hook.
|
|
if err != nil && errors.Is(err, ErrUnauthorized) {
|
|
t.Errorf("apply should not be refused after successful authorize: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestApplyNoAuthorizeHookSkipsCheck verifies that when Authorize is
|
|
// nil (legacy/test path), the apply proceeds without an auth check.
|
|
// This preserves backward compat for tests that call Apply directly.
|
|
func TestApplyNoAuthorizeHookSkipsCheck(t *testing.T) {
|
|
tr := &authMockTransport{execOut: []byte("applied\n")}
|
|
opts := ApplyOptions{Namespace: "myapp"}
|
|
err := Apply(context.Background(), "T-deadbeefdeadbeef", "lead.example.com", tr, opts)
|
|
// Any error is fine as long as it's not ErrUnauthorized.
|
|
if err != nil && errors.Is(err, ErrUnauthorized) {
|
|
t.Errorf("apply should skip auth when Authorize is nil: %v", err)
|
|
}
|
|
}
|
|
|
|
// authMockTransport is a minimal Transport for the auth tests.
|
|
type authMockTransport struct {
|
|
execOut []byte
|
|
execErr error
|
|
}
|
|
|
|
func (m *authMockTransport) WriteFileIdempotent(ctx context.Context, peer, path string, content []byte, mode os.FileMode) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
func (m *authMockTransport) Exec(ctx context.Context, peer, cmd string) ([]byte, error) {
|
|
return m.execOut, m.execErr
|
|
}
|