access_grant 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.codegraph/.gitignore +5 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +98 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +33 -0
  7. data/CONTRIBUTING.md +99 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +123 -0
  11. data/Rakefile +12 -0
  12. data/docs/architecture.md +1157 -0
  13. data/docs/proposal.md +143 -0
  14. data/docs/superpowers/plans/2026-09-08-access-grant-v1.md +468 -0
  15. data/docs/superpowers/plans/2026-09-08-gem-release.md +367 -0
  16. data/docs/superpowers/specs/2026-09-05-owner-role-design.md +271 -0
  17. data/docs/superpowers/specs/2026-09-07-proposal-review.md +71 -0
  18. data/docs/superpowers/specs/2026-09-07-usage-scenarios.md +301 -0
  19. data/docs/superpowers/specs/2026-09-08-gem-release-design.md +82 -0
  20. data/lib/access_grant/catalog/dsl.rb +138 -0
  21. data/lib/access_grant/catalog.rb +76 -0
  22. data/lib/access_grant/configuration.rb +55 -0
  23. data/lib/access_grant/controller_methods.rb +104 -0
  24. data/lib/access_grant/models/permission.rb +36 -0
  25. data/lib/access_grant/models/role.rb +152 -0
  26. data/lib/access_grant/models/role_permission.rb +11 -0
  27. data/lib/access_grant/owner.rb +144 -0
  28. data/lib/access_grant/permission_key.rb +29 -0
  29. data/lib/access_grant/railtie.rb +17 -0
  30. data/lib/access_grant/recovery.rb +90 -0
  31. data/lib/access_grant/sync.rb +68 -0
  32. data/lib/access_grant/tenant.rb +47 -0
  33. data/lib/access_grant/user.rb +102 -0
  34. data/lib/access_grant/version.rb +5 -0
  35. data/lib/access_grant.rb +125 -0
  36. data/lib/generators/access_grant/install/install_generator.rb +22 -0
  37. data/lib/generators/access_grant/install/templates/create_access_grant_tables.rb.tt +39 -0
  38. data/lib/generators/access_grant/setup/setup_generator.rb +188 -0
  39. data/lib/generators/access_grant/setup/templates/access_grant.rb.tt +80 -0
  40. data/lib/generators/access_grant/setup/templates/create_access_grant_user_roles.rb.tt +14 -0
  41. data/lib/generators/access_grant/setup/templates/permissions.rb.tt +10 -0
  42. data/lib/generators/access_grant/setup/templates/roles.rb.tt +27 -0
  43. data/lib/tasks/access_grant_tasks.rake +27 -0
  44. metadata +121 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 286d6345a4ae68c6b87490f90d069e25cd7ea54c74543814ddee2d408083e451
4
+ data.tar.gz: f00a934a2747d2a5182abca43d85cda476854e4bd68086b86076ad60ad9a0f68
5
+ SHA512:
6
+ metadata.gz: 28ebe5a51b2ac76094c5fd207d9f720f8763f3bed803752b9a09ed9da0fa4527375c52c27eed75ad63fd877ca03f0d11e0e003b9d772118e3c3628cb009e58e0
7
+ data.tar.gz: 5e5c81eb84665e3b9da6dde01ebbcb83f9ed6ca24a78b4ec55410f22330c54feda960245d9ada5c6cdaea195c1d9d91ebdd969db7f354824380f0aecbb1a6910
@@ -0,0 +1,5 @@
1
+ # CodeGraph data files — local to each machine, not for committing.
2
+ # Ignore everything in .codegraph/ except this file itself, so transient
3
+ # files (the database, daemon.pid, sockets, logs) never show up in git.
4
+ *
5
+ !.gitignore
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,98 @@
1
+ plugins:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.1
6
+ NewCops: enable
7
+ SuggestExtensions: false
8
+ Exclude:
9
+ - "bin/**/*"
10
+ - "pkg/**/*"
11
+ - "tmp/**/*"
12
+ - "vendor/**/*"
13
+
14
+ Style/StringLiterals:
15
+ EnforcedStyle: double_quotes
16
+
17
+ Style/Documentation:
18
+ Enabled: false
19
+
20
+ Metrics/BlockLength:
21
+ Exclude:
22
+ - "spec/**/*"
23
+ - "*.gemspec"
24
+
25
+ Metrics/MethodLength:
26
+ Max: 20
27
+ CountAsOne: ["array", "hash", "heredoc"]
28
+ Exclude:
29
+ - "lib/generators/**/*"
30
+
31
+ Metrics/AbcSize:
32
+ Max: 25
33
+ Exclude:
34
+ - "lib/generators/**/*"
35
+
36
+ Metrics/ClassLength:
37
+ Max: 200
38
+ Exclude:
39
+ - "lib/generators/**/*"
40
+
41
+ Metrics/CyclomaticComplexity:
42
+ Max: 10
43
+
44
+ Metrics/PerceivedComplexity:
45
+ Max: 10
46
+
47
+ Layout/LineLength:
48
+ Max: 120
49
+
50
+ Gemspec/DevelopmentDependencies:
51
+ EnforcedStyle: Gemfile
52
+
53
+ # Integration / generator specs intentionally use string describes and richer examples.
54
+ RSpec/ExampleLength:
55
+ Max: 50
56
+
57
+ RSpec/MultipleExpectations:
58
+ Max: 25
59
+
60
+ RSpec/DescribeClass:
61
+ Exclude:
62
+ - "spec/integration/**/*"
63
+ - "spec/generators/**/*"
64
+ - "spec/support/**/*"
65
+ - "spec/access_grant/user_spec.rb"
66
+ - "spec/access_grant/controller_methods_spec.rb"
67
+ - "spec/access_grant/railtie_spec.rb"
68
+ - "spec/access_grant/apply_configuration_spec.rb"
69
+ - "spec/access_grant/owner_spec.rb"
70
+
71
+ RSpec/SpecFilePathFormat:
72
+ Exclude:
73
+ - "spec/access_grant/models/**/*"
74
+
75
+ RSpec/InstanceVariable:
76
+ Exclude:
77
+ - "spec/generators/**/*"
78
+ - "spec/access_grant/controller_methods_spec.rb"
79
+ - "spec/access_grant/owner_spec.rb"
80
+
81
+ RSpec/ContextWording:
82
+ Prefixes:
83
+ - when
84
+ - with
85
+ - without
86
+ - and
87
+ - for
88
+ - if
89
+
90
+ RSpec/VerifiedDoubles:
91
+ Exclude:
92
+ - "spec/access_grant/controller_methods_spec.rb"
93
+
94
+ RSpec/MessageSpies:
95
+ EnforcedStyle: receive
96
+
97
+ RSpec/StubbedMock:
98
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.0.0] - 2026-09-08
10
+
11
+ ### Added
12
+
13
+ - **Configuration** — table names, tenant/user classes, owner role mode, controller
14
+ hooks (`current_user_method`, `current_tenant_method`), and recovery callback.
15
+ - **Permission catalog DSL** — code-defined `AccessGrant.permissions` catalog with
16
+ `resource` / `action` blocks; synced to DB via `rake access_grant:sync_permissions`.
17
+ - **Sync** — idempotent upsert of catalog permissions into the database (never deletes).
18
+ - **`permitted?(key, tenant:)`** — runtime authorization check on user models.
19
+ - **Owner role** — protected/bypass/both modes, last-owner guard, `grant_owner!` /
20
+ `revoke_owner!`.
21
+ - **Recovery rake** — `access_grant:grant_role` for ops lockout recovery.
22
+ - **Controller authorization** — `access_grant_authorize!` before-action hook
23
+ (raises `AccessGrant::NotAuthorizedError`).
24
+ - **Generators** — `access_grant:install` (core tables) and `access_grant:setup`
25
+ (initializer, catalog, user_roles migration, model patches).
26
+ - **Railtie** — loads rake tasks and `ActiveSupport.on_load` hooks for
27
+ `ActionController` and `ActiveRecord`.
28
+ - Project scaffolding: gemspec, RSpec/RuboCop harness, CI, and design docs
29
+ (docs/proposal.md, docs/architecture.md).
30
+
31
+ ## [0.1.0] - 2026-09-05
32
+
33
+ - Initial gem skeleton (pre-release, unpublished).
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,99 @@
1
+ # Contributing
2
+
3
+ ## Getting started
4
+
5
+ ```
6
+ bin/setup
7
+ bundle exec rspec
8
+ bundle exec rubocop
9
+ ```
10
+
11
+ `bin/console` starts an IRB session with the gem loaded, for quick manual
12
+ experiments.
13
+
14
+ ## Branching
15
+
16
+ `main` is always releasable. Work happens on short-lived branches named by
17
+ type:
18
+
19
+ - `feature/<slug>` — new functionality
20
+ - `fix/<slug>` — bug fixes
21
+ - `docs/<slug>` — documentation-only changes
22
+ - `chore/<slug>` — tooling, dependencies, CI, maintenance
23
+
24
+ ## Commit messages
25
+
26
+ This project follows [Conventional Commits](https://www.conventionalcommits.org/):
27
+
28
+ ```
29
+ <type>[optional scope]: <description>
30
+
31
+ [optional body]
32
+
33
+ [optional footer(s)]
34
+ ```
35
+
36
+ Common types: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `ci`.
37
+
38
+ Examples:
39
+
40
+ ```
41
+ feat: add permitted? memoization per instance
42
+ fix: prevent duplicate role names within a tenant
43
+ docs: document the migration sequencing guardrail
44
+ ```
45
+
46
+ A breaking change is marked either with `!` after the type/scope
47
+ (`feat!: rename acts_as_permissible to acts_as_grantable`) or a
48
+ `BREAKING CHANGE:` footer explaining the impact.
49
+
50
+ ## Pull requests
51
+
52
+ Every PR should:
53
+
54
+ - Pass `bundle exec rspec` and `bundle exec rubocop` (enforced by CI).
55
+ - Update `CHANGELOG.md` under `[Unreleased]` for any user-facing change.
56
+ - Update `README.md` / `docs/` if behavior or the public API changed.
57
+
58
+ The PR template checklist covers this — fill it in rather than deleting it.
59
+
60
+ ## Versioning & releases
61
+
62
+ This project follows [Semantic Versioning](https://semver.org/).
63
+
64
+ ### Cut a release
65
+
66
+ 1. Open a PR on `main` that:
67
+ - Bumps `AccessGrant::VERSION` in `lib/access_grant/version.rb`
68
+ - Moves `[Unreleased]` entries in `CHANGELOG.md` under
69
+ `## [X.Y.Z] - YYYY-MM-DD` and leaves a fresh empty `[Unreleased]`
70
+ - Uses commit message `chore: release vX.Y.Z`
71
+ 2. Merge the PR and wait for CI on `main` to pass.
72
+ 3. In GitHub Actions, run the **Release** workflow on `main` with input
73
+ `version` set to `X.Y.Z` (must match `AccessGrant::VERSION`).
74
+ 4. The workflow:
75
+ - Fails fast if the version or CHANGELOG heading does not match
76
+ - Publishes the gem to RubyGems.org via Trusted Publishing (OIDC)
77
+ - Creates git tag `vX.Y.Z` and a GitHub Release from the CHANGELOG section
78
+
79
+ Do **not** run `bundle exec rake release` locally for production publishes;
80
+ Actions owns tagging and `gem push`.
81
+
82
+ ### One-time RubyGems setup
83
+
84
+ 1. Create a RubyGems.org account and enable MFA.
85
+ 2. Configure a [Trusted Publisher](https://guides.rubygems.org/trusted-publishing/)
86
+ for gem `access_grant`:
87
+ - Repository owner: `SahSantoshh`
88
+ - Repository name: `access_grant`
89
+ - Workflow filename: `release.yml`
90
+ - Environment: leave blank (must match the workflow — no `environment:` key)
91
+ 3. For the first publish, use RubyGems’ pending trusted-publisher flow if the
92
+ gem name is not on the index yet.
93
+
94
+ ## Code style
95
+
96
+ RuboCop must pass with no offenses (`bundle exec rubocop`). Match the
97
+ conventions already established in the codebase rather than introducing new
98
+ ones ad hoc; propose `.rubocop.yml` changes separately if a rule genuinely
99
+ doesn't fit.
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "rake", "~> 13.0"
8
+ gem "rspec", "~> 3.13"
9
+ gem "rubocop", "~> 1.75"
10
+ gem "rubocop-rspec", "~> 3.0"
11
+ gem "sqlite3", ">= 1.4"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Santosh Sah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # AccessGrant
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/access_grant.svg)](https://rubygems.org/gems/access_grant)
4
+ [![CI](https://github.com/SahSantoshh/access_grant/actions/workflows/ci.yml/badge.svg)](https://github.com/SahSantoshh/access_grant/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.txt)
6
+
7
+ Dynamic, database-backed, per-tenant role and permission management for
8
+ Rails — the "roles and permissions live in the database, admins edit them at
9
+ runtime" pattern, without a canonical Rails equivalent until now.
10
+
11
+ > **Status:** v1.0 published on [RubyGems](https://rubygems.org/gems/access_grant).
12
+ > Design: [`docs/architecture.md`](docs/architecture.md),
13
+ > [`docs/proposal.md`](docs/proposal.md).
14
+
15
+ ## The problem
16
+
17
+ Every mainstream Rails authorization gem hardcodes "who can do what" in Ruby
18
+ code — a policy class, an `Ability` class. Changing a permission means a
19
+ developer opening a pull request and deploying. AccessGrant instead keeps
20
+ the permission *catalog* in code (so a new capability always requires a code
21
+ change to exist) but keeps the *role → permission mapping* in the database,
22
+ fully editable by tenant admins at runtime.
23
+
24
+ ## How it compares
25
+
26
+ | Gem | Roles as data | Permission catalog | Runtime-editable | Per-tenant scoping |
27
+ |---|---|---|---|---|
28
+ | Pundit | No | No | No | App-defined |
29
+ | CanCanCan | No | No | No | App-defined |
30
+ | Action Policy | No | No | No | App-defined |
31
+ | Rolify | Yes | **No** — no permission concept at all | N/A | Partial |
32
+ | **AccessGrant** | Yes | **Yes** | **Yes** | **Yes** |
33
+
34
+ Built **from scratch** (not on Pundit or Rolify). See [`docs/proposal.md`](docs/proposal.md).
35
+
36
+ ## Installation
37
+
38
+ ```ruby
39
+ # Gemfile
40
+ gem "access_grant"
41
+ ```
42
+
43
+ ```
44
+ bundle install
45
+ rails g access_grant:install
46
+ rails g access_grant:setup \
47
+ --multi-tenant \
48
+ --tenant=Organization \
49
+ --user=User \
50
+ --owner-role=protected \
51
+ --tables=auto
52
+ rails db:migrate
53
+ bundle exec rake access_grant:sync_permissions
54
+ ```
55
+
56
+ `setup` patches models with `access_grant :tenant` / `access_grant :user`
57
+ and writes `config/initializers/access_grant.rb` plus
58
+ `config/access_grant/{permissions,roles}.rb`.
59
+
60
+ **On every deploy:** run `bundle exec rake access_grant:sync_permissions`
61
+ after migrate. Catalog sync is **not** a migration.
62
+
63
+ ## Usage
64
+
65
+ ```ruby
66
+ class Organization < ApplicationRecord
67
+ access_grant :tenant
68
+ end
69
+
70
+ class User < ApplicationRecord
71
+ access_grant :user
72
+ end
73
+
74
+ # config/access_grant/permissions.rb
75
+ AccessGrant.permissions do
76
+ resource :invoices do
77
+ action :couple, description: "Can couple invoices together"
78
+ end
79
+ end
80
+
81
+ org.grant_owner!(current_user)
82
+ current_user.permitted?("invoices.index", tenant: org) # => true / false
83
+ ```
84
+
85
+ ```ruby
86
+ # ApplicationController
87
+ access_grant_authorize! # uses current_user + current_tenant
88
+ # host defines: def current_tenant; …; end
89
+
90
+ # Skip public auth endpoints (Devise sessions/registrations/passwords, etc.)
91
+ skip_access_grant_authorize! if: :devise_controller?
92
+
93
+ # Raises AccessGrant::NotAuthorizedError — rescue in the host, e.g.:
94
+ # rescue_from AccessGrant::NotAuthorizedError, with: -> { head :forbidden }
95
+ ```
96
+
97
+ Keys are strictly `resource.action`. Permission descriptions come from the
98
+ catalog (dev/sync only); role descriptions are admin-editable.
99
+
100
+ See [`docs/architecture.md`](docs/architecture.md) for models, Owner,
101
+ [configuration reference](docs/architecture.md#configuration-reference)
102
+ (every `config.*` option with examples), overrides, and the full decision
103
+ table. Acceptance scenarios:
104
+ [`docs/superpowers/specs/2026-09-07-usage-scenarios.md`](docs/superpowers/specs/2026-09-07-usage-scenarios.md).
105
+
106
+ ## Development
107
+
108
+ ```
109
+ bin/setup
110
+ bundle exec rspec
111
+ bundle exec rubocop
112
+ ```
113
+
114
+ Public APIs use **YARD** comments (`@param`, `@return`, `@raise`, `@example`) —
115
+ the usual Ruby documentation style (YARD extends RDoc). IDEs show them on
116
+ hover; optionally generate HTML with `yard doc` if the `yard` gem is installed.
117
+
118
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for branching, commit, and release
119
+ conventions.
120
+
121
+ ## License
122
+
123
+ MIT — see [`LICENSE.txt`](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]