diff --git a/hscontrol/db/credential.go b/hscontrol/db/credential.go index 95a317126..fdb9d34b8 100644 --- a/hscontrol/db/credential.go +++ b/hscontrol/db/credential.go @@ -39,19 +39,20 @@ func credentialToOAuthClient(c *types.Credential) *types.OAuthClient { // the User association is carried through when preloaded. func credentialToPreAuthKey(c *types.Credential) *types.PreAuthKey { return &types.PreAuthKey{ - ID: c.ID, - Prefix: c.Identifier, - Hash: c.Hash, - UserID: c.UserID, - User: c.User, - Description: c.Description, - Reusable: c.Reusable, - Ephemeral: c.Ephemeral, - Used: c.Used, - Tags: c.Tags, - CreatedAt: c.CreatedAt, - Expiration: c.Expiration, - Revoked: c.Revoked, + ID: c.ID, + Prefix: c.Identifier, + Hash: c.Hash, + UserID: c.UserID, + User: c.User, + Description: c.Description, + Reusable: c.Reusable, + Ephemeral: c.Ephemeral, + Used: c.Used, + Tags: c.Tags, + CreatedByToken: c.CreatedByToken, + CreatedAt: c.CreatedAt, + Expiration: c.Expiration, + Revoked: c.Revoked, } } diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 033a9deb4..bc2a241cb 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -331,6 +331,33 @@ WHERE auth_key_id IN ( }, Rollback: func(db *gorm.DB) error { return nil }, }, + { + // Mark the auth keys an OAuth access token makes. A database + // that got the credentials table from the DDL above already + // has the column; no released version made keys from tokens, + // so existing rows stay false. + ID: "202610221200-credential-created-by-token", + Migrate: func(tx *gorm.DB) error { + return tx.Transaction(func(tx *gorm.DB) error { + if tx.Migrator().HasColumn(&types.Credential{}, "created_by_token") { + return nil + } + + colType := "numeric" + if tx.Name() != sqlite.DriverName { + colType = "boolean" + } + + err := tx.Exec(`ALTER TABLE credentials ADD COLUMN created_by_token ` + colType + ` DEFAULT false`).Error + if err != nil { + return fmt.Errorf("adding created_by_token to credentials: %w", err) + } + + return nil + }) + }, + Rollback: func(db *gorm.DB) error { return nil }, + }, }, ) diff --git a/hscontrol/db/migrate_credentials.go b/hscontrol/db/migrate_credentials.go index 3290f1fbe..60c74525b 100644 --- a/hscontrol/db/migrate_credentials.go +++ b/hscontrol/db/migrate_credentials.go @@ -32,6 +32,7 @@ const credentialsDDLSQLite = `CREATE TABLE credentials( created_at datetime, expiration datetime, revoked datetime, + created_by_token numeric DEFAULT false, CONSTRAINT fk_credentials_user FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT chk_credentials_hash CHECK(hash IS NOT NULL OR revoked IS NOT NULL), @@ -59,6 +60,7 @@ const credentialsDDLPostgres = `CREATE TABLE credentials( created_at timestamptz, expiration timestamptz, revoked timestamptz, + created_by_token boolean DEFAULT false, CONSTRAINT fk_credentials_user FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT chk_credentials_hash CHECK(hash IS NOT NULL OR revoked IS NOT NULL), diff --git a/hscontrol/db/schema.sql b/hscontrol/db/schema.sql index fa96bf502..73620eca1 100644 --- a/hscontrol/db/schema.sql +++ b/hscontrol/db/schema.sql @@ -60,6 +60,7 @@ CREATE TABLE credentials( created_at datetime, expiration datetime, revoked datetime, + created_by_token numeric DEFAULT false, CONSTRAINT fk_credentials_user FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT chk_credentials_hash CHECK(hash IS NOT NULL OR revoked IS NOT NULL), diff --git a/hscontrol/types/credential.go b/hscontrol/types/credential.go index beee8eed5..8d8b0a61d 100644 --- a/hscontrol/types/credential.go +++ b/hscontrol/types/credential.go @@ -52,6 +52,10 @@ type Credential struct { // the Identifier of its issuing OAuth client. ClientID string + // CreatedByToken marks a pre-auth key an OAuth access token made: a token + // lists those without all=true. + CreatedByToken bool `gorm:"default:false"` + CreatedAt *time.Time Expiration *time.Time Revoked *time.Time @@ -72,19 +76,20 @@ func (pak *PreAuthKey) AsCredential() *Credential { } return &Credential{ - ID: pak.ID, - Kind: CredentialPreAuthKey, - Identifier: pak.Prefix, - Hash: pak.Hash, - UserID: pak.UserID, - User: pak.User, - Description: pak.Description, - Reusable: pak.Reusable, - Ephemeral: pak.Ephemeral, - Used: pak.Used, - Tags: pak.Tags, - CreatedAt: pak.CreatedAt, - Expiration: pak.Expiration, - Revoked: pak.Revoked, + ID: pak.ID, + Kind: CredentialPreAuthKey, + Identifier: pak.Prefix, + Hash: pak.Hash, + UserID: pak.UserID, + User: pak.User, + Description: pak.Description, + Reusable: pak.Reusable, + Ephemeral: pak.Ephemeral, + Used: pak.Used, + Tags: pak.Tags, + CreatedByToken: pak.CreatedByToken, + CreatedAt: pak.CreatedAt, + Expiration: pak.Expiration, + Revoked: pak.Revoked, } } diff --git a/hscontrol/types/preauth_key.go b/hscontrol/types/preauth_key.go index 12ada70e3..5048bbed9 100644 --- a/hscontrol/types/preauth_key.go +++ b/hscontrol/types/preauth_key.go @@ -65,6 +65,9 @@ type PreAuthKey struct { // If non-empty, this creates tagged nodes (not user-owned). Tags []string `gorm:"serializer:json"` + // CreatedByToken is set when an OAuth access token made the key. + CreatedByToken bool + CreatedAt *time.Time Expiration *time.Time diff --git a/hscontrol/types/types_clone.go b/hscontrol/types/types_clone.go index 2177c7444..d72db8a7e 100644 --- a/hscontrol/types/types_clone.go +++ b/hscontrol/types/types_clone.go @@ -144,19 +144,20 @@ func (src *PreAuthKey) Clone() *PreAuthKey { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _PreAuthKeyCloneNeedsRegeneration = PreAuthKey(struct { - ID uint64 - Prefix string - Hash []byte - UserID *uint - User *User - Description string - Reusable bool - Ephemeral bool - Used bool - Tags []string - CreatedAt *time.Time - Expiration *time.Time - Revoked *time.Time + ID uint64 + Prefix string + Hash []byte + UserID *uint + User *User + Description string + Reusable bool + Ephemeral bool + Used bool + Tags []string + CreatedByToken bool + CreatedAt *time.Time + Expiration *time.Time + Revoked *time.Time }{}) // Clone makes a deep copy of Credential. @@ -193,21 +194,22 @@ func (src *Credential) Clone() *Credential { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _CredentialCloneNeedsRegeneration = Credential(struct { - ID uint64 - Kind CredentialKind - Identifier string - Hash []byte - UserID *uint - User *User - Description string - Scopes []string - Tags []string - Reusable bool - Ephemeral bool - Used bool - LastSeen *time.Time - ClientID string - CreatedAt *time.Time - Expiration *time.Time - Revoked *time.Time + ID uint64 + Kind CredentialKind + Identifier string + Hash []byte + UserID *uint + User *User + Description string + Scopes []string + Tags []string + Reusable bool + Ephemeral bool + Used bool + LastSeen *time.Time + ClientID string + CreatedByToken bool + CreatedAt *time.Time + Expiration *time.Time + Revoked *time.Time }{}) diff --git a/hscontrol/types/types_view.go b/hscontrol/types/types_view.go index 8b78b6c95..ad77bc3e2 100644 --- a/hscontrol/types/types_view.go +++ b/hscontrol/types/types_view.go @@ -425,6 +425,9 @@ func (v PreAuthKeyView) Used() bool { return v.ж.Used } // Tags are copied to the node during registration. // If non-empty, this creates tagged nodes (not user-owned). func (v PreAuthKeyView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) } + +// CreatedByToken is set when an OAuth access token made the key. +func (v PreAuthKeyView) CreatedByToken() bool { return v.ж.CreatedByToken } func (v PreAuthKeyView) CreatedAt() views.ValuePointer[time.Time] { return views.ValuePointerOf(v.ж.CreatedAt) } @@ -442,19 +445,20 @@ func (v PreAuthKeyView) Revoked() views.ValuePointer[time.Time] { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _PreAuthKeyViewNeedsRegeneration = PreAuthKey(struct { - ID uint64 - Prefix string - Hash []byte - UserID *uint - User *User - Description string - Reusable bool - Ephemeral bool - Used bool - Tags []string - CreatedAt *time.Time - Expiration *time.Time - Revoked *time.Time + ID uint64 + Prefix string + Hash []byte + UserID *uint + User *User + Description string + Reusable bool + Ephemeral bool + Used bool + Tags []string + CreatedByToken bool + CreatedAt *time.Time + Expiration *time.Time + Revoked *time.Time }{}) // View returns a read-only view of Credential. @@ -547,6 +551,10 @@ func (v CredentialView) LastSeen() views.ValuePointer[time.Time] { // ClientID links an OAuth access token (Kind == CredentialOAuthToken) back to // the Identifier of its issuing OAuth client. func (v CredentialView) ClientID() string { return v.ж.ClientID } + +// CreatedByToken marks a pre-auth key an OAuth access token made: a token +// lists those without all=true. +func (v CredentialView) CreatedByToken() bool { return v.ж.CreatedByToken } func (v CredentialView) CreatedAt() views.ValuePointer[time.Time] { return views.ValuePointerOf(v.ж.CreatedAt) } @@ -561,21 +569,22 @@ func (v CredentialView) Revoked() views.ValuePointer[time.Time] { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _CredentialViewNeedsRegeneration = Credential(struct { - ID uint64 - Kind CredentialKind - Identifier string - Hash []byte - UserID *uint - User *User - Description string - Scopes []string - Tags []string - Reusable bool - Ephemeral bool - Used bool - LastSeen *time.Time - ClientID string - CreatedAt *time.Time - Expiration *time.Time - Revoked *time.Time + ID uint64 + Kind CredentialKind + Identifier string + Hash []byte + UserID *uint + User *User + Description string + Scopes []string + Tags []string + Reusable bool + Ephemeral bool + Used bool + LastSeen *time.Time + ClientID string + CreatedByToken bool + CreatedAt *time.Time + Expiration *time.Time + Revoked *time.Time }{})