SuperAuth
Super auth is turn-key authorization gem that makes unauthorized access unrepresentable. Stop writing tests for authorization with confidence
The intent is to use with ruby applications, as well as centralize authorization for multiple applications. If you look at the OWASP top vulnerabilty, broken access control is the NUMBER 1 most common security risk in modern applications today. super_auth provides a authentication strategy that allows you to completely de-risk your application, solving this issue once confidently.
Installation
gem "super_auth"
Docs
How super_auth stacks up against other authentication strategies:
Do you really understand Authorization
Graph Visualization
SuperAuth includes an interactive graph visualization tool to help you understand and debug your authorization rules!
See the complete authorization graph with:
- Color-coded nodes (Users, Groups, Roles, Permissions, Resources)
- Interactive path finding
- Real-time authorization queries
- Example scenarios from the README
Quick Start:
# 1. Generate initializer
rails generate super_auth:install
# 2. Mount the engine in config/routes.rb
mount SuperAuth::Engine => '/super_auth'
# 3. Load sample data (optional)
rails runner "load File.join(SuperAuth::Engine.root, 'db/seeds/sample_data.rb')"Then visit: http://localhost:3000/super_auth/visualization
See VISUALIZATION.md for complete documentation.
Postgres Row-Level Security (optional)
The ByCurrentUser scope enforces authorization at the ORM layer. On Postgres you can
additionally enforce the same rules inside the database itself, so raw SQL, unscoped,
background jobs, and any other client on the same database are subject to them too —
unauthorized rows become invisible at the connection level. Enforcement is pure SQL:
participating apps don't load this gem, or Ruby, at all. The gem's role is
administrative — define the graph, compile authorizations, enable the policies — which
is what makes super_auth usable as a central authorization service for apps in any
language.
The contract (any language)
Identity is asserted per transaction by calling the super_auth_become function that
SuperAuth::RLS.enable installs:
BEGIN;
SELECT super_auth_become(user_external_id => '42', user_external_type => 'AppUser');
-- run normal queries; rows the user isn't authorized for don't exist --
COMMIT; -- identity dies with the transaction; there is nothing to clearFor a user managed inside super_auth, pass user_id => '7' instead; system => true
bypasses the policies (migrations, seeds, admin jobs).
The assertion is anchored to the calling transaction: super_auth_become sets
transaction-local identity settings plus a stamp of the current transaction id, and
every policy requires a stamp from the current transaction. Outside a transaction the
settings have already reverted, and identity smuggled in as session settings carries a
dead transaction's stamp — either way queries return no rows and writes are rejected.
Misuse fails closed, and the scheme works unchanged behind transaction-pooling proxies
like pgbouncer, because a transaction is exactly what they keep on one server
connection.
Setup (Rails)
1. Match column types to your primary keys — before your first migration.
The policies compare super_auth_authorizations.resource_external_id directly
against your tables' pks with no casting, so the columns must share a type:
# config/initializers/super_auth.rb
SuperAuth.setup do |config|
config.external_id_type = :bigint # Rails' default pk type; use :uuid, :string, ... to match yours
endIf super_auth is already migrated with the wrong type, alter the four external id
columns (super_auth_users.external_id, super_auth_resources.external_id,
super_auth_authorizations.user_external_id, super_auth_authorizations.resource_external_id)
in a migration of your own.
2. Enable RLS on the tables you want protected:
rails generate super_auth:rls Document Invoice
rails db:migrateThis creates one migration calling SuperAuth::RLS.enable(:documents, resource_type: "Document")
per model — you can also call that directly for tables outside Rails. resource_type
must match the resource_external_type used in your authorization rows (the model's
class name when you use the AR integration).
3. Connect as a role RLS applies to. Superusers and BYPASSRLS roles skip
policies entirely, so the app must not connect as one (owning the tables is fine —
the policies use FORCE ROW LEVEL SECURITY). The role needs SELECT on
super_auth_authorizations, which the policies read; EXECUTE on
super_auth_become is granted to PUBLIC by default, so no extra grant is needed:
CREATE ROLE app_runtime LOGIN PASSWORD '...';
GRANT SELECT, INSERT, UPDATE, DELETE ON documents, invoices TO app_runtime;
GRANT SELECT ON super_auth_authorizations TO app_runtime;4. Wrap work in an identity assertion. In Ruby:
SuperAuth.as(current_user) do
# every query in here is enforced by the database
endSuperAuth.as opens a transaction and calls super_auth_become for you — use it in
an around_action (or around a job) to cover a whole request. Non-Ruby apps use the
SQL contract directly. Each policy checks super_auth_authorizations with the same
semantics as ByCurrentUser: type-level authorizations (resource_external_id IS NULL)
act as a wildcard, per-record authorizations match on id, system? users bypass.
Notes
- Queries with no identity asserted see nothing, and writes are rejected — fail closed, by design. A client that has never heard of super_auth cannot accidentally reach protected rows.
- Creating rows requires a type-level authorization for that resource type (or system
context): the policy is
FOR ALLwith noWITH CHECK, so Postgres reuses itsUSINGexpression as the implicitWITH CHECKfor INSERTs and UPDATEs. - The transaction stamp calls
pg_current_xact_id(), which assigns a real transaction id even to read-only transactions — one extra xid per protected transaction. Negligible for almost everyone; revisit with a virtual-xid variant only if transaction id churn ever matters at extreme read volume. - One
external_id_typecovers the whole install, so every protected table across every participating app needs the same pk type. - Postgres 13+ only (
pg_current_xact_id). On other databasesSuperAuth::RLSraises, and the ORM scope remains the enforcement layer.
Configuration
# config/initializers/super_auth.rb
SuperAuth.setup do |config|
# Raise an error when a query runs without a current user set.
# Default is :none (returns empty results silently).
config.missing_user_behavior = :raise
end| Option | Values | Default | Description |
|---|---|---|---|
missing_user_behavior |
:none, :raise
|
:none |
Controls what happens when SuperAuth.current_user is blank. :none returns an empty result set. :raise raises SuperAuth::Error. |
external_id_type |
:string, :bigint, :uuid, ... |
:string |
Column type for the external id columns, applied when the migrations run. Set it to your application's primary key type so every comparison against your tables' pks is natively typed — no casting anywhere. |
Usage
SuperAuth is a rules engine engine that works on 5 different authorization concepts:
- Users
- Groups
- Roles
- Permissions
- Resources
The basis for how this works is that the rules engine is trying to match a user with a resource to determine access. The engine determines if it can find an authorization route betewen a user and a resource. It does so by looking at users, groups, roles, permissions.
+-------+ +------+
| Group |<----->| Role |
+-------+\ / +------+
^ \ / ^
| \/ |
| /\ |
| / \ |
V / \ V
+---------------+ +------+/ \+------------+ +----------+ +-------------------+
| YourApp::User |<-->| User |<------>| Permission |<-->| Resource | <--> | YourApp::Resource |
+---------------+ +------+ +------------+ +----------+ +-------------------+
^ ^
| |
+----------------------------------+
The lines between the boxes are called edges.
Note that Group and Role trees.
In general the super_auth has 5 different pathing strategies to search for access.
1. users <-> group[s] <-> role[s] <-> permission <-> resource
2. users <-> role[s] <-> permission <-> resource
3. users <-> group[s] <-> permission <-> resource
4. users <-> permission <-> resource
5. users <-> resource
Edges can be drawn between any 2 objects, allowing super_auth can seamlessly scale in complexity with you.
When Group and Role are used, the rules will apply to all descedants. If there are any edges
between the specified user and the resource, then access is granted.
You can see usage examples spec/example_spec.rb.
We're going to need some users:
Users:
- Peter
- Michael
- Bethany
- Eloise
- Anna
- Dillon
- Guest (Unknown User)
Let's see an example company structure:
Groups:
- Company
- Engineering_dept
- Backend
- Frontend
- Sales Department
- Marketing Department
- Customers
- CustomerA
- CustomerB
- Vendors
- VendorA
- VendorB
We're going to define a roles:
Roles:
- Employee
- Engineering
- Señor Software Developer
- Señor Designer
- Software Developer
- Production Support
- Sales and Marketing
- Marketing Manager
- Marketing Associate
- CustomerRole
We're going to define some permissions:
Permissions:
- create
- read
- update
- delete
- invoice
- login
- reboot
- deploy
- sign_contract
- subscribe
- unsubscribe
- publish_design
Finally, we need some resources:
Resources:
- app1
- app2
- staging
- db1
- db2
- core_design_template
- customer_profile
- marketing_website
- customer_post1
- customer_post2
- customer_post3
So we have sufficient prerequisite data to do some interesting authorizations. Let's draw some edges:
Peter <-> Frontend # Peter is on the Frontend team. (via Company->Engineering_dept->Frontend)
Engineering_dept <-> Engineering # Group "Engineering_dept" has the Role "Engineering"
Engineering <-> create # Engineering role can do basic CRUD operations
Engineering <-> read # Peter can CRUD too
Engineering <-> update
Engineering <-> delete
core_design_template <-> create # Now, those CRUD permissions apply to core_design_template resource
core_design_template <-> read
core_design_template <-> update
core_design_template <-> delete
With this, the following paths are created from Peter to the core_design_template:
Peter <-> Frontend <-> Engineering_dept <-> Engineering <-> create <-> core_design_template
Peter <-> Frontend <-> Engineering_dept <-> Engineering <-> read <-> core_design_template
Peter <-> Frontend <-> Engineering_dept <-> Engineering <-> update <-> core_design_template
Peter <-> Frontend <-> Engineering_dept <-> Engineering <-> delete <-> core_design_template
Which completes the circuit using the path
user <-> group <-> group <-> role <-> permission <-> resource
When you create/delete an edge new authorizations are generated and stored in the super_auth database table.
Since the path is stored with the record, it trivial to audit access permissions using basic SQL.
TODO: Write usage instructions here
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/JonathanFrias/super_auth.
License
The gem is available as open source under the terms of the GPL.