feat: add scoped access token model for read-only API access
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# owner_type :string
|
||||
# scope :string default("full"), not null
|
||||
# token :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
@@ -16,6 +17,13 @@
|
||||
#
|
||||
|
||||
class AccessToken < ApplicationRecord
|
||||
# Schema-wise this table can hold any number of tokens per owner, but we
|
||||
# intentionally cap user-facing tokens at two: one `full` and one `read_only`.
|
||||
# The User model's has_one associations + auto-create callbacks enforce the
|
||||
# 1+1 contract; this validation only constrains the allowed scope values.
|
||||
SCOPES = %w[full read_only].freeze
|
||||
|
||||
has_secure_token :token
|
||||
belongs_to :owner, polymorphic: true
|
||||
validates :scope, inclusion: { in: SCOPES }
|
||||
end
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
module AccessTokenable
|
||||
extend ActiveSupport::Concern
|
||||
# AccessToken is polymorphic and the table allows many rows per owner. By
|
||||
# convention we expose at most two per User (one `full`, one `read_only`);
|
||||
# AgentBot and PlatformApp keep their single `full` token. The scoped
|
||||
# has_one below is what makes `owner.access_token` deterministic.
|
||||
included do
|
||||
has_one :access_token, as: :owner, dependent: :destroy_async
|
||||
has_one :access_token, -> { where(scope: 'full') },
|
||||
as: :owner, class_name: 'AccessToken', inverse_of: :owner, dependent: :destroy_async
|
||||
after_create :create_access_token
|
||||
end
|
||||
|
||||
def create_access_token
|
||||
AccessToken.create!(owner: self)
|
||||
AccessToken.create!(owner: self, scope: 'full')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,6 +84,13 @@ class User < ApplicationRecord
|
||||
encrypts :otp_secret, deterministic: true
|
||||
encrypts :otp_backup_codes
|
||||
|
||||
# A user has exactly two AccessTokens by product contract: the `full` one
|
||||
# from AccessTokenable, plus the `read_only` one declared here. The DB does
|
||||
# not enforce this cap (polymorphic owner_id allows many rows); we rely on
|
||||
# these scoped has_one + after_create pairs to keep the 1+1 invariant.
|
||||
has_one :read_only_access_token, -> { where(scope: 'read_only') },
|
||||
as: :owner, class_name: 'AccessToken', inverse_of: :owner, dependent: :destroy_async
|
||||
|
||||
has_many :account_users, dependent: :destroy_async
|
||||
has_many :accounts, through: :account_users
|
||||
accepts_nested_attributes_for :account_users
|
||||
@@ -117,6 +124,7 @@ class User < ApplicationRecord
|
||||
# rubocop:enable Rails/HasManyOrHasOneDependent
|
||||
|
||||
before_validation :set_password_and_uid, on: :create
|
||||
after_create :create_read_only_access_token
|
||||
after_destroy :remove_macros
|
||||
|
||||
scope :order_by_full_name, -> { order('lower(name) ASC') }
|
||||
@@ -133,6 +141,10 @@ class User < ApplicationRecord
|
||||
self.uid = email
|
||||
end
|
||||
|
||||
def create_read_only_access_token
|
||||
AccessToken.create!(owner: self, scope: 'read_only')
|
||||
end
|
||||
|
||||
def assigned_inboxes
|
||||
administrator? ? Current.account.inboxes : inboxes.where(account_id: Current.account.id)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddScopeToAccessTokens < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :access_tokens, :scope, :string, default: 'full', null: false
|
||||
end
|
||||
end
|
||||
+2
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_05_15_000000) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_05_20_073722) do
|
||||
# These extensions should be enabled to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -24,6 +24,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_05_15_000000) do
|
||||
t.string "token"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "scope", default: "full", null: false
|
||||
t.index ["owner_type", "owner_id"], name: "index_access_tokens_on_owner_type_and_owner_id"
|
||||
t.index ["token"], name: "index_access_tokens_on_token", unique: true
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user