Module: ApiKeys::Models::Concerns::HasApiKeys
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/api_keys/models/concerns/has_api_keys.rb
Overview
Concern to add API key capabilities to an owner model (e.g., User, Organization).
This module provides the has_api_keys class method when extended onto ActiveRecord::Base.
Defined Under Namespace
Modules: ClassMethods Classes: DslProvider
Constant Summary collapse
- SUPPORTED_SETTINGS =
%i[max_keys require_name default_scopes].freeze
- MAX_DEFAULT_SCOPES =
100- MAX_SCOPE_BYTESIZE =
128
Class Method Summary collapse
Instance Method Summary collapse
-
#available_api_key_scopes ⇒ Array<String>
Returns the available scopes for API keys on this owner.
-
#can_create_api_key?(key_type: nil, environment: nil) ⇒ Boolean
Checks if this owner can create an API key of the given type.
-
#create_api_key!(name: nil, scopes: nil, expires_at: nil, expires_at_preset: nil, metadata: nil, key_type: nil, environment: nil) ⇒ ApiKeys::ApiKey
Creates a new API key for this owner instance and returns the ApiKey instance.
Class Method Details
.validate_and_freeze_settings(settings) ⇒ Object
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/api_keys/models/concerns/has_api_keys.rb', line 18 def validate_and_freeze_settings(settings) max_keys = settings[:max_keys] unless max_keys.nil? || (max_keys.is_a?(Integer) && max_keys >= 0) raise ArgumentError, "max_keys must be a non-negative Integer or nil" end require_name = settings[:require_name] unless require_name == true || require_name == false raise ArgumentError, "require_name must be true or false" end scopes = settings[:default_scopes] unless scopes.is_a?(Array) && scopes.length <= MAX_DEFAULT_SCOPES raise ArgumentError, "default_scopes must be a bounded Array of safe scope strings" end normalized_scopes = scopes.map { |scope| scope.is_a?(Symbol) ? scope.to_s : scope } unless normalized_scopes.all? { |scope| valid_scope_name?(scope) } raise ArgumentError, "default_scopes must be a bounded Array of safe scope strings" end { max_keys: max_keys, require_name: require_name, default_scopes: normalized_scopes.uniq.map { |scope| scope.dup.freeze }.freeze }.freeze end |
Instance Method Details
#available_api_key_scopes ⇒ Array<String>
Returns the available scopes for API keys on this owner. Uses owner-specific settings if defined, otherwise falls back to global config. Useful for populating scope checkboxes in forms.
158 159 160 161 |
# File 'lib/api_keys/models/concerns/has_api_keys.rb', line 158 def available_api_key_scopes owner_settings = self.class.api_keys_settings owner_settings&.[](:default_scopes) || ApiKeys.configuration.default_scopes || [] end |
#can_create_api_key?(key_type: nil, environment: nil) ⇒ Boolean
Checks if this owner can create an API key of the given type. Returns false if the limit for this key type/environment is reached. Useful for conditional UI (e.g., hiding "Create" button when at limit).
Note: This is a best-effort check for UI purposes without locking. Concurrent requests could see stale data. The actual limit is enforced with pessimistic locking in create_api_key!, so this is safe to use for UI decisions (worst case: button shown but creation fails validation).
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/api_keys/models/concerns/has_api_keys.rb', line 181 def can_create_api_key?(key_type: nil, environment: nil) config = ApiKeys.configuration # If no key_type specified, check global quota only return within_global_quota? if key_type.nil? # Resolve environment resolved_environment = resolve_environment(environment, key_type, config) # Get type-specific limit type_config = config.key_types&.find { |type, _settings| type.to_s == key_type.to_s }&.last return within_global_quota? unless type_config limit = type_config[:limit] return within_global_quota? unless limit # Count existing keys of this type/environment existing_count = api_keys .active .where(key_type: key_type.to_s) .where(environment: resolved_environment.to_s) .count existing_count < limit && within_global_quota? end |
#create_api_key!(name: nil, scopes: nil, expires_at: nil, expires_at_preset: nil, metadata: nil, key_type: nil, environment: nil) ⇒ ApiKeys::ApiKey
Creates a new API key for this owner instance and returns the ApiKey instance. Raises ActiveRecord::RecordInvalid if creation fails.
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
# File 'lib/api_keys/models/concerns/has_api_keys.rb', line 225 def create_api_key!(name: nil, scopes: nil, expires_at: nil, expires_at_preset: nil, metadata: nil, key_type: nil, environment: nil) config = ApiKeys.configuration # Parse expires_at_preset if provided (takes precedence over expires_at) if expires_at_preset.present? expires_at = ApiKeys::Helpers::ExpirationOptions.parse(expires_at_preset) end # Auto-clean scopes: remove blank values from arrays (common when using form checkboxes) if scopes.is_a?(Array) scopes = scopes.reject { |s| s.blank? } end # Check for missing columns if key_types feature is enabled if key_types_feature_enabled?(config) check_required_columns! end # Use default_key_type if not specified and key_types feature is enabled resolved_key_type = key_type if resolved_key_type.nil? && key_types_feature_enabled?(config) && config.default_key_type.present? resolved_key_type = config.default_key_type end # Validate key_type if provided and key_types feature is enabled if resolved_key_type.present? validate_key_type!(resolved_key_type, config) elsif key_types_feature_enabled?(config) raise ArgumentError, "key_type is required when key types are configured" end # Determine environment: use provided, or default from config resolved_environment = resolve_environment(environment, resolved_key_type, config) # Validate environment if key_types feature is enabled if resolved_environment.present? && key_types_feature_enabled?(config) validate_environment!(resolved_environment, config) end # Fetch default scopes from this owner class's settings, falling back to global config. owner_settings = self.class.api_keys_settings default_scopes = owner_settings&.[](:default_scopes) || config.default_scopes || [] # Use provided scopes if given, otherwise use the calculated defaults. key_scopes = scopes.nil? ? default_scopes : Array(scopes) # Filter scopes based on key type permissions ceiling if resolved_key_type.present? key_scopes = (key_scopes, resolved_key_type, config) end raise ArgumentError, "API key owner must be persisted before creating a key" unless persisted? # ApiKey's creation callback locks the owner row before quota validation. # Keep an explicit transaction here so the helper's creation workflow is # a single atomic unit; direct ApiKey.create! calls are protected too. api_key = self.class.transaction do self.api_keys.create!( name: name, scopes: key_scopes, expires_at: expires_at, metadata: || {}, # Ensure metadata is at least an empty hash key_type: resolved_key_type&.to_s, environment: resolved_environment&.to_s # prefix, token_digest, digest_algorithm are set by ApiKey callbacks ) end # Return the ApiKey instance itself. # The plaintext token is available via `api_key.token` immediately after this. api_key end |