feat: support permanent credentials and overlapping token rotation
This commit is contained in:
@@ -117,16 +117,16 @@ func (s *ConsoleApplicationStore) GetApplication(ctx context.Context, token auth
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err = tx.QueryContext(ctx, `SELECT id,name,revision,expires_at,revoked_at,permissions FROM orbit_credentials WHERE namespace_id=? ORDER BY id LIMIT 101`, namespace)
|
||||
rows, err = tx.QueryContext(ctx, `SELECT id,name,revision,expires_at,revoked_at,previous_valid_until,permissions FROM orbit_credentials WHERE namespace_id=? ORDER BY id LIMIT 101`, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var c consoleauth.AccessCredential
|
||||
var expires, revoked sql.NullTime
|
||||
var expires, revoked, previous sql.NullTime
|
||||
var raw []byte
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Revision, &expires, &revoked, &raw); err != nil {
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Revision, &expires, &revoked, &previous, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(raw, &c.Permissions); err != nil {
|
||||
@@ -134,6 +134,7 @@ func (s *ConsoleApplicationStore) GetApplication(ctx context.Context, token auth
|
||||
}
|
||||
c.ExpiresAt = nullableTime(expires)
|
||||
c.RevokedAt = nullableTime(revoked)
|
||||
c.PreviousValidUntil = nullableTime(previous)
|
||||
out.Credentials = append(out.Credentials, c)
|
||||
}
|
||||
if len(out.Queues) > 100 {
|
||||
@@ -181,8 +182,14 @@ func applicationPermissions(in consoleauth.ApplicationInput) (auth.Permissions,
|
||||
return p, p.Validate()
|
||||
}
|
||||
func validateApplicationInput(in consoleauth.ApplicationInput) error {
|
||||
if in.ValidDays < 1 || in.ValidDays > 365 {
|
||||
return fault.Invalid("valid_days must be 1-365")
|
||||
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 err := validCredentialReason(in.Reason); err != nil {
|
||||
return err
|
||||
@@ -194,17 +201,17 @@ func validateApplicationInput(in consoleauth.ApplicationInput) error {
|
||||
}
|
||||
_, err := applicationPermissions(in)
|
||||
return err
|
||||
case "ROTATE":
|
||||
case "ROTATE", "SET_EXPIRY", "FINALIZE":
|
||||
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")
|
||||
}
|
||||
default:
|
||||
return fault.Invalid("kind must be CREATE or ROTATE")
|
||||
return fault.Invalid("kind must be CREATE, ROTATE, SET_EXPIRY or FINALIZE")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *ConsoleApplicationStore) PrepareApplication(ctx context.Context, token auth.Token, in consoleauth.ApplicationInput) (consoleauth.ApplicationOperation, error) {
|
||||
out := consoleauth.ApplicationOperation{Input: in}
|
||||
out := consoleauth.ApplicationOperation{Version: 2, Input: in}
|
||||
if err := validateApplicationInput(in); err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -216,7 +223,12 @@ func (s *ConsoleApplicationStore) PrepareApplication(ctx context.Context, token
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out.ExpiresAt = now.Add(time.Duration(in.ValidDays) * 24 * time.Hour)
|
||||
if in.ValidDays > 0 {
|
||||
out.ExpiresAt = timePtr(now.Add(time.Duration(in.ValidDays) * 24 * time.Hour))
|
||||
}
|
||||
if in.GraceDays > 0 {
|
||||
out.PreviousValidUntil = timePtr(now.Add(time.Duration(in.GraceDays) * 24 * time.Hour))
|
||||
}
|
||||
out.OperationID, err = core.NewID()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -243,7 +255,9 @@ func (s *ConsoleApplicationStore) PrepareApplication(ctx context.Context, token
|
||||
return fault.Conflict
|
||||
}
|
||||
}
|
||||
out.Token, _, err = auth.Issue(out.NamespaceID, out.CredentialID)
|
||||
if in.Kind == "CREATE" || in.Kind == "ROTATE" {
|
||||
out.Token, _, err = auth.Issue(out.NamespaceID, out.CredentialID)
|
||||
}
|
||||
return err
|
||||
})
|
||||
return out, err
|
||||
@@ -253,15 +267,21 @@ func (s *ConsoleApplicationStore) ApplyApplication(ctx context.Context, token au
|
||||
if err := validateApplicationInput(op.Input); err != nil {
|
||||
return out, err
|
||||
}
|
||||
parsed, err := auth.Parse(op.Token)
|
||||
if err != nil || parsed.NamespaceID != op.NamespaceID || parsed.CredentialID != op.CredentialID || op.OperationID.Validate() != nil || op.ApplicationID.Validate() != nil || op.ExpiresAt.IsZero() {
|
||||
return out, fault.Invalid("invalid fixed operation")
|
||||
if (op.Version != 0 && op.Version != 2) || 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.Kind == "ROTATE" {
|
||||
if (op.Input.ValidDays == 0) != (op.ExpiresAt == nil) || (op.Input.GraceDays == 0) != (op.PreviousValidUntil == nil) {
|
||||
return out, fault.Invalid("fixed lifetime does not match input")
|
||||
}
|
||||
version := op.Version
|
||||
if version == 0 {
|
||||
version = 1
|
||||
}
|
||||
if op.Input.Kind != "CREATE" {
|
||||
if op.QueueID != "" || op.NamespaceID != op.Input.NamespaceID || op.CredentialID != op.Input.CredentialID {
|
||||
return out, fault.Invalid("rotation identity mismatch")
|
||||
return out, fault.Invalid("credential identity mismatch")
|
||||
}
|
||||
change := CredentialOperation{Version: 1, OperationID: op.OperationID, NamespaceID: op.NamespaceID, CredentialID: op.CredentialID, ExpectedRevision: op.Input.ExpectedRevision, Action: "ROTATE", Reason: op.Input.Reason, Token: op.Token, ExpiresAt: &op.ExpiresAt}
|
||||
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}
|
||||
receipt, err := applyCredentialOperation(ctx, s.db, change, func(tx *sql.Tx) (string, error) {
|
||||
actor, err := s.admin(ctx, tx, token)
|
||||
if err != nil {
|
||||
@@ -282,6 +302,10 @@ func (s *ConsoleApplicationStore) ApplyApplication(ctx context.Context, token au
|
||||
})
|
||||
return consoleauth.ApplicationReceipt{OperationID: op.OperationID, ApplicationID: op.ApplicationID, NamespaceID: op.NamespaceID, CredentialID: op.CredentialID, Revision: receipt.Revision, Deduplicated: receipt.Deduplicated}, err
|
||||
}
|
||||
parsed, err := auth.Parse(op.Token)
|
||||
if err != nil || parsed.NamespaceID != op.NamespaceID || parsed.CredentialID != op.CredentialID || (version == 1 && op.ExpiresAt == nil) {
|
||||
return out, fault.Invalid("invalid new credential")
|
||||
}
|
||||
if op.QueueID.Validate() != nil {
|
||||
return out, fault.Invalid("invalid queue identity")
|
||||
}
|
||||
@@ -325,7 +349,7 @@ func (s *ConsoleApplicationStore) ApplyApplication(ctx context.Context, token au
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !op.ExpiresAt.After(now) || op.ExpiresAt.After(now.Add(365*24*time.Hour)) {
|
||||
if op.ExpiresAt != nil && (!op.ExpiresAt.After(now) || op.ExpiresAt.After(now.Add(365*24*time.Hour))) {
|
||||
return fault.Invalid("expired or excessive credential lifetime")
|
||||
}
|
||||
statements := []struct {
|
||||
@@ -336,7 +360,7 @@ func (s *ConsoleApplicationStore) ApplyApplication(ctx context.Context, token au
|
||||
{`INSERT INTO orbit_application_resources(application_id) VALUES (?)`, []any{op.ApplicationID}},
|
||||
{`INSERT INTO orbit_namespaces(id,application_id,name,environment) VALUES (?,?,?,?)`, []any{op.NamespaceID, op.ApplicationID, op.Input.Namespace, op.Input.Environment}},
|
||||
{`INSERT INTO pulse_queues(namespace_id,id,name,max_concurrency,rate_per_second,policy) VALUES (?,?,?,4,10,'{}')`, []any{op.NamespaceID, op.QueueID, op.Input.Queue}},
|
||||
{`INSERT INTO orbit_credentials(namespace_id,id,name,secret_digest,permissions,expires_at) VALUES (?,?,'console-issued',?,?,?)`, []any{op.NamespaceID, op.CredentialID, parsed.Digest, string(raw), op.ExpiresAt.UTC()}},
|
||||
{`INSERT INTO orbit_credentials(namespace_id,id,name,secret_digest,permissions,expires_at) VALUES (?,?,'console-issued',?,?,?)`, []any{op.NamespaceID, op.CredentialID, parsed.Digest, string(raw), op.ExpiresAt}},
|
||||
}
|
||||
for _, st := range statements {
|
||||
if _, err := tx.ExecContext(ctx, st.query, st.args...); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user