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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +137 -0
- data/lib/yes/auth/cerbos/read_resource_access/principal_attributes.rb +49 -0
- data/lib/yes/auth/cerbos/read_resource_access/principal_data.rb +59 -0
- data/lib/yes/auth/cerbos/write_resource_access/principal_attributes.rb +49 -0
- data/lib/yes/auth/cerbos/write_resource_access/principal_data.rb +59 -0
- data/lib/yes/auth/principals/read_resource_access.rb +23 -0
- data/lib/yes/auth/principals/role.rb +36 -0
- data/lib/yes/auth/principals/user.rb +69 -0
- data/lib/yes/auth/principals/write_resource_access.rb +23 -0
- data/lib/yes/auth/railtie.rb +17 -0
- data/lib/yes/auth/read_models/principals/read_resource_access/builder.rb +24 -0
- data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_principal_assigned.rb +20 -0
- data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_removed.rb +20 -0
- data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_resource_assigned.rb +20 -0
- data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_resource_type_changed.rb +20 -0
- data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_role_changed.rb +20 -0
- data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_scope_changed.rb +20 -0
- data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_service_changed.rb +20 -0
- data/lib/yes/auth/read_models/principals/role/builder.rb +24 -0
- data/lib/yes/auth/read_models/principals/role/on_role_name_changed.rb +20 -0
- data/lib/yes/auth/read_models/principals/user/builder.rb +24 -0
- data/lib/yes/auth/read_models/principals/user/on_principal_attribute_changed.rb +23 -0
- data/lib/yes/auth/read_models/principals/user/on_principal_identity_assigned.rb +20 -0
- data/lib/yes/auth/read_models/principals/user/on_principal_removed.rb +20 -0
- data/lib/yes/auth/read_models/principals/user/on_principal_role_added.rb +24 -0
- data/lib/yes/auth/read_models/principals/user/on_principal_role_removed.rb +24 -0
- data/lib/yes/auth/read_models/principals/write_resource_access/builder.rb +24 -0
- data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_attribute_changed.rb +23 -0
- data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_context_changed.rb +20 -0
- data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_principal_assigned.rb +20 -0
- data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_removed.rb +20 -0
- data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_resource_assigned.rb +20 -0
- data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_resource_type_changed.rb +20 -0
- data/lib/yes/auth/read_models/principals/write_resource_access/on_write_resource_access_role_changed.rb +20 -0
- data/lib/yes/auth/subscriptions.rb +57 -0
- data/lib/yes/auth/version.rb +7 -0
- data/lib/yes/auth.rb +27 -0
- 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
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
|
data/lib/yes/auth/read_models/principals/read_resource_access/on_read_resource_access_removed.rb
ADDED
|
@@ -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
|