171 lines
7.1 KiB
Go
171 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/subtle"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.cin.su/m/Assistant/contracts/authority"
|
|
"git.cin.su/m/Assistant/contracts/identity"
|
|
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
|
authoritydomain "git.cin.su/m/Assistant/modules/authority/domain"
|
|
"git.cin.su/m/Assistant/platform/ids"
|
|
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
|
)
|
|
|
|
const (
|
|
delegatedControlPlaneActor identity.ActorID = "domovoy-control-plane-actor"
|
|
delegatedControlPlanePrincipal identity.PrincipalID = "domovoy-control-plane-principal"
|
|
delegatedControlPlaneTokenFile = "control_plane_service_token"
|
|
)
|
|
|
|
type delegatedControlPlaneAuthoritySource interface {
|
|
LoadDelegatedControlPlaneGrant(context.Context, authority.GrantID) (authority.DelegatedControlPlaneGrant, error)
|
|
RecordDelegatedControlPlaneAudit(context.Context, authority.DelegatedControlPlaneAudit) error
|
|
RevokeDelegatedControlPlaneGrant(context.Context, authority.GrantID, string) (bool, error)
|
|
}
|
|
|
|
type delegatedControlPlaneCore interface {
|
|
Session(context.Context, string) (identity.CreatorSession, error)
|
|
Binding(context.Context, identity.ActorID, identity.PrincipalID) (identity.PrincipalBinding, error)
|
|
}
|
|
|
|
type delegatedControlPlaneHandler struct {
|
|
authority delegatedControlPlaneAuthoritySource
|
|
core delegatedControlPlaneCore
|
|
credential func() (string, error)
|
|
now func() time.Time
|
|
newID func() (ids.ID, error)
|
|
}
|
|
|
|
func registerDelegatedControlPlaneRoutes(mux *http.ServeMux, handler delegatedControlPlaneHandler) {
|
|
mux.HandleFunc("POST /v1/internal/control-plane/authorize", handler.authorize)
|
|
mux.HandleFunc("POST /v1/internal/control-plane/completions", handler.complete)
|
|
mux.HandleFunc("POST /v1/creator/control-plane/grants/{grant}/revoke", handler.revoke)
|
|
}
|
|
|
|
func (h delegatedControlPlaneHandler) authorize(w http.ResponseWriter, r *http.Request) {
|
|
if !h.serviceAuthenticated(r) {
|
|
http.Error(w, "service principal denied", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
var request authority.DelegatedControlPlaneRequest
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, 8<<10))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&request); err != nil {
|
|
http.Error(w, "invalid delegated control-plane request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
grant, err := h.authority.LoadDelegatedControlPlaneGrant(r.Context(), request.GrantID)
|
|
if err != nil {
|
|
http.Error(w, "delegated authority denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
binding, err := h.core.Binding(r.Context(), delegatedControlPlaneActor, delegatedControlPlanePrincipal)
|
|
if err != nil || authoritydomain.ValidateDelegatedControlPlaneGrant(h.now().UnixMilli(), binding, grant, request) != nil {
|
|
_ = h.audit(r.Context(), grant, request, "DENY", "FAILED")
|
|
http.Error(w, "delegated authority denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
if err := h.audit(r.Context(), grant, request, "ALLOW", "PENDING"); err != nil {
|
|
http.Error(w, "delegated audit unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h delegatedControlPlaneHandler) complete(w http.ResponseWriter, r *http.Request) {
|
|
if !h.serviceAuthenticated(r) {
|
|
http.Error(w, "service principal denied", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
var value authority.DelegatedControlPlaneCompletion
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, 8<<10))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&value); err != nil || (value.Outcome != "COMPLETED" && value.Outcome != "FAILED") {
|
|
http.Error(w, "invalid delegated completion", http.StatusBadRequest)
|
|
return
|
|
}
|
|
grant, err := h.authority.LoadDelegatedControlPlaneGrant(r.Context(), value.Request.GrantID)
|
|
if err != nil {
|
|
http.Error(w, "delegated authority unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
if !authoritydomain.RequestMatchesDelegatedControlPlaneGrant(grant, value.Request) {
|
|
http.Error(w, "delegated completion denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
if err := h.audit(r.Context(), grant, value.Request, "ALLOW", value.Outcome); err != nil {
|
|
http.Error(w, "delegated audit unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h delegatedControlPlaneHandler) revoke(w http.ResponseWriter, r *http.Request) {
|
|
session, err := h.core.Session(r.Context(), r.Header.Get("Authorization"))
|
|
if err != nil {
|
|
http.Error(w, "creator session denied", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
grant, err := h.authority.LoadDelegatedControlPlaneGrant(r.Context(), authority.GrantID(r.PathValue("grant")))
|
|
if err != nil || !authoritydomain.CanIssuerRevokeDelegatedGrant(session.PrincipalID, grant) {
|
|
http.Error(w, "delegated grant revoke denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
ok, err := h.authority.RevokeDelegatedControlPlaneGrant(r.Context(), grant.ID, string(session.PrincipalID))
|
|
if err != nil || !ok {
|
|
http.Error(w, "delegated grant revoke denied", http.StatusConflict)
|
|
return
|
|
}
|
|
request := authority.DelegatedControlPlaneRequest{GrantID: grant.ID, TaskID: grant.TaskID, TaskRevision: grant.TaskRevision, Operation: authority.DelegatedOperationRevokeControlPlaneGrant, Resource: grant.Resource}
|
|
if err := h.audit(r.Context(), grant, request, "REVOKE", "REVOKED"); err != nil {
|
|
http.Error(w, "delegated audit unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h delegatedControlPlaneHandler) serviceAuthenticated(r *http.Request) bool {
|
|
if h.credential == nil {
|
|
return false
|
|
}
|
|
expected, err := h.credential()
|
|
presented := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
|
return err == nil && expected != "" && presented != "" && subtle.ConstantTimeCompare([]byte(expected), []byte(presented)) == 1
|
|
}
|
|
|
|
func (h delegatedControlPlaneHandler) audit(ctx context.Context, grant authority.DelegatedControlPlaneGrant, request authority.DelegatedControlPlaneRequest, decision, outcome string) error {
|
|
if h.newID == nil {
|
|
return errors.New("delegated audit id unavailable")
|
|
}
|
|
id, err := h.newID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return h.authority.RecordDelegatedControlPlaneAudit(ctx, authority.DelegatedControlPlaneAudit{AuditID: id, GrantID: grant.ID, ServiceActorID: delegatedControlPlaneActor, ServicePrincipalID: delegatedControlPlanePrincipal, TaskID: request.TaskID, TaskRevision: request.TaskRevision, Operation: request.Operation, Resource: request.Resource, Decision: decision, Outcome: outcome, RecordedAtUnixMillis: h.now().UnixMilli()})
|
|
}
|
|
|
|
func delegatedControlPlaneCredential() (string, error) {
|
|
ref := os.Getenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF")
|
|
if ref == "" {
|
|
return "", errors.New("control-plane service credential ref is required")
|
|
}
|
|
root := os.Getenv("CONTROL_PLANE_SERVICE_SECRET_ROOT")
|
|
if root == "" {
|
|
root = "/run/control-plane-secrets"
|
|
}
|
|
resolver, err := platformsecrets.NewFileResolver(root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumer: secretcontracts.RuntimeConsumerCommit, Path: filepath.Join(root, delegatedControlPlaneTokenFile)})
|
|
}
|