mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 14:51:30 +00:00
21aeed4f4e
Phase 0 closure (Path B2, post-rewrite):
addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:
// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).
Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.
Generated via:
addlicense -c "certctl LLC" -y 2026 \
-f cowork/legal/copyright-header.tpl \
-ignore '**/testdata/**' -ignore '**/*_test.go' \
cmd/ internal/
Verification:
find cmd internal -name '*.go' -not -name '*_test.go' \
-not -path '*/testdata/*' \
-exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l
Returns: 0
gofmt clean. Header additions are comments only, no compile impact.
Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
138 lines
5.0 KiB
Go
138 lines
5.0 KiB
Go
// Copyright 2026 certctl LLC. All rights reserved.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/certctl-io/certctl/internal/auth"
|
|
"github.com/certctl-io/certctl/internal/domain"
|
|
authdomain "github.com/certctl-io/certctl/internal/domain/auth"
|
|
)
|
|
|
|
// DemoResidualCleanupFn deletes every live actor_roles row for the
|
|
// synthetic actor-demo-anon and returns the count removed. Provided by
|
|
// cmd/server/main.go which holds the *sql.DB. Returning an error from
|
|
// this func surfaces as HTTP 500; returning (0, nil) is the legitimate
|
|
// "nothing to clean up" idempotent response.
|
|
type DemoResidualCleanupFn func(ctx context.Context) (int64, error)
|
|
|
|
// DemoResidualHandler exposes POST /api/v1/auth/demo-residual/cleanup —
|
|
// an admin-gated convenience endpoint that removes residual
|
|
// actor-demo-anon role grants from a deployment that previously ran
|
|
// CERTCTL_AUTH_TYPE=none (or any deployment, since migration 000029
|
|
// seeds the row unconditionally). Audit 2026-05-11 A-8 closure.
|
|
//
|
|
// The endpoint refuses to run when the server is currently in demo
|
|
// mode (Auth.Type == "none") because the residual IS the active
|
|
// runtime state at that auth type; deleting it would break the demo
|
|
// path. The 503 response makes the constraint observable to the GUI.
|
|
type DemoResidualHandler struct {
|
|
cleanup DemoResidualCleanupFn
|
|
authType func() string
|
|
auditWriter AuditWriter
|
|
}
|
|
|
|
// AuditWriter is the minimal projection of *service.AuditService that
|
|
// the DemoResidualHandler uses. Kept local to avoid pulling the full
|
|
// service package into the handler's import set.
|
|
type AuditWriter interface {
|
|
RecordEventWithCategory(
|
|
ctx context.Context, actor string, actorType domain.ActorType,
|
|
action, eventCategory, resourceType, resourceID string,
|
|
details map[string]interface{},
|
|
) error
|
|
}
|
|
|
|
// NewDemoResidualHandler wires the cleanup function and auth-type
|
|
// getter. authType is a closure so the handler always sees the
|
|
// live config value (post-startup mutation is unsupported, but
|
|
// the closure pattern keeps the dependency direction clean).
|
|
func NewDemoResidualHandler(
|
|
cleanup DemoResidualCleanupFn,
|
|
authType func() string,
|
|
audit AuditWriter,
|
|
) DemoResidualHandler {
|
|
return DemoResidualHandler{
|
|
cleanup: cleanup,
|
|
authType: authType,
|
|
auditWriter: audit,
|
|
}
|
|
}
|
|
|
|
// demoResidualCleanupResponse is the JSON body returned by POST
|
|
// /api/v1/auth/demo-residual/cleanup. Removed is the count of
|
|
// actor_roles rows that were live for actor-demo-anon at the time
|
|
// of the call. Always present; idempotent calls return removed=0.
|
|
type demoResidualCleanupResponse struct {
|
|
Removed int64 `json:"removed"`
|
|
}
|
|
|
|
// Cleanup handles POST /api/v1/auth/demo-residual/cleanup. RBAC-gated
|
|
// at the router via auth.role.assign (the admin-class permission).
|
|
// Rejects requests when the server is in demo mode (Auth.Type=none)
|
|
// with HTTP 503. Emits an audit row recording the count removed +
|
|
// the caller actor on every successful run.
|
|
func (h DemoResidualHandler) Cleanup(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
if h.cleanup == nil {
|
|
_ = Error(w, http.StatusInternalServerError, "demo-residual cleanup not configured")
|
|
return
|
|
}
|
|
|
|
authType := ""
|
|
if h.authType != nil {
|
|
authType = h.authType()
|
|
}
|
|
if authType == "none" {
|
|
// Refusing to "clean up" the active demo-mode state. The
|
|
// GUI surface should hide the button when /api/v1/auth/info
|
|
// reports auth_type=none; this guard is defense-in-depth.
|
|
_ = Error(w, http.StatusServiceUnavailable,
|
|
"demo-residual cleanup refused: server is currently in demo mode (CERTCTL_AUTH_TYPE=none); the actor-demo-anon grants are the active runtime state at this auth type")
|
|
return
|
|
}
|
|
|
|
removed, err := h.cleanup(ctx)
|
|
if err != nil {
|
|
_ = Error(w, http.StatusInternalServerError, "demo-residual cleanup failed")
|
|
return
|
|
}
|
|
|
|
// Audit row records the count removed + the caller. The actor is
|
|
// pulled from the request context (set by the auth middleware
|
|
// chain after the rbacGate at the router level has authorized).
|
|
if h.auditWriter != nil {
|
|
actorID, _ := r.Context().Value(auth.ActorIDKey{}).(string)
|
|
if actorID == "" {
|
|
actorID = "unknown"
|
|
}
|
|
actorTypeRaw, _ := r.Context().Value(auth.ActorTypeKey{}).(string)
|
|
actorType := domain.ActorType(actorTypeRaw)
|
|
if actorType == "" {
|
|
actorType = domain.ActorTypeAPIKey
|
|
}
|
|
_ = h.auditWriter.RecordEventWithCategory(
|
|
ctx, actorID, actorType,
|
|
"auth.demo_residual_grants_cleaned",
|
|
domain.EventCategoryAuth,
|
|
"actor_roles", authdomain.DemoAnonActorID,
|
|
map[string]interface{}{"removed": removed},
|
|
)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(demoResidualCleanupResponse{Removed: removed})
|
|
}
|
|
|
|
// ErrDemoResidualNotConfigured is returned by callers that probe the
|
|
// handler's wiring state. Currently unused outside tests but exported
|
|
// to keep the contract observable for documentation purposes.
|
|
var ErrDemoResidualNotConfigured = errors.New("demo-residual cleanup not configured")
|