Skip to content

Instantly share code, notes, and snippets.

@bjeanes
Created December 1, 2023 02:58
Show Gist options
  • Save bjeanes/f06ba065880feccae4bd5c8e2b56225b to your computer and use it in GitHub Desktop.
Save bjeanes/f06ba065880feccae4bd5c8e2b56225b to your computer and use it in GitHub Desktop.
module Authentication
class API
# Some methods we use are on the "instance" which Rodauth doesn't (yet) consider part of its API. We'll hardcode the
# ones we want to let through here so we are conscious which parts of the private API we are exposing.
@@delegated_instance_methods = [
:otp_interval,
:otp_exists?,
:account_webauthn_usage,
:set_password,
:clear_other_sessions,
/_path$/,
/_url$/,
/_redirect$/, # redirect locations
/_subject$/, # email subjects
/\?$/ # any predicate is unlikely to cause harm
].freeze
def initialize(
config_or_record = :user,
actor: nil,
evidence: nil, # deprecated, use :audit_reason
env: {},
session: {},
**opts
)
env = (RequestContext.request&.env || {}).merge(env || {}).except(
Rack::REQUEST_METHOD,
Rack::PATH_INFO,
Rack::HTTP_HOST,
Rack::CONTENT_TYPE,
Rack::SCRIPT_NAME
)
actor ||= RequestContext.actor || :missing # don't allow an explicit nil actor
opts[:audit_actor] = actor if actor
opts[:audit_reason] ||= evidence if evidence
opts[:allowed_emails] ||= :none
opts.each do |k, v|
env.merge!("internal.#{k}" => v) if v.present?
end
@config_name, @account = resolve_config_name_and_account(config_or_record)
@session = session
@env = env
@auth_class = RodauthConfig.opts[:rodauths].fetch(@config_name)
end
def internal(**opts, &blk)
blk ||= proc { self }
internal_request_eval(**opts) do
account_from_session if session_value
instance_eval(&blk)
end
end
private
def resolve_config_name_and_account(config_or_record)
case config_or_record
when :admin, :user, "admin", "user"
[config_or_record.to_sym, nil]
when User, Admin # NOTE: object can be wrapped with Draper; this matches either way
config = config_or_record.model_name.param_key.to_sym # this approach for both decorated and naked objects
account = Account.find(config_or_record.id)
[config, account]
when Account
account = config_or_record
[(account.user ? :user : :admin), account]
else
raise ArgumentError, "expected config name or instance of User or Admin, got: #{config_or_record.inspect}}"
end
end
def method_missing(meth, *args, **opts, &blk)
if delegated_class_method?(meth)
opts[:account_id] = @account.id if @account
opts[:env] = @env.merge(opts[:env] || {})
opts[:session] = @session.merge(opts[:session] || {})
@auth_class.send(meth, *args, **opts, &blk)
elsif delegated_instance_method?(meth)
internal { send(meth, *args, **opts, &blk) }
else
super
end
end
def respond_to_missing?(meth, _include_private = false)
delegated_class_method?(meth) || delegated_instance_method?(meth) || super
end
def delegated_class_method?(meth)
# This _should_ only match internal request methods, but this has only been anecdotally demonstrated
@auth_class.methods(_ancestors = false).include?(meth)
end
def delegated_instance_method?(meth)
case meth.to_sym
when *@@delegated_instance_methods
true
else
false
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment