feat: manage existing credential actions with audited replay

This commit is contained in:
2026-11-01 19:10:37 +08:00
parent baa81c4923
commit 98ad60c0b8
18 changed files with 1350 additions and 53 deletions

View File

@@ -6,6 +6,7 @@ import (
"database/sql"
"encoding/json"
"errors"
"sort"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/consoleauth"
@@ -194,14 +195,17 @@ func applicationPermissions(in consoleauth.ApplicationInput) (auth.Permissions,
return p, p.Validate()
}
func validateApplicationInput(in consoleauth.ApplicationInput) error {
if in.Kind != "SET_PERMISSIONS" && in.Actions != nil {
return fault.Invalid("actions is only valid for SET_PERMISSIONS")
}
if in.ValidDays < 0 || in.ValidDays > 365 {
return fault.Invalid("valid_days must be 0 (permanent) or 1-365")
}
if in.GraceDays < 0 || in.GraceDays > 30 || (in.Kind != "ROTATE" && in.GraceDays != 0) {
return fault.Invalid("grace_days must be 0-30 for rotation only")
}
if in.Kind == "FINALIZE" && in.ValidDays != 0 {
return fault.Invalid("finalize cannot set validity")
if (in.Kind == "FINALIZE" || in.Kind == "SET_PERMISSIONS") && in.ValidDays != 0 {
return fault.Invalid("this change cannot set validity")
}
if err := validCredentialReason(in.Reason); err != nil {
return err
@@ -213,12 +217,15 @@ func validateApplicationInput(in consoleauth.ApplicationInput) error {
}
_, err := applicationPermissions(in)
return err
case "ROTATE", "SET_EXPIRY", "FINALIZE":
case "ROTATE", "SET_EXPIRY", "FINALIZE", "SET_PERMISSIONS":
if in.NamespaceID.Validate() != nil || in.CredentialID.Validate() != nil || in.ExpectedRevision == 0 || in.Application != "" || in.Namespace != "" || in.Environment != "" || in.Queue != "" || len(in.Capabilities) != 0 {
return fault.Invalid("invalid rotation fields")
return fault.Invalid("invalid credential change fields")
}
if in.Kind == "SET_PERMISSIONS" {
return validateCredentialChangeInput(CredentialChangeInput{NamespaceID: in.NamespaceID, CredentialID: in.CredentialID, ExpectedRevision: in.ExpectedRevision, Action: in.Kind, Actions: in.Actions, Reason: in.Reason})
}
default:
return fault.Invalid("kind must be CREATE, ROTATE, SET_EXPIRY or FINALIZE")
return fault.Invalid("kind must be CREATE, ROTATE, SET_EXPIRY, FINALIZE or SET_PERMISSIONS")
}
return nil
}
@@ -227,6 +234,12 @@ func (s *ConsoleApplicationStore) PrepareApplication(ctx context.Context, token
if err := validateApplicationInput(in); err != nil {
return out, err
}
if in.Kind == "SET_PERMISSIONS" {
out.Version = 3
// 固定排序副本,避免准备返回后由调用方改变切片或重放时重算意图。
out.Input.Actions = append([]string(nil), in.Actions...)
sort.Strings(out.Input.Actions)
}
err := WithTx(ctx, s.db, func(tx *sql.Tx) error {
if _, err := s.admin(ctx, tx, token); err != nil {
return err
@@ -279,7 +292,8 @@ func (s *ConsoleApplicationStore) ApplyApplication(ctx context.Context, token au
if err := validateApplicationInput(op.Input); err != nil {
return out, err
}
if (op.Version != 0 && op.Version != 2) || op.OperationID.Validate() != nil || op.ApplicationID.Validate() != nil || op.NamespaceID.Validate() != nil || op.CredentialID.Validate() != nil {
validVersion := (op.Input.Kind == "SET_PERMISSIONS" && op.Version == 3) || (op.Input.Kind != "SET_PERMISSIONS" && (op.Version == 0 || op.Version == 2))
if !validVersion || op.OperationID.Validate() != nil || op.ApplicationID.Validate() != nil || op.NamespaceID.Validate() != nil || op.CredentialID.Validate() != nil {
return out, fault.Invalid("invalid fixed operation identity")
}
if (op.Input.ValidDays == 0) != (op.ExpiresAt == nil) || (op.Input.GraceDays == 0) != (op.PreviousValidUntil == nil) {
@@ -293,7 +307,7 @@ func (s *ConsoleApplicationStore) ApplyApplication(ctx context.Context, token au
if op.QueueID != "" || op.NamespaceID != op.Input.NamespaceID || op.CredentialID != op.Input.CredentialID {
return out, fault.Invalid("credential identity mismatch")
}
change := CredentialOperation{Version: version, OperationID: op.OperationID, NamespaceID: op.NamespaceID, CredentialID: op.CredentialID, ExpectedRevision: op.Input.ExpectedRevision, Action: op.Input.Kind, Reason: op.Input.Reason, Token: op.Token, ExpiresAt: op.ExpiresAt, PreviousValidUntil: op.PreviousValidUntil}
change := CredentialOperation{Version: version, OperationID: op.OperationID, NamespaceID: op.NamespaceID, CredentialID: op.CredentialID, ExpectedRevision: op.Input.ExpectedRevision, Action: op.Input.Kind, Actions: op.Input.Actions, Reason: op.Input.Reason, Token: op.Token, ExpiresAt: op.ExpiresAt, PreviousValidUntil: op.PreviousValidUntil}
receipt, err := applyCredentialOperation(ctx, s.db, change, func(tx *sql.Tx) (string, error) {
actor, err := s.admin(ctx, tx, token)
if err != nil {