yes-auth 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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +137 -0
  5. data/lib/yes/auth/cerbos/read_resource_access/principal_attributes.rb +49 -0
  6. data/lib/yes/auth/cerbos/read_resource_access/principal_data.rb +59 -0
  7. data/lib/yes/auth/cerbos/write_resource_access/principal_attributes.rb +49 -0
  8. data/lib/yes/auth/cerbos/write_resource_access/principal_data.rb +59 -0
  9. data/lib/yes/auth/principals/read_resource_access.rb +23 -0
  10. data/lib/yes/auth/principals/role.rb +36 -0
  11. data/lib/yes/auth/principals/user.rb +69 -0
  12. data/lib/yes/auth/principals/write_resource_access.rb +23 -0
  13. data/lib/yes/auth/railtie.rb +17 -0
  14. data/lib/yes/auth/read_models/principals/read_resource_access/builder.rb +24 -0
  15. data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_principal_assigned.rb +20 -0
  16. data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_removed.rb +20 -0
  17. data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_resource_assigned.rb +20 -0
  18. data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_resource_type_changed.rb +20 -0
  19. data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_role_changed.rb +20 -0
  20. data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_scope_changed.rb +20 -0
  21. data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_service_changed.rb +20 -0
  22. data/lib/yes/auth/read_models/principals/role/builder.rb +24 -0
  23. data/lib/yes/auth/read_models/principals/role/on_role_name_changed.rb +20 -0
  24. data/lib/yes/auth/read_models/principals/user/builder.rb +24 -0
  25. data/lib/yes/auth/read_models/principals/user/on_principal_attribute_changed.rb +23 -0
  26. data/lib/yes/auth/read_models/principals/user/on_principal_identity_assigned.rb +20 -0
  27. data/lib/yes/auth/read_models/principals/user/on_principal_removed.rb +20 -0
  28. data/lib/yes/auth/read_models/principals/user/on_principal_role_added.rb +24 -0
  29. data/lib/yes/auth/read_models/principals/user/on_principal_role_removed.rb +24 -0
  30. data/lib/yes/auth/read_models/principals/write_resource_access/builder.rb +24 -0
  31. data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_attribute_changed.rb +23 -0
  32. data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_context_changed.rb +20 -0
  33. data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_principal_assigned.rb +20 -0
  34. data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_removed.rb +20 -0
  35. data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_resource_assigned.rb +20 -0
  36. data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_resource_type_changed.rb +20 -0
  37. data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_role_changed.rb +20 -0
  38. data/lib/yes/auth/subscriptions.rb +57 -0
  39. data/lib/yes/auth/version.rb +7 -0
  40. data/lib/yes/auth.rb +27 -0
  41. metadata +112 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5b8e3b75db27fa4e2079f268a02bbb3a3d3321a65dc25d7d167888cf8d7f222e
4
+ data.tar.gz: 3def0c48b89e0a3b42032d215d574c6cfb388a8ca40b08d3d07045665520e907
5
+ SHA512:
6
+ metadata.gz: 60b40d20b625ca87978ed8238d7bb4f5d571217afeca6eb809cfaaff960d9278ff05c88d888740ae7dca9f059d90a07f8e2e552eb5f09ff762ee13df91a3e828
7
+ data.tar.gz: 6f7bccb82e4c9b9f4074e76e9595454e1153a3cbb6147fc1dbe86aaf29c4e525aaf84580749dba78a08c6ac259ac44fe78ae1dae1072431f7d5e3d8a2a9db7d0
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2026-03-25
4
+
5
+ ### Added
6
+
7
+ - Initial release
8
+ - Authorization principal models (User, Role, ReadResourceAccess, WriteResourceAccess)
9
+ - Cerbos principal data builders for read and write resource access
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 ncri
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # Yes Auth
2
+
3
+ Authorization principals and Cerbos integration for the [Yes](https://github.com/yousty/yes) event sourcing framework.
4
+
5
+ ## Overview
6
+
7
+ `yes-auth` provides ActiveRecord-backed authorization principal models and Cerbos principal data builders. It is designed to work with `yes-core` to provide role-based access control with fine-grained read and write resource access permissions.
8
+
9
+ ### Principal Models
10
+
11
+ - **User** - represents an authorization principal with roles and resource accesses
12
+ - **Role** - named roles that can be assigned to users and resource accesses
13
+ - **ReadResourceAccess** - links a principal to a role-based read permission scoped by service, scope, and resource type
14
+ - **WriteResourceAccess** - links a principal to a role-based write permission scoped by context and resource type
15
+
16
+ ### Cerbos Integration
17
+
18
+ - **WriteResourceAccess::PrincipalData** - builds Cerbos principal data from write resource accesses
19
+ - **ReadResourceAccess::PrincipalData** - builds Cerbos principal data from read resource accesses
20
+
21
+ ## Installation
22
+
23
+ Add to your `Gemfile`:
24
+
25
+ ```ruby
26
+ gem 'yes-auth'
27
+ ```
28
+
29
+ Or if using a monorepo with path references:
30
+
31
+ ```ruby
32
+ gem 'yes-auth', path: 'yes-auth'
33
+ ```
34
+
35
+ ### Auto-Configuration
36
+
37
+ When loaded in a Rails application, yes-auth automatically configures the Cerbos principal data builders in yes-core:
38
+
39
+ - `config.cerbos_principal_data_builder` → `Yes::Auth::Cerbos::WriteResourceAccess::PrincipalData`
40
+ - `config.cerbos_read_principal_data_builder` → `Yes::Auth::Cerbos::ReadResourceAccess::PrincipalData`
41
+
42
+ This means you don't need to manually configure these in your initializer — just adding `yes-auth` to your Gemfile is enough.
43
+
44
+ To override the default builders, set them explicitly in your initializer (after yes-auth's railtie runs):
45
+
46
+ ```ruby
47
+ Yes::Core.configure do |config|
48
+ config.cerbos_principal_data_builder = MyCustomPrincipalDataBuilder.method(:call)
49
+ end
50
+ ```
51
+
52
+ ### Environment Variables
53
+
54
+ | Variable | Default | Description |
55
+ |----------|---------|-------------|
56
+ | `CERBOS_URL` | `cerbos-cluster-ip-service:3593` | Cerbos server address (set via yes-core config) |
57
+
58
+ ## Usage
59
+
60
+ ### Principal Models
61
+
62
+ ```ruby
63
+ # Find a user by identity
64
+ user = Yes::Auth::Principals::User.find_by(identity_id: 'user-uuid')
65
+
66
+ # Check roles
67
+ user.read_resource_access_authorization_roles
68
+ user.write_resource_access_authorization_roles
69
+ user.super_admin?
70
+
71
+ # Access resource permissions
72
+ user.read_resource_accesses
73
+ user.write_resource_accesses
74
+ ```
75
+
76
+ ### Cerbos Principal Data
77
+
78
+ Build principal data for Cerbos authorization checks:
79
+
80
+ ```ruby
81
+ # For write operations
82
+ write_data = Yes::Auth::Cerbos::WriteResourceAccess::PrincipalData.call(
83
+ identity_id: 'user-uuid'
84
+ )
85
+ # => { id: 'identity-id', roles: ['manager'], attributes: { write_resource_access: { ... } } }
86
+
87
+ # For read operations
88
+ read_data = Yes::Auth::Cerbos::ReadResourceAccess::PrincipalData.call(
89
+ identity_id: 'user-uuid'
90
+ )
91
+ # => { id: 'identity-id', roles: ['viewer'], attributes: { read_resource_access: { ... } } }
92
+ ```
93
+
94
+ ### Configuration
95
+
96
+ Plug into `yes-core`'s Cerbos authorization by configuring the principal data builder:
97
+
98
+ ```ruby
99
+ # config/initializers/yes.rb
100
+ Yes::Core.configure do |config|
101
+ config.cerbos_principal_data_builder = Yes::Auth::Cerbos::WriteResourceAccess::PrincipalData
102
+ end
103
+ ```
104
+
105
+ For read APIs that need read-scoped authorization:
106
+
107
+ ```ruby
108
+ Yes::Core.configure do |config|
109
+ config.cerbos_principal_data_builder = Yes::Auth::Cerbos::ReadResourceAccess::PrincipalData
110
+ end
111
+ ```
112
+
113
+ ## Database Schema
114
+
115
+ The gem expects the following tables to exist:
116
+
117
+ - `auth_principals_users` - stores user principals
118
+ - `auth_principals_roles` - stores named roles
119
+ - `auth_principals_read_resource_accesses` - stores read resource access records
120
+ - `auth_principals_write_resource_accesses` - stores write resource access records
121
+ - A join table for the users-roles HABTM association
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ cd yes-auth
127
+ bundle install
128
+ bundle exec rspec
129
+ ```
130
+
131
+ ## Contributing
132
+
133
+ See the [contributing guide](../CONTRIBUTING.md) for instructions on how to contribute to the Yes framework.
134
+
135
+ ## License
136
+
137
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module Cerbos
6
+ module ReadResourceAccess
7
+ # Builds principal attributes for Cerbos authorization based on read resource accesses.
8
+ #
9
+ # @example Building attributes
10
+ # Yes::Auth::Cerbos::ReadResourceAccess::PrincipalAttributes.call(
11
+ # principal: user,
12
+ # read_resource_accesses: accesses
13
+ # )
14
+ class PrincipalAttributes
15
+ class << self
16
+ # @param principal [Yes::Auth::Principals::User, nil] the principal user
17
+ # @param read_resource_accesses [Array, ActiveRecord::Relation] read resource accesses
18
+ # @return [HashWithIndifferentAccess] Cerbos principal attributes
19
+ def call(principal: nil, read_resource_accesses: [])
20
+ return {} unless principal
21
+
22
+ {
23
+ **(principal.auth_attributes || {}),
24
+ read_resource_access: read_attributes(read_resource_accesses)
25
+ }.with_indifferent_access
26
+ end
27
+
28
+ private
29
+
30
+ # @param accesses [Array, ActiveRecord::Relation] read resource accesses
31
+ # @return [Hash] nested hash of read resource access attributes
32
+ def read_attributes(accesses)
33
+ attributes = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
34
+
35
+ accesses.each do |access|
36
+ next unless access.authorization_complete?
37
+
38
+ attributes[access.service][access.scope][access.resource_type][access.role&.resource_authorization_name][access.resource_id] =
39
+ access.auth_attributes || {}
40
+ end
41
+
42
+ attributes
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module Cerbos
6
+ module ReadResourceAccess
7
+ # Builds principal data for Cerbos authorization based on read resource accesses.
8
+ #
9
+ # @example Building principal data
10
+ # Yes::Auth::Cerbos::ReadResourceAccess::PrincipalData.call(identity_id: 'user-uuid')
11
+ # # => { id: 'identity-id', roles: ['role1'], attributes: { ... } }
12
+ class PrincipalData
13
+ class << self
14
+ # @param auth_data [Hash] authentication data containing :identity_id
15
+ # @return [Hash] Cerbos-compatible principal data, or empty hash if principal not found
16
+ def call(auth_data)
17
+ return {} unless (principal = load_principal(auth_data[:identity_id]))
18
+
19
+ read_resource_accesses = load_read_resource_accesses(principal.id)
20
+
21
+ {
22
+ id: principal.identity_id,
23
+ roles: roles(principal),
24
+ attributes: attributes(principal, read_resource_accesses)
25
+ }.with_indifferent_access
26
+ end
27
+
28
+ private
29
+
30
+ # @param principal_id [String] the principal's database ID
31
+ # @return [ActiveRecord::Relation] read resource accesses with joined roles
32
+ def load_read_resource_accesses(principal_id)
33
+ Principals::ReadResourceAccess.eager_load(:role).where(principal_id:)
34
+ end
35
+
36
+ # @param identity_id [String] the identity ID to look up
37
+ # @return [Yes::Auth::Principals::User, nil] the found principal or nil
38
+ def load_principal(identity_id)
39
+ Principals::User.includes(:roles).find_by(identity_id:)
40
+ end
41
+
42
+ # @param principal [Yes::Auth::Principals::User] the principal user
43
+ # @return [Array<String>] authorization roles or fallback roles
44
+ def roles(principal)
45
+ principal.read_resource_access_authorization_roles.presence || Principals::User::NO_AUTHORIZATION_ROLES_YET
46
+ end
47
+
48
+ # @param principal [Yes::Auth::Principals::User] the principal user
49
+ # @param read_resource_accesses [ActiveRecord::Relation] the read resource accesses
50
+ # @return [Hash] Cerbos principal attributes
51
+ def attributes(principal, read_resource_accesses)
52
+ PrincipalAttributes.call(principal:, read_resource_accesses:)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module Cerbos
6
+ module WriteResourceAccess
7
+ # Builds principal attributes for Cerbos authorization based on write resource accesses.
8
+ #
9
+ # @example Building attributes
10
+ # Yes::Auth::Cerbos::WriteResourceAccess::PrincipalAttributes.call(
11
+ # principal: user,
12
+ # write_resource_accesses: accesses
13
+ # )
14
+ class PrincipalAttributes
15
+ class << self
16
+ # @param principal [Yes::Auth::Principals::User, nil] the principal user
17
+ # @param write_resource_accesses [Array, ActiveRecord::Relation] write resource accesses
18
+ # @return [HashWithIndifferentAccess] Cerbos principal attributes
19
+ def call(principal: nil, write_resource_accesses: [])
20
+ return {} unless principal
21
+
22
+ {
23
+ **(principal.auth_attributes || {}),
24
+ write_resource_access: write_attributes(write_resource_accesses)
25
+ }.with_indifferent_access
26
+ end
27
+
28
+ private
29
+
30
+ # @param accesses [Array, ActiveRecord::Relation] write resource accesses
31
+ # @return [Hash] nested hash of write resource access attributes
32
+ def write_attributes(accesses)
33
+ attributes = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
34
+
35
+ accesses.each do |access|
36
+ next unless access.authorization_complete?
37
+
38
+ attributes[access.context][access.resource_type][access.role&.resource_authorization_name][access.resource_id] =
39
+ access.auth_attributes || {}
40
+ end
41
+
42
+ attributes
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module Cerbos
6
+ module WriteResourceAccess
7
+ # Builds principal data for Cerbos authorization based on write resource accesses.
8
+ #
9
+ # @example Building principal data
10
+ # Yes::Auth::Cerbos::WriteResourceAccess::PrincipalData.call(identity_id: 'user-uuid')
11
+ # # => { id: 'identity-id', roles: ['role1'], attributes: { ... } }
12
+ class PrincipalData
13
+ class << self
14
+ # @param auth_data [Hash] authentication data containing :identity_id
15
+ # @return [Hash] Cerbos-compatible principal data, or empty hash if principal not found
16
+ def call(auth_data)
17
+ return {} unless (principal = load_principal(auth_data[:identity_id]))
18
+
19
+ write_resource_accesses = load_write_resource_accesses(principal.id)
20
+
21
+ {
22
+ id: principal.identity_id,
23
+ roles: roles(principal),
24
+ attributes: attributes(principal, write_resource_accesses)
25
+ }.with_indifferent_access
26
+ end
27
+
28
+ private
29
+
30
+ # @param principal_id [String] the principal's database ID
31
+ # @return [ActiveRecord::Relation] write resource accesses with joined roles
32
+ def load_write_resource_accesses(principal_id)
33
+ Principals::WriteResourceAccess.eager_load(:role).where(principal_id:)
34
+ end
35
+
36
+ # @param identity_id [String] the identity ID to look up
37
+ # @return [Yes::Auth::Principals::User, nil] the found principal or nil
38
+ def load_principal(identity_id)
39
+ Principals::User.includes(:roles).find_by(identity_id:)
40
+ end
41
+
42
+ # @param principal [Yes::Auth::Principals::User] the principal user
43
+ # @return [Array<String>] authorization roles or fallback roles
44
+ def roles(principal)
45
+ principal.write_resource_access_authorization_roles.presence || Principals::User::NO_AUTHORIZATION_ROLES_YET
46
+ end
47
+
48
+ # @param principal [Yes::Auth::Principals::User] the principal user
49
+ # @param write_resource_accesses [ActiveRecord::Relation] the write resource accesses
50
+ # @return [Hash] Cerbos principal attributes
51
+ def attributes(principal, write_resource_accesses)
52
+ PrincipalAttributes.call(principal:, write_resource_accesses:)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module Principals
6
+ # Represents a read resource access record linking a principal to a role-based read permission.
7
+ #
8
+ # @example Checking if an access is complete
9
+ # access = Yes::Auth::Principals::ReadResourceAccess.find(id)
10
+ # access.authorization_complete?
11
+ class ReadResourceAccess < ActiveRecord::Base
12
+ self.table_name = 'auth_principals_read_resource_accesses'
13
+
14
+ belongs_to :role, class_name: 'Yes::Auth::Principals::Role', optional: true
15
+
16
+ # @return [Boolean] whether all required fields are present for authorization
17
+ def authorization_complete?
18
+ service.present? && scope.present? && resource_type.present? && role.present? && resource_id.present?
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module Principals
6
+ # Represents an authorization role that can be assigned to users and resource accesses.
7
+ #
8
+ # @example Finding the super admin role
9
+ # Yes::Auth::Principals::Role.super_admin_role
10
+ class Role < ActiveRecord::Base
11
+ self.table_name = 'auth_principals_roles'
12
+
13
+ SUPER_ADMIN_ROLE_NAME = 'admin'
14
+
15
+ has_and_belongs_to_many :users, class_name: 'Yes::Auth::Principals::User',
16
+ foreign_key: :auth_principals_role_id,
17
+ association_foreign_key: :auth_principals_user_id
18
+
19
+ has_many :read_resource_accesses, class_name: 'Yes::Auth::Principals::ReadResourceAccess'
20
+ has_many :write_resource_accesses, class_name: 'Yes::Auth::Principals::WriteResourceAccess'
21
+
22
+ scope :complete, -> { where.not(name: nil) }
23
+
24
+ # @return [String, nil] the role name with colons replaced by underscores
25
+ def resource_authorization_name
26
+ name&.tr(':', '_')
27
+ end
28
+
29
+ # @return [Yes::Auth::Principals::Role, nil] the super admin role
30
+ def self.super_admin_role
31
+ find_by(name: SUPER_ADMIN_ROLE_NAME)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module Principals
6
+ # Represents an authorization principal user with roles and resource accesses.
7
+ #
8
+ # @example Finding a user and checking roles
9
+ # user = Yes::Auth::Principals::User.find_by(identity_id: 'some-uuid')
10
+ # user.read_resource_access_authorization_roles
11
+ class User < ActiveRecord::Base
12
+ self.table_name = 'auth_principals_users'
13
+
14
+ NO_AUTHORIZATION_ROLES_YET = ['no-roles-yet'].freeze
15
+
16
+ has_and_belongs_to_many :roles, class_name: 'Yes::Auth::Principals::Role',
17
+ foreign_key: :auth_principals_user_id,
18
+ association_foreign_key: :auth_principals_role_id
19
+
20
+ has_many :write_resource_accesses, class_name: 'Yes::Auth::Principals::WriteResourceAccess',
21
+ foreign_key: :principal_id
22
+
23
+ has_many :read_resource_accesses, class_name: 'Yes::Auth::Principals::ReadResourceAccess',
24
+ foreign_key: :principal_id
25
+
26
+ # @return [Array<String>] role names for read resource access authorization
27
+ # NOTE: Runs 2 queries (resource access roles + direct roles). The direct roles
28
+ # query is shared with write_resource_access_authorization_roles but cannot be
29
+ # easily combined since they query different join tables. Use .includes(:roles)
30
+ # when loading the User to avoid N+1 on the direct roles association.
31
+ def read_resource_access_authorization_roles
32
+ read_role_names = Set.new(
33
+ Role.joins(:read_resource_accesses).where(read_resource_accesses: { principal_id: id }).complete.pluck(:name)
34
+ )
35
+
36
+ (read_role_names + complete_role_names).to_a
37
+ end
38
+
39
+ # @return [Array<String>] role names for write resource access authorization
40
+ # NOTE: Runs 2 queries (resource access roles + direct roles). The direct roles
41
+ # query is shared with read_resource_access_authorization_roles but cannot be
42
+ # easily combined since they query different join tables. Use .includes(:roles)
43
+ # when loading the User to avoid N+1 on the direct roles association.
44
+ def write_resource_access_authorization_roles
45
+ write_role_names = Set.new(
46
+ Role.joins(:write_resource_accesses).where(write_resource_accesses: { principal_id: id }).complete.pluck(:name)
47
+ )
48
+
49
+ (write_role_names + complete_role_names).to_a
50
+ end
51
+
52
+ # @return [Boolean] whether the user has the super admin role
53
+ def super_admin?
54
+ super_admin_role_id = Role.super_admin_role&.id
55
+ return false unless super_admin_role_id
56
+
57
+ roles.ids.include?(super_admin_role_id)
58
+ end
59
+
60
+ private
61
+
62
+ # @return [Array<String>] cached complete role names for the user
63
+ def complete_role_names
64
+ @complete_role_names ||= roles.complete.pluck(:name)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module Principals
6
+ # Represents a write resource access record linking a principal to a role-based write permission.
7
+ #
8
+ # @example Checking if an access is complete
9
+ # access = Yes::Auth::Principals::WriteResourceAccess.find(id)
10
+ # access.authorization_complete?
11
+ class WriteResourceAccess < ActiveRecord::Base
12
+ self.table_name = 'auth_principals_write_resource_accesses'
13
+
14
+ belongs_to :role, class_name: 'Yes::Auth::Principals::Role', optional: true
15
+
16
+ # @return [Boolean] whether all required fields are present for authorization
17
+ def authorization_complete?
18
+ context.present? && resource_type.present? && role.present? && resource_id.present?
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ # Auto-configures yes-core's Cerbos principal data builders when yes-auth is loaded.
6
+ class Railtie < Rails::Railtie
7
+ initializer 'yes_auth.configure_cerbos' do
8
+ Yes::Core.configure do |config|
9
+ config.cerbos_principal_data_builder =
10
+ Yes::Auth::Cerbos::WriteResourceAccess::PrincipalData.method(:call)
11
+ config.cerbos_read_principal_data_builder =
12
+ Yes::Auth::Cerbos::ReadResourceAccess::PrincipalData.method(:call)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module ReadModels
6
+ module Principals
7
+ module ReadResourceAccess
8
+ # @see Yes::Core::ReadModel::Builder
9
+ class Builder < Yes::Core::ReadModel::Builder
10
+ private
11
+
12
+ def default_read_model_class
13
+ Yes::Auth::Principals::ReadResourceAccess
14
+ end
15
+
16
+ def aggregate_id_key
17
+ 'read_resource_access_id'
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module ReadModels
6
+ module Principals
7
+ module ReadResourceAccess
8
+ # @see Yes::Core::ReadModel::EventHandler
9
+ class OnReadResourceAccessPrincipalAssigned < Yes::Core::ReadModel::EventHandler
10
+ # @param event [Yes::Core::Event]
11
+ # @return [void]
12
+ def call(event)
13
+ read_model.update_columns(principal_id: event.data['principal_id'])
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module ReadModels
6
+ module Principals
7
+ module ReadResourceAccess
8
+ # @see Yes::Core::ReadModel::EventHandler
9
+ class OnReadResourceAccessRemoved < Yes::Core::ReadModel::EventHandler
10
+ # @param event [Yes::Core::Event]
11
+ # @return [void]
12
+ def call(_event)
13
+ read_model.delete
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module ReadModels
6
+ module Principals
7
+ module ReadResourceAccess
8
+ # @see Yes::Core::ReadModel::EventHandler
9
+ class OnReadResourceAccessResourceAssigned < Yes::Core::ReadModel::EventHandler
10
+ # @param event [Yes::Core::Event]
11
+ # @return [void]
12
+ def call(event)
13
+ read_model.update_columns(resource_id: event.data['resource_id'])
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yes
4
+ module Auth
5
+ module ReadModels
6
+ module Principals
7
+ module ReadResourceAccess
8
+ # @see Yes::Core::ReadModel::EventHandler
9
+ class OnReadResourceAccessResourceTypeChanged < Yes::Core::ReadModel::EventHandler
10
+ # @param event [Yes::Core::Event]
11
+ # @return [void]
12
+ def call(event)
13
+ read_model.update_columns(resource_type: event.data['resource_type'])
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end