# frozen_string_literal: true module Masks # Concern to help with building access classes. # # Access classes must include this module, which will result in the class # behaving like an +ActiveModel+ instance, with attributes, validations, and # json serialization available by default. # # After including the module, classes must call +access+ to register the # class and its canonical name. # # @example # class ExampleAccess # include Masks::Access # # access 'example' # # def say_hi # puts 'hello world!' # end # end # # # Later, this class can be accessed with a valid session, # # provided there is a corresponding mask that allows it. # access = Masks.access('example', session) # access.say_hi if access # # @see Masks::Access::ClassMethods module Access extend ActiveSupport::Concern class << self # @visibility private def classes @classes ||= {} end # @visibility private def register(name, **defaults) classes[name.to_s] = defaults end # @visibility private def defaults(name) classes.fetch(name.to_s, nil) end end included do attr_accessor :session def actor raise Masks::Error::InvalidSession unless session session.actor end delegate :config, :roles, :role?, :role_records, :scopes, :scope?, :params, to: :session delegate :access_config, to: :class end # Class methods for classes that include +Masks::Access+. module ClassMethods # Overrides `new` to ensure classes are constructed with proper access. # @visibility private def new(*args, **opts) original = if args[0].is_a?(Masks::Session) args.delete_at(0) elsif opts[:session] opts.delete(:session) else raise Masks::Error::InvalidSession end model = original.config.model(:access) session = model.new(name: access_name, original:) session.mask! raise Masks::Error::Unauthorized unless session.passed? instance = super(*args, **opts) instance.session = session instance end # Returns the canonical access name for the class. # # @return [String] def access_name @access_name end # Registers the class by the supplied name. # # Any +opts+ provided will be used as defaults for instances, # in addition to the data supplied in the app's configuration. # # This can be called multiple times. If names are the same, +opts+ # will be deep merged together. Different names can be supplied as # well. # # @param [String] name # @param [Hash] opts # @return [nil] def access(name, **opts) @access_name = name Masks::Access.register(access_name, **opts.merge(cls: self)) nil end # Returns the configuration for the class. # # This is a combination of data provided to +access+ along with whatever # is specifed in the configuration object for the access class. # # @return [String] def access_config Masks.configuration.access(access_name) end # Builds a new access class, given a variety of arguments. # # @overload build(request) # @param [ActionDispatch::Request] request # @overload build(controller) # @param [ActionController::Metal] controller # @overload build(session) # @param [Masks::Session] session # @overload build(actor) # @param [Masks::Actor] actor # @overload build(**attrs) # @param [ActionController::Metal] controller # @return [Masks::Access] def build(arg1 = nil, **args) case arg1 when ActionDispatch::Request new(session: Masks::Sessions::Request.new(session: arg1)) when ActionController::Metal new(session: arg1.send(:masked_session)) when Masks::Session new(session: arg1, **args) when Masks::Actor new(session: Masks::Sessions::Actor.new(arg1, **opts)) when nil new(**opts) end end end end end