mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 21:11:30 +00:00
cbb47aaf5d
# Phase 11 — RBAC MCP tools
12 new tools in internal/mcp/tools_auth.go mirroring the Phase-4
+ Phase-7 HTTP surface so operators driving certctl from Claude
/ VS Code / any MCP client get the same management capability
the GUI + CLI already expose:
certctl_auth_me GET /v1/auth/me
certctl_auth_list_roles GET /v1/auth/roles
certctl_auth_get_role GET /v1/auth/roles/{id}
certctl_auth_create_role POST /v1/auth/roles
certctl_auth_update_role PUT /v1/auth/roles/{id}
certctl_auth_delete_role DELETE /v1/auth/roles/{id}
certctl_auth_list_permissions GET /v1/auth/permissions
certctl_auth_add_permission_to_role POST /v1/auth/roles/{id}/permissions
certctl_auth_remove_permission_from_role DELETE /v1/auth/roles/{id}/permissions/{perm}
certctl_auth_list_keys GET /v1/auth/keys
certctl_auth_assign_role_to_key POST /v1/auth/keys/{id}/roles
certctl_auth_revoke_role_from_key DELETE /v1/auth/keys/{id}/roles/{role_id}
Each tool routes through the existing HTTP client (no parallel
business logic), so permission gates fire server-side: a
non-admin caller's MCP tool invocation returns whatever 403 the
underlying HTTP handler emits, fenced via errorResult for LLM-
prompt-injection defense.
Input types in internal/mcp/types.go (AuthRoleIDInput,
AuthCreateRoleInput, AuthUpdateRoleInput,
AuthRolePermissionGrantInput, AuthRolePermissionRevokeInput,
AuthAssignKeyRoleInput, AuthRevokeKeyRoleInput) carry
jsonschema descriptions so the MCP consumer's tool catalogue
shows operator-friendly hints.
internal/mcp/tools_auth_test.go ships 14 tests:
- TestAuthMCP_AllToolsRegister (registration must not panic)
- TestAuthMCP_PathsAndMethods (table-driven, 12 rows pinning
each tool's HTTP method + URL)
- TestAuthMCP_ForbiddenSurfacesFencedError (12 tools × 403
mock → error surface)
internal/mcp/tools_per_tool_test.go's allHappyPathCases extended
with the 12 new rows so the in-memory dispatch coverage gate
(TestMCP_RegisterTools_DispatchableToolCount) stays green at the
new total of 139 registered tools.
Re-derived total via 'grep -cE "gomcp\.AddTool\(" internal/mcp/tools*.go':
133 (121 in tools.go + 12 in tools_auth.go).
# Phase 12 — negative-test coverage gate
Audit of the prompt's 12 negative-test paths against existing
coverage:
1. Missing actor → 401 ✓ TestRequirePermission_NoActorReturns401, TestRBACGate_NoActorReturns401
2. No roles → 403 ✓ TestRequirePermission_DeniedActorReturns403, TestRBACGate_AuditorRole_403sOnAdminRoutes
3. Role lacks specific perm → 403 ✓ same suite
4. Wrong scope → 403 ✓ TestAuthorizer_SpecificScopeMatchesExactID (wrongID arm)
5. Self-grant w/o auth.role.assign → 403 ✓ TestActorRoleService_GrantRequiresAuthRoleAssign
6. Bootstrap token wrong → 401 ✓ TestEnvTokenStrategy_WrongTokenReturnsInvalidToken, TestBootstrapHandler_Mint_WrongToken_401
7. Bootstrap used twice → 410 ✓ TestEnvTokenStrategy_OneShotConsumption, TestBootstrapHandler_Mint_TwiceReturns410
8. Bootstrap when admin exists → 410 ✓ TestEnvTokenStrategy_AdminExistsClosesPath, TestBootstrapHandler_Mint_AdminExists410
9. Role delete with assignees → 409 NEW: TestRoleService_DeleteWithActorsAssignedReturns409
10. Profile-edit loophole → gated ✓ TestProfileEdit_RequiresApprovalLoopholeClosed
11. Permission not in catalog → 400 ✓ TestRoleService_AddPermissionRejectsNonCanonical
12. Scope ID for nonexistent resource → 404 (validation deferred — no FK constraint between role_permissions.scope_id and the resource tables; documented for a future bundle)
Filled the gap at #9 with TestRoleService_DeleteWithActorsAssignedReturns409
which pins the repository sentinel pass-through (postgres FK
ON DELETE RESTRICT → repository.ErrAuthRoleInUse → service
returns the sentinel verbatim → handler maps to HTTP 409).
# Coverage gates
.github/coverage-thresholds.yml gains 2 entries:
- internal/auth: floor 85
- internal/service/auth: floor 85
.github/workflows/ci.yml's coverage test command extended with
./internal/auth/... and ./internal/api/router/... so the
threshold check has data to evaluate.
# Protocol-endpoint not-gated test (Category F)
internal/api/router/phase12_protocol_allowlist_test.go (new)
adds 3 router-level invariant tests:
- TestPhase12_ProtocolEndpointsNotGated: AST-walks router.go,
asserts no rbacGate(...) call references a path under any
protocol-endpoint prefix (/acme, /scep, /.well-known/est,
/.well-known/pki/ocsp, /.well-known/pki/crl).
- TestPhase12_IsProtocolEndpoint_CoversCanonicalPrefixes:
pins auth.IsProtocolEndpoint against the canonical prefix
set; if a future protocol lands without lockstep allowlist
update, this fails.
- TestPhase12_RBACGateRoutesAreUnderAPIv1: belt-and-braces —
every rbacGate-wrapped route MUST start with /api/v1/.
Catches accidental cross-prefix wraps.
Complements the existing TestRequirePermission_ProtocolEndpointBypassesGate
(middleware-level) + TestRouter_AuthExemptAllowlist_PinsActualRegistrations
(allowlist drift) so the Category F invariant is pinned at all
three layers (middleware + router + dispatch).
# Verifications
* gofmt clean repo-wide.
* go vet ./... clean.
* staticcheck across internal/auth + handler + router + cli +
service + repository + cmd + domain + mcp: clean.
* go test -short -count=1 green across internal/auth (incl.
bootstrap), internal/api/handler, internal/api/router,
internal/cli, internal/service (incl. auth),
internal/domain/auth, internal/mcp, cmd/server, cmd/cli.
108 lines
4.0 KiB
YAML
108 lines
4.0 KiB
YAML
# Coverage floors per gated package.
|
|
#
|
|
# Each entry: floor: <integer percentage>, why: <load-bearing context>.
|
|
# Adding a new gated package: one entry here; CI's `Check Coverage Thresholds`
|
|
# step auto-picks up. Lowering a floor REQUIRES corresponding code-side test
|
|
# work — never lower the gate to make CI green.
|
|
#
|
|
# Per ci-pipeline-cleanup bundle Phase 2 / frozen decision 0.3.
|
|
|
|
internal/service:
|
|
floor: 70
|
|
why: |
|
|
Bundle R-CI-extended raise (post-Bundle-N.C-extended): service
|
|
55 → 70. HEAD 73.4% (3pp margin). Prescribed Bundle R target
|
|
was 80; held lower to avoid false-positives on single low-
|
|
coverage files dragging the global per-file-average down.
|
|
|
|
internal/api/handler:
|
|
floor: 75
|
|
why: |
|
|
Bundle R-CI-extended raise: handler 60 → 75. HEAD 79.8% (4pp
|
|
margin). Prescribed Bundle R target was 80; held lower for
|
|
same reason as service layer.
|
|
|
|
internal/domain:
|
|
floor: 40
|
|
why: |
|
|
Domain layer is mostly type definitions + validators; 40% is
|
|
the load-bearing-paths floor.
|
|
|
|
internal/api/middleware:
|
|
floor: 30
|
|
why: |
|
|
Middleware coverage is per-handler-test-driven. 30% is the
|
|
floor that catches the wired-up middleware paths; the
|
|
unwired paths (alternative auth providers not currently
|
|
enabled) sit below.
|
|
|
|
internal/crypto:
|
|
floor: 88
|
|
why: |
|
|
Bundle R closure CI checkpoint #3: crypto floor lifted 85 → 88.
|
|
Post-Bundle-Q package-scoped coverage at HEAD: 88.2%. The
|
|
remaining ~12% gap is platform-failure branches (rand.Reader /
|
|
aes.NewCipher) that require interface seams the production
|
|
code doesn't use; closing them is tracked as R-CI-extended,
|
|
not Bundle R scope.
|
|
|
|
internal/connector/issuer/local:
|
|
floor: 86
|
|
why: |
|
|
Bundle R closure CI checkpoint #3: local-issuer floor lifted
|
|
85 → 86. Post-Bundle-Q package-scoped coverage at HEAD: 86.7%.
|
|
The prescribed Bundle R target was 92, but reaching it
|
|
requires interface seams for crypto/x509 signing-error
|
|
branches — tracked as R-CI-extended.
|
|
|
|
internal/connector/issuer/acme:
|
|
floor: 80
|
|
why: |
|
|
Bundle R-CI-extended threshold raise (post-Bundle-J-extended):
|
|
ACME 50 → 80. The Pebble-style mock + per-CA failure tests
|
|
lift package-scoped ACME to 85.4%; gate at 80 with 5pp margin
|
|
to absorb the global-run per-file-average dip.
|
|
|
|
internal/connector/issuer/stepca:
|
|
floor: 80
|
|
why: |
|
|
Bundle L.B / Coverage-Audit C-005 — StepCA failure-mode + JWE
|
|
round-trip tests lift package from 52.1% to 90.4% (per-package
|
|
run). Floor at 80 with margin.
|
|
|
|
internal/mcp:
|
|
floor: 85
|
|
why: |
|
|
Bundle K / Coverage-Audit C-002 — MCP per-tool dispatch via
|
|
in-memory transport lifts package from 28.0% to 93.1% (per-
|
|
package run). Floor at 85.
|
|
|
|
internal/auth:
|
|
floor: 85
|
|
why: |
|
|
Bundle 1 Phase 12 — RBAC primitive coverage gate.
|
|
internal/auth ships keystore + middleware + RequirePermission +
|
|
bootstrap + the Phase-3 context keys + the protocol-endpoint
|
|
allowlist. Negative-test coverage (no actor → 401, no role →
|
|
403, wrong scope → 403, bootstrap-token-wrong → 401, bootstrap-
|
|
used-twice → 410, admin-already-exists → 410, zero-length token
|
|
rejection) is now in place. Prescribed Bundle 1 target was 90;
|
|
held at 85 to absorb the per-file-average dip from the
|
|
middleware shim files (testfixtures.go) which CI runs but only
|
|
test fixtures exercise. Sub-package internal/auth/bootstrap
|
|
inherits this floor.
|
|
|
|
internal/service/auth:
|
|
floor: 85
|
|
why: |
|
|
Bundle 1 Phase 12 — RBAC service-layer coverage gate.
|
|
PermissionService + RoleService + ActorRoleService + Authorizer
|
|
each have positive + negative tests covering the
|
|
privilege-escalation guard (auth.role.assign required for
|
|
Grant/Revoke), the reserved-actor invariant (actor-demo-anon
|
|
cannot be mutated), the canonical-permission validation, the
|
|
role-in-use guard on Delete, and every sentinel-error path
|
|
(ErrUnauthenticated / ErrForbidden / ErrSelfRoleAssignment /
|
|
ErrAuthReservedActor / ErrAuthUnknownPermission /
|
|
ErrAuthRoleInUse).
|