lib/attio/oauth/scope_validator.rb
# frozen_string_literal: true module Attio module OAuth # Validates and manages OAuth scopes for Attio API class ScopeValidator # Define all valid scopes with their descriptions SCOPE_DEFINITIONS = { # Record scopes "record:read" => "Read access to records", "record:write" => "Write access to records (includes read)", # Object scopes "object:read" => "Read access to objects and their configuration", "object:write" => "Write access to objects (includes read)", # List scopes "list:read" => "Read access to lists and list entries", "list:write" => "Write access to lists (includes read)", # Webhook scopes "webhook:read" => "Read access to webhooks", "webhook:write" => "Write access to webhooks (includes read)", # User scopes "user:read" => "Read access to workspace members", # Note scopes "note:read" => "Read access to notes", "note:write" => "Write access to notes (includes read)", # Attribute scopes "attribute:read" => "Read access to attributes", "attribute:write" => "Write access to attributes (includes read)", # Comment scopes "comment:read" => "Read access to comments", "comment:write" => "Write access to comments (includes read)", # Task scopes "task:read" => "Read access to tasks", "task:write" => "Write access to tasks (includes read)" }.freeze # Array of all valid scope strings VALID_SCOPES = SCOPE_DEFINITIONS.keys.freeze # Scope hierarchy - write scopes include read scopes SCOPE_HIERARCHY = { "record:write" => ["record:read"], "object:write" => ["object:read"], "list:write" => ["list:read"], "webhook:write" => ["webhook:read"], "note:write" => ["note:read"], "attribute:write" => ["attribute:read"], "comment:write" => ["comment:read"], "task:write" => ["task:read"] }.freeze class << self # Validate that all provided scopes are valid # @param scopes [Array<String>, String] Scopes to validate # @return [Array<String>] Validated scopes # @raise [InvalidScopeError] If any scope is invalid def validate(scopes) scopes = Array(scopes).map(&:to_s) invalid_scopes = scopes - VALID_SCOPES unless invalid_scopes.empty? raise InvalidScopeError, "Invalid scopes: #{invalid_scopes.join(", ")}" end scopes end # Validate scopes and return true if valid # @param scopes [Array<String>, String] Scopes to validate # @return [true] # @raise [InvalidScopeError] If any scope is invalid def validate!(scopes) validate(scopes) true end def valid?(scope) VALID_SCOPES.include?(scope.to_s) end # Get the description for a scope # @param scope [String] The scope to describe # @return [String, nil] Description or nil if scope not found def description(scope) SCOPE_DEFINITIONS[scope.to_s] end # Check if a set of scopes includes a specific permission def includes?(scopes, required_scope) scopes = Array(scopes).map(&:to_s) required = required_scope.to_s return true if scopes.include?(required) # Check if any scope in the set provides the required scope scopes.any? do |scope| implied_scopes = SCOPE_HIERARCHY[scope] || [] implied_scopes.include?(required) end end # Expand scopes to include all implied scopes def expand(scopes) scopes = Array(scopes).map(&:to_s) expanded = Set.new(scopes) scopes.each do |scope| implied = SCOPE_HIERARCHY[scope] || [] expanded.merge(implied) end expanded.to_a.sort end # Get minimal set of scopes (remove redundant read scopes) def minimize(scopes) scopes = Array(scopes).map(&:to_s) minimized = scopes.dup SCOPE_HIERARCHY.each do |write_scope, read_scopes| if minimized.include?(write_scope) minimized -= read_scopes end end minimized.sort end # Group scopes by resource type def group_by_resource(scopes) scopes = Array(scopes).map(&:to_s) grouped = {} scopes.each do |scope| resource = scope.split(":").first grouped[resource] ||= [] grouped[resource] << scope end grouped end # Check if scopes are sufficient for an operation def sufficient_for?(scopes, resource:, operation:) required_scope = "#{resource}:#{operation}" includes?(scopes, required_scope) end end # Raised when invalid scopes are provided class InvalidScopeError < StandardError; end end end end