keycloak-api-rails 1.1.1 → 2.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +51 -21
  5. data/keycloak-api-rails.gemspec +16 -6
  6. data/lib/keycloak-api-rails/authentication.rb +3 -11
  7. data/lib/keycloak-api-rails/configuration.rb +84 -0
  8. data/lib/keycloak-api-rails/helper.rb +31 -4
  9. data/lib/keycloak-api-rails/http_client.rb +54 -19
  10. data/lib/keycloak-api-rails/middleware.rb +17 -20
  11. data/lib/keycloak-api-rails/public_key_cached_resolver.rb +26 -7
  12. data/lib/keycloak-api-rails/railtie.rb +16 -1
  13. data/lib/keycloak-api-rails/service.rb +77 -28
  14. data/lib/keycloak-api-rails/testing.rb +5 -3
  15. data/lib/keycloak-api-rails/token_error.rb +41 -23
  16. data/lib/keycloak-api-rails/version.rb +1 -1
  17. data/lib/keycloak-api-rails.rb +21 -6
  18. metadata +39 -23
  19. data/.github/workflows/ci.yml +0 -35
  20. data/.github/workflows/release.yml +0 -52
  21. data/.gitignore +0 -9
  22. data/.rspec +0 -2
  23. data/Dockerfile +0 -13
  24. data/Gemfile +0 -3
  25. data/Rakefile +0 -6
  26. data/spec/keycloak-api-rails/activesupport_free_spec.rb +0 -18
  27. data/spec/keycloak-api-rails/authentication_spec.rb +0 -70
  28. data/spec/keycloak-api-rails/configuration_spec.rb +0 -101
  29. data/spec/keycloak-api-rails/helper_spec.rb +0 -94
  30. data/spec/keycloak-api-rails/public_key_cached_resolver_spec.rb +0 -80
  31. data/spec/keycloak-api-rails/service_spec.rb +0 -281
  32. data/spec/keycloak-api-rails/testing_spec.rb +0 -102
  33. data/spec/spec_helper.rb +0 -14
  34. data/spec/support/activesupport_free_probe.rb +0 -131
  35. data/spec/support/public_key_cached_resolver_stub.rb +0 -11
  36. data/spec/support/public_key_resolver_stub.rb +0 -7
  37. data/spec/support/rails_helper.rb +0 -4
@@ -1,30 +1,49 @@
1
1
  module KeycloakApiRails
2
2
  class PublicKeyCachedResolver
3
+ FAILED_REFRESH_RETRY_DELAY_IN_SECONDS = 10
4
+
3
5
  attr_reader :cached_public_key_retrieved_at
4
6
 
5
- def initialize(http_client, realm_id, public_key_cache_ttl)
7
+ def initialize(http_client, realm_id, public_key_cache_ttl, logger = nil)
6
8
  @resolver = PublicKeyResolver.new(http_client, realm_id)
7
9
  @public_key_cache_ttl = public_key_cache_ttl
10
+ @logger = logger
8
11
  @cached_public_keys = nil
9
12
  @cached_public_key_retrieved_at = nil
13
+ @last_refresh_failure_at = nil
14
+ @mutex = Mutex.new
10
15
  end
11
16
 
12
17
  def self.from_configuration(http_client, configuration)
13
- PublicKeyCachedResolver.new(http_client, configuration.realm_id, configuration.public_key_cache_ttl)
18
+ new(http_client, configuration.realm_id, configuration.public_key_cache_ttl, configuration.logger)
14
19
  end
15
20
 
16
21
  def find_public_keys
17
- if public_keys_are_outdated?
18
- @cached_public_keys = @resolver.find_public_keys
19
- @cached_public_key_retrieved_at = Time.now
22
+ @mutex.synchronize do
23
+ refresh_public_keys if public_keys_are_outdated?
24
+ @cached_public_keys
20
25
  end
21
- @cached_public_keys
22
26
  end
23
27
 
24
28
  private
25
29
 
30
+ def refresh_public_keys
31
+ @cached_public_keys = @resolver.find_public_keys
32
+ @cached_public_key_retrieved_at = Time.now
33
+ @last_refresh_failure_at = nil
34
+ rescue StandardError => e
35
+ raise if @cached_public_keys.nil?
36
+
37
+ @last_refresh_failure_at = Time.now
38
+ @logger&.warn("KeycloakApiRails: could not refresh the public keys (#{e.class}: #{e.message}). Keeping the ones retrieved at #{@cached_public_key_retrieved_at}.")
39
+ end
40
+
26
41
  def public_keys_are_outdated?
27
- @cached_public_keys.nil? || @cached_public_key_retrieved_at.nil? || Time.now > (@cached_public_key_retrieved_at + @public_key_cache_ttl)
42
+ @cached_public_keys.nil? ||
43
+ @cached_public_key_retrieved_at.nil? ||
44
+ (Time.now > @cached_public_key_retrieved_at + @public_key_cache_ttl &&
45
+ (@last_refresh_failure_at.nil? ||
46
+ Time.now > @last_refresh_failure_at + FAILED_REFRESH_RETRY_DELAY_IN_SECONDS))
28
47
  end
29
48
  end
30
49
  end
@@ -5,5 +5,20 @@ module KeycloakApiRails
5
5
  initializer("keycloak.insert_middleware") do |app|
6
6
  app.config.middleware.use(KeycloakApiRails::Middleware)
7
7
  end
8
+
9
+ # Runs once every initializer has run, config/initializers/keycloak.rb included, so that a
10
+ # misconfiguration fails at boot instead of on the first request reaching the middleware.
11
+ config.after_initialize do
12
+ keycloak_configuration = KeycloakApiRails.config
13
+ keycloak_configuration.validate!
14
+
15
+ unless keycloak_configuration.server_configured?
16
+ keycloak_configuration.logger.warn(
17
+ "KeycloakApiRails: 'server_url' and 'realm_id' are not both configured. No token can be " \
18
+ "verified until they are, unless the public key resolver is replaced -- as " \
19
+ "\"keycloak-api-rails/testing\" does."
20
+ )
21
+ end
22
+ end
8
23
  end
9
- end
24
+ end
@@ -1,38 +1,43 @@
1
1
  module KeycloakApiRails
2
+ class MissingPublicKeysError < StandardError; end
3
+
2
4
  class Service
3
-
5
+
4
6
  def initialize(key_resolver)
7
+ configuration = KeycloakApiRails.config
5
8
  @key_resolver = key_resolver
6
- @skip_paths = KeycloakApiRails.config.skip_paths
7
- @opt_in = KeycloakApiRails.config.opt_in
8
- @logger = KeycloakApiRails.config.logger
9
- @token_expiration_tolerance_in_seconds = KeycloakApiRails.config.token_expiration_tolerance_in_seconds
9
+ @skip_paths = normalize_skip_paths(configuration.skip_paths)
10
+ @opt_in = configuration.opt_in
11
+ @token_expiration_tolerance_in_seconds = configuration.token_expiration_tolerance_in_seconds
12
+ @expected_audiences = Array(configuration.expected_audience).map(&:to_s)
13
+ @expected_token_type = configuration.expected_token_type
14
+ @verify_not_before = configuration.verify_not_before
15
+ @allow_token_in_query_string = configuration.allow_token_in_query_string
10
16
  end
11
17
 
12
18
  def decode_and_verify(token)
13
- unless token.nil? || token&.empty?
14
- public_key = @key_resolver.find_public_keys
15
- decoded_token = JSON::JWT.decode(token, public_key)
16
-
17
- unless expired?(decoded_token)
18
- decoded_token.verify!(public_key)
19
- decoded_token
20
- else
21
- raise TokenError.expired(token)
22
- end
23
- else
24
- raise TokenError.no_token(token)
19
+ raise TokenError.no_token(token) if token.nil? || token.empty?
20
+
21
+ public_keys = @key_resolver.find_public_keys
22
+
23
+ if public_keys.nil?
24
+ raise MissingPublicKeysError, "No Keycloak public key is available to verify the token"
25
25
  end
26
- rescue JSON::JWT::VerificationFailed => e
27
- raise TokenError.verification_failed(token, e)
28
- rescue JSON::JWK::Set::KidNotFound => e
29
- raise TokenError.verification_failed(token, e)
30
- rescue JSON::JWT::InvalidFormat
31
- raise TokenError.invalid_format(token, e)
26
+
27
+ decoded_token = decode(token, public_keys)
28
+ verify_claims!(token, decoded_token)
29
+ decoded_token
32
30
  end
33
31
 
34
32
  def read_token(uri, headers)
35
- Helper.read_token_from_query_string(uri) || Helper.read_token_from_headers(headers)
33
+ header_token = Helper.read_token_from_headers(headers)
34
+ if !header_token.empty?
35
+ header_token
36
+ elsif @allow_token_in_query_string
37
+ Helper.read_token_from_query_string(uri).to_s
38
+ else
39
+ ""
40
+ end
36
41
  end
37
42
 
38
43
  def need_middleware_authentication?(method, path, headers)
@@ -41,14 +46,39 @@ module KeycloakApiRails
41
46
 
42
47
  private
43
48
 
49
+ def decode(token, public_keys)
50
+ decoded_token = JSON::JWT.decode(token, public_keys)
51
+ decoded_token.verify!(public_keys)
52
+ decoded_token
53
+ rescue JSON::JWT::VerificationFailed, JSON::JWK::Set::KidNotFound => e
54
+ raise TokenError.verification_failed(token, e)
55
+ rescue JSON::JWT::InvalidFormat => e
56
+ raise TokenError.invalid_format(token, e)
57
+ rescue StandardError => e
58
+ raise TokenError.unknown(token, e)
59
+ end
60
+
61
+ def verify_claims!(token, decoded_token)
62
+ raise TokenError.missing_claim(token, "exp") unless decoded_token.key?("exp")
63
+ raise TokenError.expired(token) if expired?(decoded_token)
64
+ raise TokenError.not_yet_valid(token) if not_yet_valid?(decoded_token)
65
+ raise TokenError.invalid_audience(token) unless audience_valid?(decoded_token)
66
+ raise TokenError.invalid_token_type(token) unless token_type_valid?(decoded_token)
67
+ end
68
+
69
+ def normalize_skip_paths(skip_paths)
70
+ (skip_paths || {}).each_with_object({}) do |(method, paths), normalized|
71
+ normalized[method.to_s.downcase.to_sym] = Array(paths)
72
+ end
73
+ end
74
+
44
75
  def should_skip?(method, path)
45
- method_symbol = method&.downcase&.to_sym
46
- skip_paths = @skip_paths[method_symbol]
47
- !skip_paths.nil? && !skip_paths.empty? && !skip_paths.find_index { |skip_path| skip_path.match(path) }.nil?
76
+ skip_paths = @skip_paths[method&.to_s&.downcase&.to_sym]
77
+ !skip_paths.nil? && skip_paths.any? { |skip_path| skip_path.match(path) }
48
78
  end
49
79
 
50
80
  def is_preflight?(method, headers)
51
- method_symbol = method&.downcase&.to_sym
81
+ method_symbol = method&.to_s&.downcase&.to_sym
52
82
  method_symbol == :options && !headers["HTTP_ACCESS_CONTROL_REQUEST_METHOD"].nil?
53
83
  end
54
84
 
@@ -56,5 +86,24 @@ module KeycloakApiRails
56
86
  token_expiration = Time.at(token["exp"])
57
87
  token_expiration < Time.now + @token_expiration_tolerance_in_seconds
58
88
  end
89
+
90
+ def not_yet_valid?(token)
91
+ return false unless @verify_not_before
92
+
93
+ not_before = token["nbf"]
94
+ !not_before.nil? && Time.at(not_before) > Time.now
95
+ end
96
+
97
+ def audience_valid?(token)
98
+ return true if @expected_audiences.empty?
99
+
100
+ Array(token["aud"]).any? { |audience| @expected_audiences.include?(audience.to_s) }
101
+ end
102
+
103
+ def token_type_valid?(token)
104
+ return true if @expected_token_type.nil?
105
+
106
+ token["typ"].to_s.casecmp?(@expected_token_type)
107
+ end
59
108
  end
60
109
  end
@@ -38,19 +38,21 @@ module KeycloakApiRails
38
38
  end
39
39
  end
40
40
 
41
+ MONITOR = Monitor.new
42
+
41
43
  class << self
42
44
  # The RSA key pair used to sign the tokens forged by this module. It is generated once per
43
45
  # process: generating a key is by far the slowest operation of a test suite that uses tokens.
44
46
  def private_key
45
- @private_key ||= OpenSSL::PKey::RSA.generate(KEY_SIZE)
47
+ MONITOR.synchronize { @private_key ||= OpenSSL::PKey::RSA.generate(KEY_SIZE) }
46
48
  end
47
49
 
48
50
  def signing_key
49
- @signing_key ||= JSON::JWK.new(private_key, kid: KEY_ID)
51
+ MONITOR.synchronize { @signing_key ||= JSON::JWK.new(private_key, kid: KEY_ID) }
50
52
  end
51
53
 
52
54
  def public_keys
53
- @public_keys ||= JSON::JWK::Set.new(JSON::JWK.new(private_key.public_key, kid: KEY_ID))
55
+ MONITOR.synchronize { @public_keys ||= JSON::JWK::Set.new(JSON::JWK.new(private_key.public_key, kid: KEY_ID)) }
54
56
  end
55
57
 
56
58
  # Makes the library validate the tokens forged by this module. Assigning
@@ -1,30 +1,48 @@
1
- class TokenError < StandardError
2
- attr_reader :token, :reason, :original_error
3
-
4
- def initialize(token, reason, message, original_error)
5
- super(message)
6
- @token = token
7
- @reason = reason
8
- @original_error = original_error
9
- end
1
+ module KeycloakApiRails
2
+ class TokenError < StandardError
3
+ attr_reader :token, :reason, :original_error
10
4
 
11
- def self.verification_failed(token, original_error)
12
- TokenError.new(token, :verification_failed, "Failed to verify JWT token", original_error)
13
- end
5
+ def initialize(token, reason, message, original_error = nil)
6
+ super(message)
7
+ @token = token
8
+ @reason = reason
9
+ @original_error = original_error
10
+ end
14
11
 
15
- def self.invalid_format(token, original_error)
16
- TokenError.new(token, :invalid_format, "Wrong JWT Format", original_error)
17
- end
12
+ def self.verification_failed(token, original_error)
13
+ new(token, :verification_failed, "Failed to verify JWT token", original_error)
14
+ end
18
15
 
19
- def self.no_token(token)
20
- TokenError.new(token, :no_token, "No JWT token provided", nil)
21
- end
16
+ def self.invalid_format(token, original_error)
17
+ new(token, :invalid_format, "Wrong JWT Format", original_error)
18
+ end
22
19
 
23
- def self.expired(token)
24
- TokenError.new(token, :expired, "JWT token is expired", nil)
25
- end
20
+ def self.no_token(token)
21
+ new(token, :no_token, "No JWT token provided")
22
+ end
23
+
24
+ def self.expired(token)
25
+ new(token, :expired, "JWT token is expired")
26
+ end
27
+
28
+ def self.not_yet_valid(token)
29
+ new(token, :not_yet_valid, "JWT token is not valid yet")
30
+ end
31
+
32
+ def self.invalid_audience(token)
33
+ new(token, :invalid_audience, "JWT token has been issued for another audience")
34
+ end
35
+
36
+ def self.invalid_token_type(token)
37
+ new(token, :invalid_token_type, "JWT token is not of the expected type")
38
+ end
39
+
40
+ def self.missing_claim(token, claim)
41
+ new(token, :missing_claim, "JWT token does not carry the mandatory claim '#{claim}'")
42
+ end
26
43
 
27
- def self.unknown(token)
28
- TokenError.new
44
+ def self.unknown(token, original_error)
45
+ new(token, :unknown, "Failed to read JWT token", original_error)
46
+ end
29
47
  end
30
48
  end
@@ -1,3 +1,3 @@
1
1
  module KeycloakApiRails
2
- VERSION = "1.1.1"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -2,6 +2,7 @@ require "logger"
2
2
  require "json/jwt"
3
3
  require "uri"
4
4
  require "date"
5
+ require "monitor"
5
6
  require "net/http"
6
7
 
7
8
  require_relative "keycloak-api-rails/authentication"
@@ -17,8 +18,13 @@ require_relative "keycloak-api-rails/railtie" if defined?(Rails)
17
18
 
18
19
  module KeycloakApiRails
19
20
 
21
+ # These objects are memoized lazily, on the first request each process serves -- which several
22
+ # threads of a threaded server reach at the same time. A Monitor rather than a Mutex: the
23
+ # memoizations nest, 'service' needing 'public_key_resolver', which needs 'http_client'.
24
+ MONITOR = Monitor.new
25
+
20
26
  def self.configure
21
- yield @configuration ||= KeycloakApiRails::Configuration.new
27
+ MONITOR.synchronize { yield @configuration ||= KeycloakApiRails::Configuration.new }
22
28
  end
23
29
 
24
30
  def self.config
@@ -26,23 +32,25 @@ module KeycloakApiRails
26
32
  end
27
33
 
28
34
  def self.http_client
29
- @http_client ||= KeycloakApiRails::HTTPClient.new(config, logger)
35
+ MONITOR.synchronize { @http_client ||= KeycloakApiRails::HTTPClient.new(config, logger) }
30
36
  end
31
37
 
32
38
  def self.public_key_resolver
33
- @public_key_resolver ||= PublicKeyCachedResolver.from_configuration(http_client, config)
39
+ MONITOR.synchronize { @public_key_resolver ||= PublicKeyCachedResolver.from_configuration(http_client, config) }
34
40
  end
35
41
 
36
42
  # Mainly used by "keycloak-api-rails/testing" to validate tokens without a Keycloak server.
37
43
  # Assigning nil restores the regular resolver. The memoized service is discarded, since it holds
38
44
  # a reference to the resolver that is being replaced.
39
45
  def self.public_key_resolver=(resolver)
40
- @public_key_resolver = resolver
41
- @service = nil
46
+ MONITOR.synchronize do
47
+ @public_key_resolver = resolver
48
+ @service = nil
49
+ end
42
50
  end
43
51
 
44
52
  def self.service
45
- @service ||= KeycloakApiRails::Service.new(public_key_resolver)
53
+ MONITOR.synchronize { @service ||= KeycloakApiRails::Service.new(public_key_resolver) }
46
54
  end
47
55
 
48
56
  def self.logger
@@ -59,6 +67,13 @@ module KeycloakApiRails
59
67
  config.token_expiration_tolerance_in_seconds = 10
60
68
  config.public_key_cache_ttl = 86400
61
69
  config.custom_attributes = []
70
+ config.ca_certificate_file = nil
71
+ config.expected_audience = nil
72
+ config.expected_token_type = nil
73
+ config.verify_not_before = false
74
+ config.allow_token_in_query_string = false
75
+ config.http_open_timeout = 5
76
+ config.http_read_timeout = 5
62
77
  end
63
78
  end
64
79
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycloak-api-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorent Lempereur
@@ -10,7 +10,7 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: rails
13
+ name: railties
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
@@ -65,6 +65,34 @@ dependencies:
65
65
  - - '='
66
66
  - !ruby/object:Gem::Version
67
67
  version: 0.9.11
68
+ - !ruby/object:Gem::Dependency
69
+ name: rails
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '4.2'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '4.2'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rack
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
68
96
  - !ruby/object:Gem::Dependency
69
97
  name: rake
70
98
  requirement: !ruby/object:Gem::Requirement
@@ -93,23 +121,19 @@ dependencies:
93
121
  - - ">="
94
122
  - !ruby/object:Gem::Version
95
123
  version: 11.1.3
96
- description: Rails middleware that validates Authorization token emitted by Keycloak
124
+ description: Rack middleware that validates the Keycloak JWT access token carried
125
+ by every request of a Ruby on Rails API. It verifies the token signature against
126
+ the realm public keys, exposes the authenticated user, its roles and the custom
127
+ attributes of the token to the controllers, and lets some routes opt out of authentication.
97
128
  email:
98
129
  - lorent.lempereur.dev@gmail.com
99
130
  executables: []
100
131
  extensions: []
101
132
  extra_rdoc_files: []
102
133
  files:
103
- - ".github/workflows/ci.yml"
104
- - ".github/workflows/release.yml"
105
- - ".gitignore"
106
- - ".rspec"
107
134
  - CHANGELOG.md
108
- - Dockerfile
109
- - Gemfile
110
135
  - MIT-LICENSE
111
136
  - README.md
112
- - Rakefile
113
137
  - keycloak-api-rails.gemspec
114
138
  - lib/keycloak-api-rails.rb
115
139
  - lib/keycloak-api-rails/authentication.rb
@@ -124,22 +148,14 @@ files:
124
148
  - lib/keycloak-api-rails/testing.rb
125
149
  - lib/keycloak-api-rails/token_error.rb
126
150
  - lib/keycloak-api-rails/version.rb
127
- - spec/keycloak-api-rails/activesupport_free_spec.rb
128
- - spec/keycloak-api-rails/authentication_spec.rb
129
- - spec/keycloak-api-rails/configuration_spec.rb
130
- - spec/keycloak-api-rails/helper_spec.rb
131
- - spec/keycloak-api-rails/public_key_cached_resolver_spec.rb
132
- - spec/keycloak-api-rails/service_spec.rb
133
- - spec/keycloak-api-rails/testing_spec.rb
134
- - spec/spec_helper.rb
135
- - spec/support/activesupport_free_probe.rb
136
- - spec/support/public_key_cached_resolver_stub.rb
137
- - spec/support/public_key_resolver_stub.rb
138
- - spec/support/rails_helper.rb
139
151
  homepage: https://github.com/looorent/keycloak-api-rails
140
152
  licenses:
141
153
  - MIT
142
- metadata: {}
154
+ metadata:
155
+ source_code_uri: https://github.com/looorent/keycloak-api-rails
156
+ changelog_uri: https://github.com/looorent/keycloak-api-rails/blob/main/CHANGELOG.md
157
+ bug_tracker_uri: https://github.com/looorent/keycloak-api-rails/issues
158
+ rubygems_mfa_required: 'true'
143
159
  rdoc_options: []
144
160
  require_paths:
145
161
  - lib
@@ -1,35 +0,0 @@
1
- name: Ruby
2
-
3
- on:
4
- push:
5
- branches: [ "main" ]
6
- pull_request:
7
- branches: [ "main" ]
8
-
9
- permissions:
10
- contents: read
11
-
12
- jobs:
13
- test:
14
- name: Ruby ${{ matrix.ruby-version }}
15
- # Pinned rather than 'ubuntu-latest': ruby/setup-ruby has no prebuilt Rubies for
16
- # ubuntu-26.04 yet, so the oldest versions of the matrix would break once the
17
- # 'latest' alias moves on.
18
- runs-on: ubuntu-24.04
19
-
20
- strategy:
21
- fail-fast: false
22
- matrix:
23
- # Each version resolves the newest dependencies it supports: Rails 7.1 on Ruby 2.7
24
- # and 3.0, Rails 7.2 on Ruby 3.1, Rails 8.1 on Ruby 3.2 and above.
25
- ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
26
-
27
- steps:
28
- - uses: actions/checkout@v4
29
- - name: Set up Ruby
30
- uses: ruby/setup-ruby@v1
31
- with:
32
- ruby-version: ${{ matrix.ruby-version }}
33
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
34
- - name: Run tests
35
- run: bundle exec rspec
@@ -1,52 +0,0 @@
1
- name: Release
2
-
3
- # Publishing is triggered by pushing a version tag, e.g.:
4
- # git tag -a v1.2.0 -m "Version 1.2.0" && git push origin v1.2.0
5
- #
6
- # No RubyGems API key is stored in this repository: the gem is pushed through
7
- # RubyGems' Trusted Publishing, which exchanges a short-lived GitHub OIDC token
8
- # for a scoped RubyGems credential. This requires a trusted publisher declared on
9
- # https://rubygems.org/gems/keycloak-api-rails pointing to this repository, this
10
- # workflow file name and the 'release' environment.
11
- on:
12
- push:
13
- tags: [ "v*" ]
14
-
15
- permissions:
16
- contents: read
17
-
18
- jobs:
19
- release:
20
- name: Push gem to RubyGems.org
21
- # Pinned rather than 'ubuntu-latest', for the same reason as in ci.yml.
22
- runs-on: ubuntu-24.04
23
-
24
- environment: release
25
-
26
- permissions:
27
- contents: write # 'rake release' pushes the version tag when it does not exist yet
28
- id-token: write # mandatory to request the OIDC token used by trusted publishing
29
-
30
- steps:
31
- - uses: actions/checkout@v4
32
-
33
- - name: Set up Ruby
34
- uses: ruby/setup-ruby@v1
35
- with:
36
- ruby-version: '3.4'
37
- bundler-cache: true
38
-
39
- - name: Check that the tag matches KeycloakApiRails::VERSION
40
- run: |
41
- tag="${GITHUB_REF_NAME#v}"
42
- version="$(ruby -Ilib -r keycloak-api-rails/version -e 'print KeycloakApiRails::VERSION')"
43
- if [ "$tag" != "$version" ]; then
44
- echo "::error::Tag '${GITHUB_REF_NAME}' does not match KeycloakApiRails::VERSION '${version}'"
45
- exit 1
46
- fi
47
-
48
- - name: Run tests
49
- run: bundle exec rspec
50
-
51
- # Builds the gem, pushes it to RubyGems.org and waits for it to be available.
52
- - uses: rubygems/release-gem@v1
data/.gitignore DELETED
@@ -1,9 +0,0 @@
1
- .bundle/
2
- log/*.log
3
- .byebug_history
4
- *.gem
5
- pkg/
6
- # This gem supports several Ruby versions, each of them resolving a different set of
7
- # dependencies (Ruby 2.7 caps Rails to 7.1, Ruby 3.1 to 7.2, Ruby 3.2+ resolves Rails 8.1).
8
- # A single committed lockfile cannot satisfy all of them.
9
- Gemfile.lock
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --color
2
- --require spec_helper
data/Dockerfile DELETED
@@ -1,13 +0,0 @@
1
- FROM ruby:3.4.10-slim-bookworm
2
-
3
- RUN apt-get update -qq && apt-get install -y build-essential git ruby-dev && apt-get clean && \
4
- mkdir -p /usr/src/app/lib/keycloak-api-rails
5
-
6
- WORKDIR /usr/src/app
7
-
8
- COPY Gemfile /usr/src/app/
9
- COPY keycloak-api-rails.gemspec /usr/src/app/
10
- COPY lib/keycloak-api-rails/version.rb /usr/src/app/lib/keycloak-api-rails/
11
- RUN bundle install
12
- COPY . /usr/src/app
13
- RUN bundle install
data/Gemfile DELETED
@@ -1,3 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gemspec
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task default: :spec
@@ -1,18 +0,0 @@
1
- require "rbconfig"
2
-
3
- RSpec.describe "The library without ActiveSupport" do
4
-
5
- let(:probe) { File.expand_path("../../support/activesupport_free_probe.rb", __FILE__) }
6
-
7
- # RUBYOPT is cleared so that nothing set up by bundler is preloaded in the probe process.
8
- def run_probe
9
- output = IO.popen({ "RUBYOPT" => nil }, [RbConfig.ruby, probe], err: [:child, :out], &:read)
10
- [$?.exitstatus, output]
11
- end
12
-
13
- it "loads and behaves the same way when ActiveSupport is not available" do
14
- status, output = run_probe
15
-
16
- expect(status).to eq(0), "The library relies on ActiveSupport:\n#{output}"
17
- end
18
- end