feat: manage existing credential actions with audited replay
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -16,17 +17,20 @@ import (
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/credentialvault"
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
|
||||
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||
"git.yuntuo.hk/k2safe/orbit/platform/auth"
|
||||
)
|
||||
|
||||
func runCredential(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("usage: orbit-admin credential status|rotate|expiry|finalize|revoke|apply")
|
||||
return errors.New("usage: orbit-admin credential status|permissions|rotate|expiry|finalize|revoke|apply")
|
||||
}
|
||||
switch args[0] {
|
||||
case "status":
|
||||
return runCredentialStatus(args[1:])
|
||||
case "rotate":
|
||||
return runCredentialChange(args[1:], "ROTATE")
|
||||
case "permissions":
|
||||
return runCredentialChange(args[1:], "SET_PERMISSIONS")
|
||||
case "expiry":
|
||||
return runCredentialChange(args[1:], "SET_EXPIRY")
|
||||
case "finalize":
|
||||
@@ -36,7 +40,7 @@ func runCredential(args []string) error {
|
||||
case "apply":
|
||||
return runCredentialApply(args[1:])
|
||||
default:
|
||||
return errors.New("usage: orbit-admin credential status|rotate|expiry|finalize|revoke|apply")
|
||||
return errors.New("usage: orbit-admin credential status|permissions|rotate|expiry|finalize|revoke|apply")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +79,11 @@ func runCredentialChange(args []string, action string) error {
|
||||
grace = 7 * 24 * time.Hour
|
||||
}
|
||||
var expected uint64
|
||||
var actionList string
|
||||
prepareOnly := flags.Bool("prepare-only", false, "save the fixed private operation without applying it; later use credential apply")
|
||||
if action == "SET_PERMISSIONS" {
|
||||
flags.StringVar(&actionList, "actions", "", "complete comma-separated permission actions; existing queue scope is preserved")
|
||||
}
|
||||
flags.StringVar(&namespace, "namespace-id", "", "credential namespace id")
|
||||
flags.StringVar(&credential, "credential-id", "", "credential id")
|
||||
flags.Uint64Var(&expected, "expected-revision", 0, "current credential revision required for first apply")
|
||||
@@ -92,6 +101,22 @@ func runCredentialChange(args []string, action string) error {
|
||||
if err != nil || expected == 0 {
|
||||
return errors.New("credential change requires valid ids and a nonzero --expected-revision")
|
||||
}
|
||||
var actions []string
|
||||
if action == "SET_PERMISSIONS" {
|
||||
invalidLifetime := false
|
||||
flags.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "valid-for" || f.Name == "grace" {
|
||||
invalidLifetime = true
|
||||
}
|
||||
})
|
||||
if invalidLifetime {
|
||||
return errors.New("permission changes preserve validity and overlap; omit --valid-for and --grace")
|
||||
}
|
||||
actions, err = parseCredentialActions(actionList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
duration := time.Duration(0)
|
||||
if action == "ROTATE" || action == "SET_EXPIRY" {
|
||||
duration, err = time.ParseDuration(validFor)
|
||||
@@ -105,7 +130,7 @@ func runCredentialChange(args []string, action string) error {
|
||||
}
|
||||
defer closeDB()
|
||||
op, err := mysql.PrepareCredentialOperation(ctx, db, mysql.CredentialChangeInput{
|
||||
NamespaceID: ns, CredentialID: id, ExpectedRevision: expected, Action: action, ValidFor: duration, GraceFor: grace, Reason: reason,
|
||||
NamespaceID: ns, CredentialID: id, ExpectedRevision: expected, Action: action, ValidFor: duration, GraceFor: grace, Reason: reason, Actions: actions,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("credential operation could not be prepared")
|
||||
@@ -113,6 +138,12 @@ func runCredentialChange(args []string, action string) error {
|
||||
if err := writePrivateOperation(output, op); err != nil {
|
||||
return err
|
||||
}
|
||||
if *prepareOnly {
|
||||
return json.NewEncoder(os.Stdout).Encode(struct {
|
||||
OperationID core.ID `json:"operation_id"`
|
||||
Prepared bool `json:"prepared"`
|
||||
}{op.OperationID, true})
|
||||
}
|
||||
vault, err := credentialvault.LoadFile(os.Getenv("ORBIT_CREDENTIAL_KEY_FILE"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -124,6 +155,24 @@ func runCredentialChange(args []string, action string) error {
|
||||
return json.NewEncoder(os.Stdout).Encode(receipt)
|
||||
}
|
||||
|
||||
func parseCredentialActions(value string) ([]string, error) {
|
||||
actions := strings.Split(value, ",")
|
||||
seen := make(map[string]bool, len(actions))
|
||||
for i, action := range actions {
|
||||
action = strings.TrimSpace(action)
|
||||
if action == "" || seen[action] {
|
||||
return nil, errors.New("--actions requires a nonempty, unique, complete action list")
|
||||
}
|
||||
seen[action] = true
|
||||
actions[i] = action
|
||||
}
|
||||
if err := (auth.Permissions{Actions: actions, Queues: []string{"*"}}).Validate(); err != nil {
|
||||
return nil, errors.New("--actions contains an unsupported action or exceeds the action limit")
|
||||
}
|
||||
sort.Strings(actions)
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
func runCredentialApply(args []string) error {
|
||||
flags := flag.NewFlagSet("credential apply", flag.ContinueOnError)
|
||||
file := ""
|
||||
|
||||
Reference in New Issue
Block a user