feat: support permanent credentials and overlapping token rotation

This commit is contained in:
2026-09-27 11:45:15 +08:00
parent 87aa710013
commit 385a618154
29 changed files with 718 additions and 161 deletions

View File

@@ -19,19 +19,23 @@ import (
func runCredential(args []string) error {
if len(args) == 0 {
return errors.New("usage: orbit-admin credential status|rotate|revoke|apply")
return errors.New("usage: orbit-admin credential status|rotate|expiry|finalize|revoke|apply")
}
switch args[0] {
case "status":
return runCredentialStatus(args[1:])
case "rotate":
return runCredentialChange(args[1:], "ROTATE")
case "expiry":
return runCredentialChange(args[1:], "SET_EXPIRY")
case "finalize":
return runCredentialChange(args[1:], "FINALIZE")
case "revoke":
return runCredentialChange(args[1:], "REVOKE")
case "apply":
return runCredentialApply(args[1:])
default:
return errors.New("usage: orbit-admin credential status|rotate|revoke|apply")
return errors.New("usage: orbit-admin credential status|rotate|expiry|finalize|revoke|apply")
}
}
@@ -64,14 +68,19 @@ func runCredentialStatus(args []string) error {
func runCredentialChange(args []string, action string) error {
flags := flag.NewFlagSet("credential "+strings.ToLower(action), flag.ContinueOnError)
namespace, credential, output, reason, validFor := "", "", "", "", "720h"
namespace, credential, output, reason, validFor := "", "", "", "", "0"
grace := time.Duration(0)
if action == "ROTATE" {
grace = 7 * 24 * time.Hour
}
var expected uint64
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")
flags.StringVar(&reason, "reason", "", "required audit reason")
flags.StringVar(&output, "output", "", "new private operation file; an existing file is never overwritten")
flags.StringVar(&validFor, "valid-for", "720h", "new credential validity, from 1h to 8760h")
flags.StringVar(&validFor, "valid-for", "0", "credential validity: 0 permanent, or 1h to 8760h")
flags.DurationVar(&grace, "grace", grace, "rotation overlap: 0 immediate, up to 720h; default 168h")
if err := flags.Parse(args); err != nil {
return err
}
@@ -83,7 +92,7 @@ func runCredentialChange(args []string, action string) error {
return errors.New("credential change requires valid ids and a nonzero --expected-revision")
}
duration := time.Duration(0)
if action == "ROTATE" {
if action == "ROTATE" || action == "SET_EXPIRY" {
duration, err = time.ParseDuration(validFor)
if err != nil {
return errors.New("invalid --valid-for duration")
@@ -95,7 +104,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, Reason: reason,
NamespaceID: ns, CredentialID: id, ExpectedRevision: expected, Action: action, ValidFor: duration, GraceFor: grace, Reason: reason,
})
if err != nil {
return errors.New("credential operation could not be prepared")