Update engine specs, audit doc, and server tests for SSH CA, transit, and user engines
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -175,6 +175,35 @@ func makeEngineRequest(mount, operation string) string {
|
||||
return `{"mount":"` + mount + `","operation":"` + operation + `","data":{}}`
|
||||
}
|
||||
|
||||
// stubEngine is a minimal engine implementation for testing the generic endpoint.
|
||||
type stubEngine struct {
|
||||
engineType engine.EngineType
|
||||
}
|
||||
|
||||
func (e *stubEngine) Type() engine.EngineType { return e.engineType }
|
||||
func (e *stubEngine) Initialize(_ context.Context, _ barrier.Barrier, _ string, _ map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
func (e *stubEngine) Unseal(_ context.Context, _ barrier.Barrier, _ string) error { return nil }
|
||||
func (e *stubEngine) Seal() error { return nil }
|
||||
func (e *stubEngine) HandleRequest(_ context.Context, req *engine.Request) (*engine.Response, error) {
|
||||
return &engine.Response{Data: map[string]interface{}{"ok": true}}, nil
|
||||
}
|
||||
|
||||
// mountStubEngine registers a factory and mounts a stub engine of the given type.
|
||||
func mountStubEngine(t *testing.T, srv *Server, name string, engineType engine.EngineType) {
|
||||
t.Helper()
|
||||
srv.engines.RegisterFactory(engineType, func() engine.Engine {
|
||||
return &stubEngine{engineType: engineType}
|
||||
})
|
||||
if err := srv.engines.Mount(context.Background(), name, engineType, nil); err != nil {
|
||||
// Ignore "already exists" from re-mounting the same name.
|
||||
if !strings.Contains(err.Error(), "already exists") {
|
||||
t.Fatalf("mount stub %q as %s: %v", name, engineType, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func withTokenInfo(r *http.Request, info *auth.TokenInfo) *http.Request {
|
||||
return r.WithContext(context.WithValue(r.Context(), tokenInfoKey, info))
|
||||
}
|
||||
@@ -184,6 +213,7 @@ func withTokenInfo(r *http.Request, info *auth.TokenInfo) *http.Request {
|
||||
func TestEngineRequestPolicyDeniesNonAdmin(t *testing.T) {
|
||||
srv, sealMgr, _ := setupTestServer(t)
|
||||
unsealServer(t, sealMgr, nil)
|
||||
mountStubEngine(t, srv, "pki", engine.EngineTypeCA)
|
||||
|
||||
body := makeEngineRequest("pki", "list-issuers")
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
|
||||
@@ -200,6 +230,7 @@ func TestEngineRequestPolicyDeniesNonAdmin(t *testing.T) {
|
||||
func TestEngineRequestPolicyAllowsAdmin(t *testing.T) {
|
||||
srv, sealMgr, _ := setupTestServer(t)
|
||||
unsealServer(t, sealMgr, nil)
|
||||
mountStubEngine(t, srv, "pki", engine.EngineTypeCA)
|
||||
|
||||
body := makeEngineRequest("pki", "list-issuers")
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
|
||||
@@ -207,7 +238,7 @@ func TestEngineRequestPolicyAllowsAdmin(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
srv.handleEngineRequest(w, req)
|
||||
|
||||
// Admin bypasses policy; will fail with mount-not-found (404), not forbidden (403).
|
||||
// Admin bypasses policy; stub engine returns 200.
|
||||
if w.Code == http.StatusForbidden {
|
||||
t.Errorf("admin should not be forbidden by policy, got 403: %s", w.Body.String())
|
||||
}
|
||||
@@ -218,6 +249,7 @@ func TestEngineRequestPolicyAllowsAdmin(t *testing.T) {
|
||||
func TestEngineRequestPolicyAllowsWithRule(t *testing.T) {
|
||||
srv, sealMgr, _ := setupTestServer(t)
|
||||
unsealServer(t, sealMgr, nil)
|
||||
mountStubEngine(t, srv, "pki", engine.EngineTypeCA)
|
||||
|
||||
ctx := context.Background()
|
||||
_ = srv.policy.CreateRule(ctx, &policy.Rule{
|
||||
@@ -235,7 +267,7 @@ func TestEngineRequestPolicyAllowsWithRule(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
srv.handleEngineRequest(w, req)
|
||||
|
||||
// Policy allows; will fail with mount-not-found (404), not forbidden (403).
|
||||
// Policy allows; stub engine returns 200.
|
||||
if w.Code == http.StatusForbidden {
|
||||
t.Errorf("user with allow rule should not be forbidden, got 403: %s", w.Body.String())
|
||||
}
|
||||
@@ -247,15 +279,32 @@ func TestEngineRequestAdminOnlyBlocksNonAdmin(t *testing.T) {
|
||||
srv, sealMgr, _ := setupTestServer(t)
|
||||
unsealServer(t, sealMgr, nil)
|
||||
|
||||
for _, op := range []string{"create-issuer", "delete-cert", "create-key", "rotate-key", "create-profile", "provision"} {
|
||||
body := makeEngineRequest("test-mount", op)
|
||||
// Mount stub engines so the admin-only lookup can resolve engine types.
|
||||
mountStubEngine(t, srv, "ca-mount", engine.EngineTypeCA)
|
||||
mountStubEngine(t, srv, "transit-mount", engine.EngineTypeTransit)
|
||||
mountStubEngine(t, srv, "sshca-mount", engine.EngineTypeSSHCA)
|
||||
mountStubEngine(t, srv, "user-mount", engine.EngineTypeUser)
|
||||
|
||||
cases := []struct {
|
||||
mount string
|
||||
op string
|
||||
}{
|
||||
{"ca-mount", "create-issuer"},
|
||||
{"ca-mount", "delete-cert"},
|
||||
{"transit-mount", "create-key"},
|
||||
{"transit-mount", "rotate-key"},
|
||||
{"sshca-mount", "create-profile"},
|
||||
{"user-mount", "provision"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
body := makeEngineRequest(tc.mount, tc.op)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
|
||||
req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false})
|
||||
w := httptest.NewRecorder()
|
||||
srv.handleEngineRequest(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("operation %q: expected 403 for non-admin, got %d", op, w.Code)
|
||||
t.Errorf("%s/%s: expected 403 for non-admin, got %d", tc.mount, tc.op, w.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,20 +315,86 @@ func TestEngineRequestAdminOnlyAllowsAdmin(t *testing.T) {
|
||||
srv, sealMgr, _ := setupTestServer(t)
|
||||
unsealServer(t, sealMgr, nil)
|
||||
|
||||
for _, op := range []string{"create-issuer", "delete-cert", "create-key", "rotate-key", "create-profile", "provision"} {
|
||||
body := makeEngineRequest("test-mount", op)
|
||||
mountStubEngine(t, srv, "ca-mount", engine.EngineTypeCA)
|
||||
mountStubEngine(t, srv, "transit-mount", engine.EngineTypeTransit)
|
||||
mountStubEngine(t, srv, "sshca-mount", engine.EngineTypeSSHCA)
|
||||
mountStubEngine(t, srv, "user-mount", engine.EngineTypeUser)
|
||||
|
||||
cases := []struct {
|
||||
mount string
|
||||
op string
|
||||
}{
|
||||
{"ca-mount", "create-issuer"},
|
||||
{"ca-mount", "delete-cert"},
|
||||
{"transit-mount", "create-key"},
|
||||
{"transit-mount", "rotate-key"},
|
||||
{"sshca-mount", "create-profile"},
|
||||
{"user-mount", "provision"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
body := makeEngineRequest(tc.mount, tc.op)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
|
||||
req = withTokenInfo(req, &auth.TokenInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true})
|
||||
w := httptest.NewRecorder()
|
||||
srv.handleEngineRequest(w, req)
|
||||
|
||||
// Admin passes the admin check; will get 404 (mount not found) not 403.
|
||||
// Admin passes the admin check; stub engine returns 200.
|
||||
if w.Code == http.StatusForbidden {
|
||||
t.Errorf("operation %q: admin should not be forbidden, got 403", op)
|
||||
t.Errorf("%s/%s: admin should not be forbidden, got 403", tc.mount, tc.op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEngineRequestUserRotateKeyOnUserMount verifies that a non-admin user
|
||||
// can call rotate-key on a user engine mount (not blocked by transit's admin gate).
|
||||
func TestEngineRequestUserRotateKeyOnUserMount(t *testing.T) {
|
||||
srv, sealMgr, _ := setupTestServer(t)
|
||||
unsealServer(t, sealMgr, nil)
|
||||
|
||||
mountStubEngine(t, srv, "user-mount", engine.EngineTypeUser)
|
||||
|
||||
// Create a policy rule allowing user operations.
|
||||
ctx := context.Background()
|
||||
_ = srv.policy.CreateRule(ctx, &policy.Rule{
|
||||
ID: "allow-user-ops",
|
||||
Priority: 100,
|
||||
Effect: policy.EffectAllow,
|
||||
Roles: []string{"user"},
|
||||
Resources: []string{"engine/*/*"},
|
||||
Actions: []string{"any"},
|
||||
})
|
||||
|
||||
body := makeEngineRequest("user-mount", "rotate-key")
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
|
||||
req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false})
|
||||
w := httptest.NewRecorder()
|
||||
srv.handleEngineRequest(w, req)
|
||||
|
||||
// rotate-key on a user mount should NOT be blocked as admin-only.
|
||||
if w.Code == http.StatusForbidden {
|
||||
t.Errorf("user rotate-key on user mount should not be forbidden, got 403: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestEngineRequestUserRotateKeyOnTransitMount verifies that a non-admin user
|
||||
// is blocked from calling rotate-key on a transit engine mount.
|
||||
func TestEngineRequestUserRotateKeyOnTransitMount(t *testing.T) {
|
||||
srv, sealMgr, _ := setupTestServer(t)
|
||||
unsealServer(t, sealMgr, nil)
|
||||
|
||||
mountStubEngine(t, srv, "transit-mount", engine.EngineTypeTransit)
|
||||
|
||||
body := makeEngineRequest("transit-mount", "rotate-key")
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
|
||||
req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false})
|
||||
w := httptest.NewRecorder()
|
||||
srv.handleEngineRequest(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("user rotate-key on transit mount should be 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOperationAction verifies the action classification of operations.
|
||||
func TestOperationAction(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
|
||||
Reference in New Issue
Block a user