verikloak-pundit 0.1.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 +16 -0
- data/LICENSE +21 -0
- data/README.md +135 -0
- data/lib/generators/verikloak/pundit/install/install_generator.rb +34 -0
- data/lib/generators/verikloak/pundit/install/templates/application_policy.rb +49 -0
- data/lib/generators/verikloak/pundit/install/templates/initializer.rb +24 -0
- data/lib/verikloak/pundit/configuration.rb +38 -0
- data/lib/verikloak/pundit/controller.rb +26 -0
- data/lib/verikloak/pundit/helpers.rb +29 -0
- data/lib/verikloak/pundit/policy.rb +36 -0
- data/lib/verikloak/pundit/railtie.rb +17 -0
- data/lib/verikloak/pundit/role_mapper.rb +21 -0
- data/lib/verikloak/pundit/user_context.rb +144 -0
- data/lib/verikloak/pundit/version.rb +10 -0
- data/lib/verikloak/pundit.rb +35 -0
- data/lib/verikloak-pundit.rb +8 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6ece87ad9829b8123230d55d270c57bf399c3b16a5c8d38e7e89e3c2e31a3f63
|
|
4
|
+
data.tar.gz: 511d923d115ebe33105099ab61b0390e46288e013ecca8a455c9b896abf9d5e7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: acf8d7d1a12e3b12ce152deb35c4c506adbb6a9002cbc30aa9293ed8c5577c838692795c27edc332802db689a5af9ae55dcf6e9b06abd18f8b3cf77e06f1a331
|
|
7
|
+
data.tar.gz: 9a514dcdd539c3a9e1ef7abbcfb858df7a8899ecb71985972b8a9199d2153efd434f06e3cbb798daf5ea9a990c7e41b750f189dbf01b0149208ead90b0f39072
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2025-09-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial public release of `verikloak-pundit`.
|
|
14
|
+
- `Verikloak::Pundit::UserContext` for working with Keycloak JWT claims.
|
|
15
|
+
- Rails controller helpers and generator for installing the initializer and base `ApplicationPolicy`.
|
|
16
|
+
- Role mapping configuration (`role_map`, `realm_roles_path`, `resource_roles_path`, `permission_role_scope`).
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 taiyaky
|
|
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,135 @@
|
|
|
1
|
+
# verikloak-pundit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/taiyaky/verikloak-pundit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/verikloak-pundit)
|
|
5
|
+

|
|
6
|
+
[](https://rubygems.org/gems/verikloak-pundit)
|
|
7
|
+
|
|
8
|
+
Pundit integration for the **Verikloak** family. This gem maps **Keycloak roles** from JWT claims (e.g., `realm_access.roles`, `resource_access[client].roles`) into a convenient **UserContext** that Pundit policies can consume.
|
|
9
|
+
|
|
10
|
+
- Requires [`verikloak`](https://rubygems.org/gems/verikloak) at runtime and pairs well with [`verikloak-rails`](https://rubygems.org/gems/verikloak-rails) for Rails integrations.
|
|
11
|
+
- Provides a `pundit_user` hook so policies can use `user.has_role?(:admin)` etc.
|
|
12
|
+
- Keeps role mapping **configurable** (project-specific mappings differ).
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **UserContext**: lightweight wrapper around JWT claims
|
|
19
|
+
- **Helpers**: `has_role?`, `in_group?`, `resource_role?(client, role)`
|
|
20
|
+
- **RoleMapper**: optional map from Keycloak roles → domain permissions
|
|
21
|
+
- **Controller integration**: `pundit_user` provider for Rails controllers
|
|
22
|
+
- **Generator**: `rails g verikloak:pundit:install` creates initializer + policy template (with `has_permission?` support for realm roles plus the configured resource scope)
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bundle add verikloak-pundit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If you're on Rails:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
rails g verikloak:pundit:install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This generates:
|
|
37
|
+
|
|
38
|
+
- `config/initializers/verikloak_pundit.rb`
|
|
39
|
+
- `app/policies/application_policy.rb` (template if missing; optional)
|
|
40
|
+
|
|
41
|
+
For error-handling guidance, see [ERRORS.md](ERRORS.md).
|
|
42
|
+
|
|
43
|
+
## Quick Start (Rails)
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# app/controllers/application_controller.rb
|
|
47
|
+
class ApplicationController < ActionController::API
|
|
48
|
+
include Pundit
|
|
49
|
+
include Verikloak::Pundit::Controller # provides pundit_user
|
|
50
|
+
|
|
51
|
+
# If you're also using verikloak-rails:
|
|
52
|
+
# before_action :authenticate_user!
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Your policy can then do:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class NotePolicy < ApplicationPolicy
|
|
60
|
+
def update?
|
|
61
|
+
user.has_role?(:admin) || user.resource_role?(:'rails-api', :editor)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Where `user` is the **UserContext** provided by `pundit_user`.
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# config/initializers/verikloak_pundit.rb
|
|
72
|
+
Verikloak::Pundit.configure do |c|
|
|
73
|
+
c.resource_client = "rails-api" # default client for resource roles
|
|
74
|
+
c.role_map = { # optional role → permission mapping
|
|
75
|
+
admin: :manage_all,
|
|
76
|
+
editor: :write_notes,
|
|
77
|
+
reader: :read_notes
|
|
78
|
+
}
|
|
79
|
+
# Where to find claims in Rack env (when using verikloak/verikloak-rails)
|
|
80
|
+
c.env_claims_key = "verikloak.user"
|
|
81
|
+
|
|
82
|
+
# How to traverse JWT for roles
|
|
83
|
+
c.realm_roles_path = %w[realm_access roles] # => claims["realm_access"]["roles"]
|
|
84
|
+
# Lambdas in the path may accept (cfg) or (cfg, client)
|
|
85
|
+
# where `client` is the argument passed to `user.resource_roles(client)`
|
|
86
|
+
c.resource_roles_path = ["resource_access", ->(cfg){ cfg.resource_client }, "roles"]
|
|
87
|
+
|
|
88
|
+
# Permission mapping scope for `user.has_permission?`:
|
|
89
|
+
# :default_resource => realm roles + default client roles (recommended)
|
|
90
|
+
# :all_resources => realm roles + roles from all clients in resource_access
|
|
91
|
+
c.permission_role_scope = :default_resource
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Non-Rails / custom usage
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
claims = { "sub" => "123", "email" => "a@b", "realm_access" => {"roles" => ["admin"]} }
|
|
99
|
+
ctx = Verikloak::Pundit::UserContext.new(claims, resource_client: "rails-api")
|
|
100
|
+
|
|
101
|
+
ctx.has_role?(:admin) # => true
|
|
102
|
+
ctx.resource_role?(:"rails-api", :writer) # depends on resource_access
|
|
103
|
+
ctx.has_permission?(:manage_all) # from role_map, realm or resource roles
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Testing
|
|
107
|
+
All pull requests and pushes are automatically tested with [RSpec](https://rspec.info/) and [RuboCop](https://rubocop.org/) via GitHub Actions.
|
|
108
|
+
See the CI badge at the top for current build status.
|
|
109
|
+
|
|
110
|
+
To run the test suite locally:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
docker compose run --rm dev rspec
|
|
114
|
+
docker compose run --rm dev rubocop -a
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Contributing
|
|
118
|
+
Bug reports and pull requests are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
119
|
+
|
|
120
|
+
## Security
|
|
121
|
+
If you find a security vulnerability, please follow the instructions in [SECURITY.md](SECURITY.md).
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
125
|
+
|
|
126
|
+
## Publishing (for maintainers)
|
|
127
|
+
Gem release instructions are documented separately in [MAINTAINERS.md](MAINTAINERS.md).
|
|
128
|
+
|
|
129
|
+
## Changelog
|
|
130
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
131
|
+
|
|
132
|
+
## References
|
|
133
|
+
- Verikloak (core): https://github.com/taiyaky/verikloak
|
|
134
|
+
- verikloak-rails (Rails integration): https://github.com/taiyaky/verikloak-rails
|
|
135
|
+
- verikloak-pundit on RubyGems: https://rubygems.org/gems/verikloak-pundit
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module Verikloak
|
|
6
|
+
module Pundit
|
|
7
|
+
module Generators
|
|
8
|
+
# Generator to install initializer and base ApplicationPolicy template.
|
|
9
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
10
|
+
source_root File.expand_path('templates', __dir__)
|
|
11
|
+
desc 'Creates Verikloak Pundit initializer and a base ApplicationPolicy (optional).'
|
|
12
|
+
|
|
13
|
+
# Skip creating application_policy.rb
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
class_option :skip_policy, type: :boolean, default: false, desc: 'Do not create application_policy.rb'
|
|
16
|
+
|
|
17
|
+
# Create the initializer file under config/initializers.
|
|
18
|
+
def create_initializer
|
|
19
|
+
template 'initializer.rb', 'config/initializers/verikloak_pundit.rb'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Create app/policies/application_policy.rb unless present.
|
|
23
|
+
def create_application_policy
|
|
24
|
+
return if options[:skip_policy]
|
|
25
|
+
|
|
26
|
+
dest = 'app/policies/application_policy.rb'
|
|
27
|
+
return if File.exist?(dest)
|
|
28
|
+
|
|
29
|
+
template 'application_policy.rb', dest
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Base Pundit policy for applications using verikloak-pundit.
|
|
4
|
+
#
|
|
5
|
+
# This class is intended as a starting point. Override per-action
|
|
6
|
+
# predicates (e.g., show?, update?) in your concrete policies.
|
|
7
|
+
class ApplicationPolicy
|
|
8
|
+
attr_reader :user, :record
|
|
9
|
+
|
|
10
|
+
# `user` is a Verikloak::Pundit::UserContext
|
|
11
|
+
# @param user [Object] Pundit user (UserContext when using verikloak-pundit)
|
|
12
|
+
# @param record [Object] The resource being authorized
|
|
13
|
+
def initialize(user, record)
|
|
14
|
+
@user = user
|
|
15
|
+
@record = record
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [Boolean]
|
|
19
|
+
def index? = false
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def show? = false
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
def create? = false
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def new? = create?
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def update? = false
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
def edit? = update?
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def destroy? = false
|
|
32
|
+
|
|
33
|
+
# Default scope for application policies.
|
|
34
|
+
class Scope
|
|
35
|
+
attr_reader :user, :scope
|
|
36
|
+
|
|
37
|
+
# @param user [Object] Pundit user (UserContext)
|
|
38
|
+
# @param scope [Class,Array] model class or dataset
|
|
39
|
+
def initialize(user, scope)
|
|
40
|
+
@user = user
|
|
41
|
+
@scope = scope
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Object] resolved scope
|
|
45
|
+
def resolve
|
|
46
|
+
scope.all
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Verikloak::Pundit initializer
|
|
4
|
+
# Configure how to read roles from Keycloak claims and how to map them
|
|
5
|
+
# into application permissions.
|
|
6
|
+
Verikloak::Pundit.configure do |c|
|
|
7
|
+
c.resource_client = ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
|
|
8
|
+
c.role_map = {
|
|
9
|
+
# admin: :manage_all,
|
|
10
|
+
# editor: :write_notes,
|
|
11
|
+
# reader: :read_notes
|
|
12
|
+
}
|
|
13
|
+
c.env_claims_key = 'verikloak.user'
|
|
14
|
+
# claims['realm_access']['roles']
|
|
15
|
+
c.realm_roles_path = %w[realm_access roles]
|
|
16
|
+
# rubocop:disable Style/SymbolProc -- we need a Proc object here, not block pass
|
|
17
|
+
# claims['resource_access'][resource_client]['roles']
|
|
18
|
+
c.resource_roles_path = ['resource_access', ->(cfg) { cfg.resource_client }, 'roles']
|
|
19
|
+
# rubocop:enable Style/SymbolProc
|
|
20
|
+
# Permission mapping scope:
|
|
21
|
+
# :default_resource => realm roles + default client roles (recommended)
|
|
22
|
+
# :all_resources => realm roles + roles from all clients in resource_access
|
|
23
|
+
c.permission_role_scope = :default_resource
|
|
24
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verikloak
|
|
4
|
+
module Pundit
|
|
5
|
+
# Runtime configuration for verikloak-pundit.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute resource_client
|
|
8
|
+
# @return [String] default Keycloak resource client used for resource roles
|
|
9
|
+
# @!attribute role_map
|
|
10
|
+
# @return [Hash{Symbol=>Symbol,String}] mapping from roles to permissions
|
|
11
|
+
# @!attribute env_claims_key
|
|
12
|
+
# @return [String] Rack env key where claims are stored (when using verikloak/verikloak-rails)
|
|
13
|
+
# @!attribute realm_roles_path
|
|
14
|
+
# @return [Array<String,Proc>] path inside JWT claims to reach realm roles
|
|
15
|
+
# @!attribute resource_roles_path
|
|
16
|
+
# @return [Array<String,Proc>] path inside JWT claims to reach resource roles
|
|
17
|
+
# @!attribute permission_role_scope
|
|
18
|
+
# @return [Symbol] :default_resource or :all_resources for permission mapping scope
|
|
19
|
+
class Configuration
|
|
20
|
+
attr_accessor :resource_client, :role_map, :env_claims_key,
|
|
21
|
+
:realm_roles_path, :resource_roles_path,
|
|
22
|
+
:permission_role_scope
|
|
23
|
+
|
|
24
|
+
# Initialize default configuration values.
|
|
25
|
+
def initialize
|
|
26
|
+
@resource_client = 'rails-api'
|
|
27
|
+
@role_map = {} # e.g., { admin: :manage_all }
|
|
28
|
+
@env_claims_key = 'verikloak.user'
|
|
29
|
+
@realm_roles_path = %w[realm_access roles]
|
|
30
|
+
# rubocop:disable Style/SymbolProc -- we need a Proc object here, not block pass
|
|
31
|
+
@resource_roles_path = ['resource_access', ->(cfg) { cfg.resource_client }, 'roles']
|
|
32
|
+
# rubocop:enable Style/SymbolProc
|
|
33
|
+
# :default_resource (realm + default client), :all_resources (realm + all clients)
|
|
34
|
+
@permission_role_scope = :default_resource
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verikloak
|
|
4
|
+
module Pundit
|
|
5
|
+
# Rails controller mixin providing `pundit_user` and claims accessor.
|
|
6
|
+
module Controller
|
|
7
|
+
# Hook used by Rails to include helper methods in views when available.
|
|
8
|
+
# @param base [Class]
|
|
9
|
+
def self.included(base)
|
|
10
|
+
base.helper_method :verikloak_claims if base.respond_to?(:helper_method)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Pundit hook returning the UserContext built from Rack env claims.
|
|
14
|
+
# @return [UserContext]
|
|
15
|
+
def pundit_user
|
|
16
|
+
Verikloak::Pundit::UserContext.from_env(request.env)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Access raw Verikloak claims from Rack env.
|
|
20
|
+
# @return [Hash, nil]
|
|
21
|
+
def verikloak_claims
|
|
22
|
+
request.env[Verikloak::Pundit.config.env_claims_key]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verikloak
|
|
4
|
+
module Pundit
|
|
5
|
+
# Helpers expose convenient delegations to the policy `user`.
|
|
6
|
+
module Helpers
|
|
7
|
+
# Check whether the user has a realm role.
|
|
8
|
+
# @param role [String, Symbol]
|
|
9
|
+
# @return [Boolean]
|
|
10
|
+
def has_role?(role) = user.has_role?(role) # rubocop:disable Naming/PredicatePrefix
|
|
11
|
+
|
|
12
|
+
# Check whether the user belongs to a group (alias to role).
|
|
13
|
+
# @param group [String, Symbol]
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
def in_group?(group) = user.in_group?(group)
|
|
16
|
+
|
|
17
|
+
# Check whether the user has a role for a specific resource client.
|
|
18
|
+
# @param client [String, Symbol]
|
|
19
|
+
# @param role [String, Symbol]
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def resource_role?(client, role) = user.resource_role?(client, role)
|
|
22
|
+
|
|
23
|
+
# Check whether the user has a mapped permission.
|
|
24
|
+
# @param perm [String, Symbol]
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def has_permission?(perm) = user.has_permission?(perm) # rubocop:disable Naming/PredicatePrefix
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verikloak
|
|
4
|
+
module Pundit
|
|
5
|
+
# Policy mixin to delegate common helpers to the `user` context.
|
|
6
|
+
module Policy
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.extend(ClassMethods)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Placeholder for future class-level helpers
|
|
12
|
+
module ClassMethods; end
|
|
13
|
+
|
|
14
|
+
# Check whether the user has a realm role.
|
|
15
|
+
# @param role [String, Symbol]
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def has_role?(role) = user.has_role?(role) # rubocop:disable Naming/PredicatePrefix
|
|
18
|
+
|
|
19
|
+
# Check whether the user belongs to a group (alias to role).
|
|
20
|
+
# @param group [String, Symbol]
|
|
21
|
+
# @return [Boolean]
|
|
22
|
+
def in_group?(group) = user.in_group?(group)
|
|
23
|
+
|
|
24
|
+
# Check whether the user has a role for a specific resource client.
|
|
25
|
+
# @param client [String, Symbol]
|
|
26
|
+
# @param role [String, Symbol]
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
def resource_role?(client, role) = user.resource_role?(client, role)
|
|
29
|
+
|
|
30
|
+
# Check whether the user has a mapped permission.
|
|
31
|
+
# @param perm [String, Symbol]
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def has_permission?(perm) = user.has_permission?(perm) # rubocop:disable Naming/PredicatePrefix
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
|
|
5
|
+
module Verikloak
|
|
6
|
+
module Pundit
|
|
7
|
+
# Railtie to auto-include Controller helpers in Rails.
|
|
8
|
+
class Railtie < ::Rails::Railtie
|
|
9
|
+
# Include controller helpers into ActionController when it loads.
|
|
10
|
+
initializer 'verikloak_pundit.controller' do
|
|
11
|
+
ActiveSupport.on_load(:action_controller) do
|
|
12
|
+
include Verikloak::Pundit::Controller
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verikloak
|
|
4
|
+
module Pundit
|
|
5
|
+
# Maps roles to permissions using project configuration.
|
|
6
|
+
module RoleMapper
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Map a Keycloak role to a domain permission via configuration.
|
|
10
|
+
#
|
|
11
|
+
# @param role [String, Symbol] Role name from JWT claims
|
|
12
|
+
# @param config [Configuration] Configuration providing the role_map
|
|
13
|
+
# @return [String, Symbol] Mapped permission (or the role itself if unmapped)
|
|
14
|
+
def map(role, config)
|
|
15
|
+
return role unless config.role_map && !config.role_map.empty?
|
|
16
|
+
|
|
17
|
+
config.role_map[role.to_sym] || role
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verikloak
|
|
4
|
+
module Pundit
|
|
5
|
+
# Lightweight wrapper around Keycloak claims for Pundit policies.
|
|
6
|
+
class UserContext
|
|
7
|
+
attr_reader :claims, :resource_client
|
|
8
|
+
|
|
9
|
+
# Create a new user context from JWT claims.
|
|
10
|
+
#
|
|
11
|
+
# @param claims [Hash] JWT claims issued by Keycloak
|
|
12
|
+
# @param resource_client [String] default resource client name for resource roles
|
|
13
|
+
def initialize(claims, resource_client: Verikloak::Pundit.config.resource_client)
|
|
14
|
+
@claims = claims || {}
|
|
15
|
+
@resource_client = resource_client.to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Subject identifier from claims.
|
|
19
|
+
# @return [String, nil]
|
|
20
|
+
def sub
|
|
21
|
+
claims['sub']
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Email or preferred username from claims.
|
|
25
|
+
# @return [String, nil]
|
|
26
|
+
def email
|
|
27
|
+
claims['email'] || claims['preferred_username']
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Realm-level roles from claims based on configuration path.
|
|
31
|
+
# @return [Array<String>]
|
|
32
|
+
def realm_roles
|
|
33
|
+
path = resolve_path(Verikloak::Pundit.config.realm_roles_path)
|
|
34
|
+
Array(claims.dig(*path))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Resource-level roles for a given client from claims based on configuration path.
|
|
38
|
+
#
|
|
39
|
+
# @param client [String] resource client id (defaults to configured resource_client)
|
|
40
|
+
# @return [Array<String>]
|
|
41
|
+
def resource_roles(client = resource_client)
|
|
42
|
+
client = client.to_s
|
|
43
|
+
path = resolve_path(Verikloak::Pundit.config.resource_roles_path, client: client)
|
|
44
|
+
Array(claims.dig(*path))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check whether the user has a realm role.
|
|
48
|
+
#
|
|
49
|
+
# @param role [String, Symbol]
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def has_role?(role) # rubocop:disable Naming/PredicatePrefix
|
|
52
|
+
r = role.to_s
|
|
53
|
+
realm_roles.include?(r)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Alias to has_role? to align with group-based naming.
|
|
57
|
+
#
|
|
58
|
+
# @param group [String, Symbol]
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def in_group?(group)
|
|
61
|
+
has_role?(group)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check whether the user has a role for a specific resource client.
|
|
65
|
+
#
|
|
66
|
+
# @param client [String, Symbol]
|
|
67
|
+
# @param role [String, Symbol]
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def resource_role?(client, role)
|
|
70
|
+
client = client.to_s
|
|
71
|
+
r = role.to_s
|
|
72
|
+
resource_roles(client).include?(r)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check whether the user has a mapped permission.
|
|
76
|
+
#
|
|
77
|
+
# Uses realm roles and resource roles depending on
|
|
78
|
+
# {Configuration#permission_role_scope}.
|
|
79
|
+
#
|
|
80
|
+
# @param perm [String, Symbol] permission to check
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def has_permission?(perm) # rubocop:disable Naming/PredicatePrefix
|
|
83
|
+
pr = perm.to_sym
|
|
84
|
+
roles = realm_roles + resource_roles_scope
|
|
85
|
+
mapped = roles.map { |r| RoleMapper.map(r, Verikloak::Pundit.config) }
|
|
86
|
+
mapped.map(&:to_sym).include?(pr)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Build a user context from Rack env using configured claims key.
|
|
90
|
+
#
|
|
91
|
+
# @param env [Hash] Rack environment
|
|
92
|
+
# @return [UserContext]
|
|
93
|
+
def self.from_env(env)
|
|
94
|
+
claims = env[Verikloak::Pundit.config.env_claims_key]
|
|
95
|
+
new(claims)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Resolve a configured path into concrete dig segments.
|
|
101
|
+
#
|
|
102
|
+
# @param path_config [Array<String, Proc>]
|
|
103
|
+
# @param client [String, nil]
|
|
104
|
+
# @return [Array<String>]
|
|
105
|
+
def resolve_path(path_config, client: nil)
|
|
106
|
+
Array(path_config).map do |seg|
|
|
107
|
+
case seg
|
|
108
|
+
when Proc
|
|
109
|
+
# Support lambdas that accept (config) or (config, client)
|
|
110
|
+
if seg.arity >= 2
|
|
111
|
+
seg.call(Verikloak::Pundit.config, client).to_s
|
|
112
|
+
else
|
|
113
|
+
seg.call(Verikloak::Pundit.config).to_s
|
|
114
|
+
end
|
|
115
|
+
else
|
|
116
|
+
seg.to_s
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Resolve resource roles based on configured permission scope.
|
|
122
|
+
# @return [Array<String>]
|
|
123
|
+
def resource_roles_scope
|
|
124
|
+
case Verikloak::Pundit.config.permission_role_scope&.to_sym
|
|
125
|
+
when :all_resources
|
|
126
|
+
resource_roles_all_clients
|
|
127
|
+
else
|
|
128
|
+
resource_roles
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Collect resource roles from all clients under resource_access.
|
|
133
|
+
# @return [Array<String>]
|
|
134
|
+
def resource_roles_all_clients
|
|
135
|
+
access = claims['resource_access']
|
|
136
|
+
return [] unless access.is_a?(Hash)
|
|
137
|
+
|
|
138
|
+
# Bypass configured path lambda (which targets the default client)
|
|
139
|
+
# and gather roles from all clients explicitly.
|
|
140
|
+
access.values.flat_map { |entry| Array(entry['roles']) }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Verikloak::Pundit provides Pundit integration over Keycloak claims.
|
|
4
|
+
require_relative 'pundit/version'
|
|
5
|
+
require_relative 'pundit/configuration'
|
|
6
|
+
require_relative 'pundit/role_mapper'
|
|
7
|
+
require_relative 'pundit/user_context'
|
|
8
|
+
require_relative 'pundit/helpers'
|
|
9
|
+
require_relative 'pundit/controller'
|
|
10
|
+
require_relative 'pundit/policy'
|
|
11
|
+
require_relative 'pundit/railtie' if defined?(Rails::Railtie)
|
|
12
|
+
|
|
13
|
+
module Verikloak
|
|
14
|
+
# Pundit integration namespace
|
|
15
|
+
module Pundit
|
|
16
|
+
class << self
|
|
17
|
+
# Configure the library at runtime.
|
|
18
|
+
#
|
|
19
|
+
# @yield [Configuration] Yields the configuration instance for mutation.
|
|
20
|
+
# @return [Configuration] the current configuration after applying changes
|
|
21
|
+
def configure
|
|
22
|
+
@config ||= Configuration.new
|
|
23
|
+
yield @config if block_given?
|
|
24
|
+
@config
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Access the current configuration without mutating it.
|
|
28
|
+
#
|
|
29
|
+
# @return [Configuration]
|
|
30
|
+
def config
|
|
31
|
+
@config ||= Configuration.new
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Minimal shim to load the namespaced entrypoint.
|
|
4
|
+
#
|
|
5
|
+
# This file preserves compatibility with Bundler's default require
|
|
6
|
+
# (`require 'verikloak-pundit'`) by delegating to the real entrypoint
|
|
7
|
+
# under the namespaced path (`verikloak/pundit`).
|
|
8
|
+
require 'verikloak/pundit'
|
metadata
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: verikloak-pundit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- taiyaky
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: pundit
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rack
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.2'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '4.0'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '2.2'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '4.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: verikloak
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 0.1.2
|
|
53
|
+
- - "<"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0.2'
|
|
56
|
+
type: :runtime
|
|
57
|
+
prerelease: false
|
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: 0.1.2
|
|
63
|
+
- - "<"
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0.2'
|
|
66
|
+
description: Maps Keycloak JWT roles to a Pundit-friendly UserContext with helpers
|
|
67
|
+
and a Rails generator.
|
|
68
|
+
executables: []
|
|
69
|
+
extensions: []
|
|
70
|
+
extra_rdoc_files: []
|
|
71
|
+
files:
|
|
72
|
+
- CHANGELOG.md
|
|
73
|
+
- LICENSE
|
|
74
|
+
- README.md
|
|
75
|
+
- lib/generators/verikloak/pundit/install/install_generator.rb
|
|
76
|
+
- lib/generators/verikloak/pundit/install/templates/application_policy.rb
|
|
77
|
+
- lib/generators/verikloak/pundit/install/templates/initializer.rb
|
|
78
|
+
- lib/verikloak-pundit.rb
|
|
79
|
+
- lib/verikloak/pundit.rb
|
|
80
|
+
- lib/verikloak/pundit/configuration.rb
|
|
81
|
+
- lib/verikloak/pundit/controller.rb
|
|
82
|
+
- lib/verikloak/pundit/helpers.rb
|
|
83
|
+
- lib/verikloak/pundit/policy.rb
|
|
84
|
+
- lib/verikloak/pundit/railtie.rb
|
|
85
|
+
- lib/verikloak/pundit/role_mapper.rb
|
|
86
|
+
- lib/verikloak/pundit/user_context.rb
|
|
87
|
+
- lib/verikloak/pundit/version.rb
|
|
88
|
+
homepage: https://github.com/taiyaky/verikloak-pundit
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata:
|
|
92
|
+
source_code_uri: https://github.com/taiyaky/verikloak-pundit
|
|
93
|
+
changelog_uri: https://github.com/taiyaky/verikloak-pundit/blob/main/CHANGELOG.md
|
|
94
|
+
bug_tracker_uri: https://github.com/taiyaky/verikloak-pundit/issues
|
|
95
|
+
documentation_uri: https://rubydoc.info/gems/verikloak-pundit/0.1.0
|
|
96
|
+
rubygems_mfa_required: 'true'
|
|
97
|
+
rdoc_options: []
|
|
98
|
+
require_paths:
|
|
99
|
+
- lib
|
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '3.1'
|
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
requirements: []
|
|
111
|
+
rubygems_version: 3.6.9
|
|
112
|
+
specification_version: 4
|
|
113
|
+
summary: Pundit integration for Keycloak roles via Verikloak
|
|
114
|
+
test_files: []
|