You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

rrx_api

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

rrx_api - rubygems Package Compare versions

Comparing version
8.0.2
to
8.0.3
+79
app/controllers/concerns/rrx_api/authenticatable.rb
# frozen_string_literal: true
module RrxApi
# Controller concern providing authentication plumbing.
#
# Include this in your ApplicationController (it is included in RrxApi::Controller
# by default). Then call +authenticate!+ on any controller that requires auth:
#
# class Api::V1::PostsController < ApplicationController
# authenticate!
# end
#
# Override +authenticated_user+ in your ApplicationController to resolve the
# current user from the request (token, API key, etc.). The default implementation
# returns +nil+, which means all requests are unauthenticated unless overridden.
#
# In test environments, requests may pass an +X-Test-User-Id+ header instead of
# a real token. Override +test_user+ if your User model has a different lookup.
module Authenticatable
extend ActiveSupport::Concern
AUTH_HEADER = 'Authorization'
BEARER_TOKEN = 'Bearer'
included do
rescue_from RrxApi::Auth::TokenExpiredError do
render json: { error: 'token_expired' }, status: :unauthorized
end
end
class_methods do
# Prepend a before_action that calls +authenticate_user!+.
# @param only [Array<Symbol>, nil]
# @param except [Array<Symbol>, nil]
def authenticate!(only: nil, except: nil)
prepend_before_action :authenticate_user!, only: only, except: except
end
end
# Returns the authenticated user or +nil+.
# In test env the +X-Test-User-Id+ header short-circuits real auth.
def current_user
@current_user ||= (Rails.env.test? ? test_user : nil) || authenticated_user
end
private
# Render 401 unless current_user is present.
def authenticate_user!
unauthorized! unless current_user
end
# Render a standardised 401 response.
# @param message [String]
def unauthorized!(message: 'Not authenticated')
render json: { error: message }, status: :unauthorized
end
# Resolve the current user from the request. Override this in your
# ApplicationController — e.g. look up by API key or bearer token.
# @return [Object, nil]
def authenticated_user
nil
end
# Yields the bearer token string if an +Authorization: Bearer <token>+ header is present.
def with_bearer_token
parts = request.headers[AUTH_HEADER]&.split(' ', 2)
yield parts[1] if parts&.first == BEARER_TOKEN
end
# Returns a user identified by the +X-Test-User-Id+ request header.
# Only active in the test environment. Override if your lookup differs.
# @return [Object, nil]
def test_user
nil
end
end
end
# frozen_string_literal: true
module ArelQuery
extend ActiveSupport::Concern
module Cast
def self.included(mod)
mod.class_eval do
alias_method :create_cast, :cast if method_defined?(:cast)
def cast(type)
::Arel::Nodes::NamedFunction.new "CAST", [self.as(type)]
end
end
end
end
class WithHelper
attr_reader :name
def initialize(name, relation)
raise ArgumentError, 'Name must be a Symbol or String' unless name.is_a?(Symbol) || name.is_a?(String)
raise ArgumentError, 'Relation must be an Arel node or select manager' unless relation.is_a?(Arel::Nodes::Node) || relation.is_a?(Arel::SelectManager)
@name = name
@relation = relation
end
def [](column)
table[column]
end
def table
@table ||= Arel::Table.new(@name)
end
def to_cte
Arel::Nodes::Cte.new(@name, @relation)
end
end
class ArelHelper
attr_reader :model
delegate :star, :sql, to: Arel
delegate :project, :where, :from, :join, :outer_join, :group, :order, :alias, to: :@table
delegate :with, to: :from
def initialize(model)
@model = model
@table = model.arel_table if model.respond_to?(:arel_table)
end
def table(name = nil)
if name
Arel::Table.new(name)
elsif @table
@table
else
raise ArgumentError, 'No table available. Provide a table name.'
end
end
alias t table
def literal(value)
Arel::Nodes::SqlLiteral.new(value.to_s)
end
alias l literal
alias lit literal
def string(value)
literal "'#{value}'"
end
alias s string
alias str string
# @return [Arel::SelectManager]
def select(...)
Arel::SelectManager.new(...)
end
def as(what, alias_name)
raise ArgumentError, 'Alias name must be a Symbol or String' unless alias_name.is_a?(Symbol) || alias_name.is_a?(String)
Arel::Nodes::As.new(what, literal(alias_name))
end
def for_with(name, relation)
WithHelper.new(name, relation)
end
def results(query)
connection.select_all(query)
end
def rows(query)
results(query).to_a
end
def connection
ActiveRecord::Base.connection
end
def respond_to_missing?(_method_name, _include_private = false)
true
end
FUNCTION_METHOD_PATTERN = /\A[a-z]+(_[a-z]+)*\z/
def method_missing(method_name, *args, &block)
node_class = "Arel::Nodes::#{method_name.to_s.camelize}".safe_constantize
if node_class
self.class.define_method method_name do |*method_args|
node_class.new(*method_args)
end
elsif @table&.respond_to?(method_name)
return @table.send(method_name, *args)
elsif method_name.to_s =~ FUNCTION_METHOD_PATTERN
self.class.define_method method_name do |*method_args|
Arel::Nodes::NamedFunction.new(method_name.to_s, method_args)
end
else
return super
end
send(method_name, *args)
end
def column(name)
@table[name]
end
alias col column
alias c column
def [](it)
case it
when Symbol
@table[it]
when String
string(it)
else
literal(it)
end
end
end
class_methods do
def aq(*args)
@arel_helper ||= ArelHelper.new(self)
args.empty? ? @arel_helper : @arel_helper.sql(*args)
end
end
included do
def aq(...)
self.class.aq(...)
end
end
end
ActiveSupport.on_load :active_record do
class ::Arel::SelectManager
def table
@ast.cores[0].source.left
end
def join_to(other, from: :id, to: :id, on: nil, type: :inner)
on ||= table[from].eq(other[to])
join_node_class = case type
when :inner then Arel::Nodes::InnerJoin
when :left, :outer then Arel::Nodes::OuterJoin
when :right then Arel::Nodes::RightOuterJoin
when :full then Arel::Nodes::FullOuterJoin
else raise ArgumentError, "Unknown join type: #{type.inspect}"
end
other = other.table if other.respond_to?(:table)
join(other, join_node_class).on(on)
end
end
::Arel::Nodes::Node.include(ArelQuery::Cast)
::Arel::Attributes::Attribute.include(ArelQuery::Cast)
end
# frozen_string_literal: true
module RrxApi
module Auth
class TokenExpiredError < StandardError
def initialize
super('User token expired')
end
end
# Abstract authentication provider base class.
# Subclass and override {#id_from_token} (and optionally {#user_info}) to implement a provider.
class Base
# Verify a bearer token and return the provider user ID.
# @param _token [String]
# @return [String]
def id_from_token(_token)
raise NotImplementedError, "#{self.class}#id_from_token is not implemented"
end
# Fetch user info for a given provider UID (optional).
# @param _uid [String]
# @return [Object, nil]
def user_info(_uid)
nil
end
end
end
end
# frozen_string_literal: true
require_relative 'base'
module RrxApi
module Auth
# Firebase authentication provider.
#
# Requires the +firebase-admin-sdk+ gem. It is intentionally not listed as a
# dependency of rrx_api — add it to your application's Gemfile when you want
# Firebase auth:
#
# gem 'firebase-admin-sdk'
#
# Configuration is read from +RrxConfig.firebase+ (project_id, client_email,
# private_key, etc.).
class Firebase < Base
def id_from_token(id_token)
require_firebase!
verified = auth.verify_id_token(id_token)
verified['sub']
rescue LoadError
raise
rescue => e
# Lazily match the expired-token error so this file can be parsed before
# the firebase-admin-sdk gem is loaded.
raise TokenExpiredError if e.class.name == 'Firebase::Admin::Auth::ExpiredTokenError'
raise
end
def user_info(uid)
require_firebase!
auth.get_user(uid)
end
private
def require_firebase!
require 'firebase-admin-sdk'
rescue LoadError
raise LoadError,
"firebase-admin-sdk is required for Firebase authentication. " \
"Add `gem 'firebase-admin-sdk'` to your Gemfile."
end
def credentials
@credentials ||= ::Firebase::Admin::Credentials.from_json(config.to_json)
end
def app
@app ||= begin
app_config = ::Firebase::Admin::Config.new(
project_id: config.project_id,
service_account_id: config.client_email
)
::Firebase::Admin::App.new(credentials:, config: app_config)
end
end
def auth
@auth ||= ::Firebase::Admin::Auth::Client.new(app)
end
def config
RrxConfig.firebase
end
end
end
end
+1
-0

@@ -6,2 +6,3 @@ # frozen_string_literal: true

include AbstractController::Helpers
include RrxApi::Authenticatable

@@ -8,0 +9,0 @@ abstract!

@@ -7,2 +7,4 @@ # frozen_string_literal: true

include ArelQuery
before_create :set_new_id

@@ -9,0 +11,0 @@

+1
-1

@@ -99,3 +99,3 @@ PATH

rails-html-sanitizer (~> 1.6)
active_record_query_trace (1.8.2)
active_record_query_trace (1.9)
activerecord (>= 6.0.0)

@@ -102,0 +102,0 @@ activejob (8.0.2)

@@ -10,2 +10,4 @@ require 'rails'

require 'actionpack/action_caching'
require_relative 'auth/base'
require_relative 'auth/firebase'

@@ -17,2 +19,23 @@ module RrxApi

config.cors_origins = []
# Checks whether +source+ matches any of the configured CORS origins.
# Each entry in +cors_origins+ may be:
# - a String for exact match (e.g. "https://app.example.com")
# - a String with a leading wildcard (e.g. "*.example.com" matches "https://foo.example.com")
# - a Regexp (e.g. /\.example\.com\z/)
def self.cors_origin_allowed?(source, origins)
origins.any? do |origin|
case origin
when Regexp then origin.match?(source)
when String
if origin.start_with?('*.')
# Wildcard subdomain: *.example.com matches any scheme+subdomain of example.com
suffix = origin[1..] # => ".example.com"
source.end_with?(suffix)
else
source == origin
end
end
end
end
config.healthcheck = nil

@@ -45,3 +68,3 @@ config.healthcheck_route = 'healthcheck'

else
app.config.cors_origins.include?(source)
Engine.cors_origin_allowed?(source, app.config.cors_origins)
end

@@ -48,0 +71,0 @@ end

# frozen_string_literal: true
module RrxApi
VERSION = '8.0.2'
VERSION = '8.0.3'
DEPENDENCY_VERSION = "~> #{VERSION}"
RAILS_VERSION = DEPENDENCY_VERSION
end