Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions boatstack/cmd/boatstack-helper/delegation_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func runFlowAuthorize(arguments []string) error {
if err != nil {
return err
}
programChange, err := preflightDelegatedProgramChange(context.Background(), resolveRequest)
programChange, err := preflightFlowAuthorizationProgramChange(context.Background(), resolveRequest)
if err != nil {
return err
}
Expand Down Expand Up @@ -183,6 +183,19 @@ func authorizeDelegation(existing *delegation.Record, request delegation.Request
return record, true, nil
}
if existing.RequestFingerprint != requestFingerprint || existing.Actor != actor || existing.ActorIdentityRole != request.HumanIdentityRole || existing.ActorIdentityProviderFingerprint != identityProviderFingerprint || existing.Status != "active" {
if existing.RequestFingerprint == requestFingerprint && existing.Actor == actor && existing.ActorIdentityRole == request.HumanIdentityRole && existing.ActorIdentityProviderFingerprint == identityProviderFingerprint && (existing.Status == "revoked" || existing.Status == "completed") {
reauthorized := *existing
reauthorized.Revision++
reauthorized.AuthorizedAt = now
reauthorized.ExpiresAt = time.Time{}
if expiresIn > 0 {
reauthorized.ExpiresAt = now.Add(expiresIn)
}
reauthorized.ReceiptID = authorizationReceiptID(requestFingerprint, actor, request.HumanIdentityRole, identityProviderFingerprint, reauthorized.Revision, now)
reauthorized.Status = "active"
reauthorized.RevokedAt, reauthorized.EndedAt, reauthorized.EndReason = time.Time{}, time.Time{}, ""
return reauthorized, true, nil
}
return delegation.Record{}, false, fmt.Errorf("DELEGATION_CONFLICT: run already has a different authorization, actor, or status")
}
if existing.ExpiresAt.IsZero() || now.Before(existing.ExpiresAt) {
Expand Down Expand Up @@ -326,7 +339,7 @@ func runFlowContinuationOptions(options commandOptions) error {
if err := advanceContinuation(&options, response); err != nil {
return err
}
if response.Delegation != nil || response.CommitRequired != nil || response.Prescription == nil || response.Receipt == nil || (response.Decision != nil && response.Decision.Kind == supervisor.DecisionTerminal) {
if response.Authorization != nil || response.CommitRequired != nil || response.Prescription == nil || response.Receipt == nil || (response.Decision != nil && response.Decision.Kind == supervisor.DecisionTerminal) {
return renderResponse(response, options.format)
}
}
Expand All @@ -342,14 +355,14 @@ func executeContinuationStep(ctx context.Context, options commandOptions) (surfa
if err != nil {
return surfaces.Response{}, err
}
programChangeResponse, err := preflightDelegatedProgramChange(ctx, resolveRequest)
programChangeResponse, err := preflightFlowAuthorizationProgramChange(ctx, resolveRequest)
if err != nil {
return surfaces.Response{}, err
}
if programChangeResponse != nil {
return *programChangeResponse, nil
}
_, delegationResponse, err := prepareDelegation(ctx, &resolveRequest)
_, delegationResponse, err := prepareFlowAuthorization(ctx, &resolveRequest)
if err != nil {
return surfaces.Response{}, err
}
Expand All @@ -370,7 +383,7 @@ func executeContinuationStep(ctx context.Context, options commandOptions) (surfa
}
resolved, err := handleWithHumanIdentity(ctx, kernel, resolveRequest)
resolveLease.Release()
if settleErr := settleDelegationAtTarget(ctx, resolveRequest, resolved, kernel.TargetSatisfied(resolved.Snapshot, resolveRequest.Objective), false); settleErr != nil && err == nil {
if settleErr := settleFlowAuthorizationAtTarget(ctx, resolveRequest, resolved, kernel.TargetSatisfied(resolved.Snapshot, resolveRequest.Objective), false); settleErr != nil && err == nil {
err = settleErr
}
if err == nil {
Expand All @@ -397,14 +410,14 @@ func executeContinuationStep(ctx context.Context, options commandOptions) (surfa
if err != nil {
return surfaces.Response{}, err
}
programChangeResponse, err = preflightDelegatedProgramChange(ctx, resolveRequest)
programChangeResponse, err = preflightFlowAuthorizationProgramChange(ctx, resolveRequest)
if err != nil {
return surfaces.Response{}, err
}
if programChangeResponse != nil {
return *programChangeResponse, nil
}
_, delegationResponse, err = prepareDelegation(ctx, &resolveRequest)
_, delegationResponse, err = prepareFlowAuthorization(ctx, &resolveRequest)
if err != nil {
return surfaces.Response{}, err
}
Expand All @@ -425,7 +438,7 @@ func executeContinuationStep(ctx context.Context, options commandOptions) (surfa
}
resolved, err = handleWithHumanIdentity(ctx, kernel, resolveRequest)
resolveLease.Release()
if settleErr := settleDelegationAtTarget(ctx, resolveRequest, resolved, kernel.TargetSatisfied(resolved.Snapshot, resolveRequest.Objective), false); settleErr != nil && err == nil {
if settleErr := settleFlowAuthorizationAtTarget(ctx, resolveRequest, resolved, kernel.TargetSatisfied(resolved.Snapshot, resolveRequest.Objective), false); settleErr != nil && err == nil {
err = settleErr
}
if err != nil || resolved.Prescription == nil {
Expand All @@ -452,7 +465,7 @@ func executeContinuationStep(ctx context.Context, options commandOptions) (surfa
if applyRequest.InputRequest != nil {
return surfaces.Response{SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, ProgramID: applyRequest.ProgramID, EntryID: applyRequest.EntryID, RunID: applyRequest.FlowID, InputRequest: applyRequest.InputRequest}, nil
}
delegationLock, delegationResponse, err := prepareDelegation(ctx, &applyRequest)
delegationLock, delegationResponse, err := prepareFlowAuthorization(ctx, &applyRequest)
if err != nil {
return surfaces.Response{}, err
}
Expand All @@ -467,7 +480,7 @@ func executeContinuationStep(ctx context.Context, options commandOptions) (surfa
}
applied, err := handleWithHumanIdentity(ctx, kernel, applyRequest)
targetSatisfied := kernel.TargetSatisfied(applied.Snapshot, applyRequest.Objective)
if settleErr := settleDelegationAtTarget(ctx, applyRequest, applied, targetSatisfied, delegationLock != nil); settleErr != nil && err == nil {
if settleErr := settleFlowAuthorizationAtTarget(ctx, applyRequest, applied, targetSatisfied, delegationLock != nil); settleErr != nil && err == nil {
err = settleErr
}
if err != nil {
Expand Down
65 changes: 47 additions & 18 deletions boatstack/cmd/boatstack-helper/delegation_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func canReprojectDelegation(layout ports.ControllerLayout, invocation model.Invo
prior.TargetID != current.TargetID || prior.ObjectiveID != current.ObjectiveID || prior.DeliveryID != current.DeliveryID ||
prior.RepositoryID != current.RepositoryID || prior.GitCommonID != current.GitCommonID ||
prior.BindingFingerprint != current.BindingFingerprint || prior.Description != current.Description ||
!sameStringSet(prior.InputFingerprints, current.InputFingerprints) || !sameStringSet(prior.RequestedAuthorities, current.RequestedAuthorities) {
!equalEntryInputFingerprints(prior.InputFingerprints, current.InputFingerprints) || !sameStringSet(prior.EntryActivationAuthorities, current.EntryActivationAuthorities) || !sameStringSet(prior.RequestedAuthorities, current.RequestedAuthorities) {
return false, nil
}
initial := invocation
Expand Down Expand Up @@ -69,8 +69,8 @@ func sameStringSet(left, right []string) bool {
return slices.Equal(left, right)
}

func prepareDelegation(ctx context.Context, request *surfaces.Request) (ports.Lock, *surfaces.Response, error) {
if request.ProgramID == "" || len(request.DelegatedAuthorities) == 0 {
func prepareFlowAuthorization(ctx context.Context, request *surfaces.Request) (ports.Lock, *surfaces.Response, error) {
if request.ProgramID == "" || (len(request.EntryActivationAuthorities) == 0 && len(request.DelegatedAuthorities) == 0) {
return nil, nil, nil
}
presentation, err := humanIdentityPresentationForRequest(*request)
Expand Down Expand Up @@ -123,20 +123,21 @@ func prepareDelegation(ctx context.Context, request *surfaces.Request) (ports.Lo
if request.Operation == surfaces.OperationExplain {
return nil, nil, nil
}
response, responseErr := delegationRequiredResponse(*request)
response, responseErr := flowAuthorizationRequiredResponse(*request)
return nil, response, responseErr
}
if err != nil {
releaseOnError()
return nil, nil, err
}
if record.RequestFingerprint != request.DelegationRequestFingerprint || record.Request.RunID != request.FlowID || record.Request.ProgramID != request.ProgramID || record.Request.ProgramFingerprint != request.ProgramFingerprint || record.Request.ControlBundleFingerprint != request.ControlBundleFingerprint || record.Request.EntryID != request.EntryID || record.Request.TargetID != string(request.Objective.TargetID) || record.Request.ObjectiveID != request.Objective.ID || record.Request.DeliveryID != request.Objective.DeliveryID || record.Request.RepositoryID != invocation.RepositoryID || record.Request.GitCommonID != invocation.GitCommonID || record.Request.BindingFingerprint != request.DelegationBindingFingerprint || record.Request.HumanIdentityRole != presentation.Role || record.Request.HumanIdentityProviderFingerprint != presentation.ProviderFingerprint || record.ActorIdentityRole != presentation.Role || record.ActorIdentityProviderFingerprint != presentation.ProviderFingerprint {
if record.RequestFingerprint != request.DelegationRequestFingerprint || record.Request.RunID != request.FlowID || record.Request.ProgramID != request.ProgramID || record.Request.ProgramFingerprint != request.ProgramFingerprint || record.Request.ControlBundleFingerprint != request.ControlBundleFingerprint || record.Request.EntryID != request.EntryID || record.Request.TargetID != string(request.Objective.TargetID) || record.Request.ObjectiveID != request.Objective.ID || record.Request.DeliveryID != request.Objective.DeliveryID || record.Request.RepositoryID != invocation.RepositoryID || record.Request.GitCommonID != invocation.GitCommonID || record.Request.BindingFingerprint != request.DelegationBindingFingerprint || !sameAuthorityClasses(record.Request.EntryActivationAuthorities, request.EntryActivationAuthorities) || record.Request.HumanIdentityRole != presentation.Role || record.Request.HumanIdentityProviderFingerprint != presentation.ProviderFingerprint || record.ActorIdentityRole != presentation.Role || record.ActorIdentityProviderFingerprint != presentation.ProviderFingerprint {
current := record.Request
current.RunID, current.ProgramID, current.ProgramFingerprint, current.ControlBundleFingerprint = request.FlowID, request.ProgramID, request.ProgramFingerprint, request.ControlBundleFingerprint
current.EntryID, current.TargetID, current.ObjectiveID, current.DeliveryID = request.EntryID, string(request.Objective.TargetID), request.Objective.ID, request.Objective.DeliveryID
current.RepositoryID, current.GitCommonID = invocation.RepositoryID, invocation.GitCommonID
current.InitialWorktreeID, current.InitialRef = invocation.WorktreeID, invocation.Ref
current.BindingFingerprint, current.HumanIdentityRole, current.HumanIdentityProviderFingerprint = request.DelegationBindingFingerprint, presentation.Role, presentation.ProviderFingerprint
current.EntryActivationAuthorities = authorityStrings(request.EntryActivationAuthorities)
current.RequestedAuthorities = make([]string, len(request.DelegatedAuthorities))
for index, authority := range request.DelegatedAuthorities {
current.RequestedAuthorities[index] = string(authority)
Expand All @@ -157,7 +158,7 @@ func prepareDelegation(ctx context.Context, request *surfaces.Request) (ports.Lo
if request.Operation == surfaces.OperationExplain {
return nil, nil, nil
}
response, responseErr := delegationRequiredResponse(*request)
response, responseErr := flowAuthorizationRequiredResponse(*request)
return nil, response, responseErr
}
return nil, nil, fmt.Errorf("DELEGATION_DRIFT: authorization does not match the current run context")
Expand All @@ -173,24 +174,36 @@ func prepareDelegation(ctx context.Context, request *surfaces.Request) (ports.Lo
releaseOnError()
return nil, nil, fmt.Errorf("DELEGATION_CONTEXT_UNAUTHORIZED: current worktree is not in the verified run lineage")
}
if record.Status == "completed" && (request.Operation == surfaces.OperationResolve || request.Operation == surfaces.OperationExplain) {
// A completed delegation carries no authority, but resolving the exact
// bound run remains safe and lets restarts replay its terminal state.
return nil, nil, nil
if record.Status == "completed" {
if request.Operation == surfaces.OperationResolve || request.Operation == surfaces.OperationExplain {
// A completed authorization carries no authority, but resolving the
// exact bound run remains safe and lets restarts replay its terminal
// state. If current evidence makes the target nonterminal again, the
// resulting apply will require a fresh exact authorization below.
return nil, nil, nil
}
releaseOnError()
response, responseErr := flowAuthorizationRequiredResponse(*request)
return nil, response, responseErr
}
if record.Status != "active" {
releaseOnError()
if request.Operation == surfaces.OperationExplain {
return nil, nil, nil
}
if record.Status == "revoked" {
response, responseErr := flowAuthorizationRequiredResponse(*request)
return nil, response, responseErr
}
return nil, nil, fmt.Errorf("DELEGATION_REVOKED: run authorization is %s", record.Status)
}
if !record.ExpiresAt.IsZero() && !time.Now().UTC().Before(record.ExpiresAt) {
releaseOnError()
if request.Operation == surfaces.OperationExplain {
return nil, nil, nil
}
return nil, nil, fmt.Errorf("DELEGATION_EXPIRED: run authorization expired")
response, responseErr := flowAuthorizationRequiredResponse(*request)
return nil, response, responseErr
}
for _, authority := range request.DelegatedAuthorities {
receiptDigest := sha256.Sum256([]byte(record.ReceiptID + "\x00" + string(authority)))
Expand All @@ -202,23 +215,39 @@ func prepareDelegation(ctx context.Context, request *surfaces.Request) (ports.Lo
return lock, nil, nil
}

func delegationRequiredResponse(request surfaces.Request) (*surfaces.Response, error) {
func flowAuthorizationRequiredResponse(request surfaces.Request) (*surfaces.Response, error) {
presentation, err := humanIdentityPresentationForRequest(request)
if err != nil {
return nil, err
}
code := "DELEGATION_REQUIRED"
if len(request.EntryActivationAuthorities) != 0 {
code = "ENTRY_ACTIVATION_AUTHORITY_REQUIRED"
}
return &surfaces.Response{
SchemaVersion: surfaces.SchemaVersion, Operation: request.Operation, ProgramID: request.ProgramID, EntryID: request.EntryID, RunID: request.FlowID, Objective: request.Objective,
Delegation: &surfaces.DelegationRequired{Code: "DELEGATION_REQUIRED", RunID: request.FlowID, RequestFingerprint: request.DelegationRequestFingerprint, Authorities: append([]catalog.AuthorityClass(nil), request.DelegatedAuthorities...), Description: "Explicitly authorize " + request.ProgramID + "/" + request.EntryID + " for this exact run", HumanIdentity: presentation},
Authorization: &surfaces.FlowAuthorizationRequired{Code: code, RunID: request.FlowID, RequestFingerprint: request.DelegationRequestFingerprint, EntryActivationAuthorities: append([]catalog.AuthorityClass(nil), request.EntryActivationAuthorities...), DelegatedAuthorities: append([]catalog.AuthorityClass(nil), request.DelegatedAuthorities...), Description: "Explicitly authorize " + request.ProgramID + "/" + request.EntryID + " for this exact run", HumanIdentity: presentation},
}, nil
}

// preflightDelegatedProgramChange observes the selected program before any
func authorityStrings(values []catalog.AuthorityClass) []string {
result := make([]string, len(values))
for index, value := range values {
result[index] = string(value)
}
return result
}

func sameAuthorityClasses(left []string, right []catalog.AuthorityClass) bool {
return sameStringSet(left, authorityStrings(right))
}

// preflightFlowAuthorizationProgramChange observes the selected program before any
// product delegation is requested. Reconciliation changes the control bundle,
// so authorizing against the prior bundle would create an authorization that
// must be rejected immediately after the accepted maintenance transition.
func preflightDelegatedProgramChange(ctx context.Context, request surfaces.Request) (*surfaces.Response, error) {
if request.ProgramID == "" || len(request.DelegatedAuthorities) == 0 || request.Operation == surfaces.OperationExplain {
func preflightFlowAuthorizationProgramChange(ctx context.Context, request surfaces.Request) (*surfaces.Response, error) {
if request.ProgramID == "" || (len(request.EntryActivationAuthorities) == 0 && len(request.DelegatedAuthorities) == 0) || request.Operation == surfaces.OperationExplain {
return nil, nil
}
probe := request
Expand Down Expand Up @@ -263,10 +292,10 @@ func isExactProgramChangeSuspension(response surfaces.Response) bool {
response.ProgramChange.AcceptanceFlag == "--accept-program-change"
}

func settleDelegationAtTarget(ctx context.Context, request surfaces.Request, response surfaces.Response, targetSatisfied, lockHeld bool) error {
func settleFlowAuthorizationAtTarget(ctx context.Context, request surfaces.Request, response surfaces.Response, targetSatisfied, lockHeld bool) error {
terminalDecision := response.Decision != nil && response.Decision.Kind == supervisor.DecisionTerminal
committedTarget := response.Receipt != nil && targetSatisfied
if len(request.DelegatedAuthorities) == 0 || (!terminalDecision && !committedTarget) {
if (len(request.EntryActivationAuthorities) == 0 && len(request.DelegatedAuthorities) == 0) || (!terminalDecision && !committedTarget) {
Comment thread
bigboateng marked this conversation as resolved.
return nil
}
resolver, err := plant.NewResolver("")
Expand Down
6 changes: 3 additions & 3 deletions boatstack/cmd/boatstack-helper/flow_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol"
)

const flowCompilerVersion = "control-program.compiler.7"
const flowCompilerVersion = "control-program.compiler.8"

type flowCommandOptions struct {
repository string
Expand Down Expand Up @@ -345,8 +345,8 @@ func validateDeclarativeFlow(compiled controlprogram.Compiled) error {
facets[facet.ID] = facet
}
for _, entry := range compiled.Document.Entries {
if entry.Delegation != nil || entry.Diagnostics != nil {
return fmt.Errorf("FLOW_RUNTIME_INVALID: declarative entries do not support delegation or domain diagnostics")
if entry.Delegation != nil || entry.Diagnostics != nil || len(entry.Requires.Authorities) != 0 {
return fmt.Errorf("FLOW_RUNTIME_INVALID: declarative entries do not support activation authority, delegation, or domain diagnostics")
}
}
for _, work := range compiled.Document.Work {
Expand Down
Loading
Loading