keycloak-admin 2.0.0 → 2.0.1
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 +4 -4
- data/.github/workflows/release.yml +112 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +3 -1
- data/README.md +17 -1
- data/Rakefile +6 -0
- data/keycloak-admin.gemspec +1 -0
- data/lib/keycloak-admin/client/client.rb +9 -2
- data/lib/keycloak-admin/client/configurable_token_client.rb +2 -1
- data/lib/keycloak-admin/client/group_client.rb +2 -1
- data/lib/keycloak-admin/client/resource.rb +98 -0
- data/lib/keycloak-admin/client/response.rb +36 -0
- data/lib/keycloak-admin/client/role_mapper_client.rb +2 -1
- data/lib/keycloak-admin/client/user_client.rb +15 -8
- data/lib/keycloak-admin/configuration.rb +22 -0
- data/lib/keycloak-admin/version.rb +1 -1
- data/spec/client/client_client_spec.rb +2 -2
- data/spec/client/client_role_mappings_client_spec.rb +3 -2
- data/spec/client/client_spec.rb +53 -0
- data/spec/client/group_client_spec.rb +6 -6
- data/spec/client/identity_provider_client_spec.rb +1 -1
- data/spec/client/organization_client_spec.rb +3 -3
- data/spec/client/realm_client_spec.rb +4 -4
- data/spec/client/resource_spec.rb +223 -0
- data/spec/client/response_spec.rb +68 -0
- data/spec/client/role_client_spec.rb +2 -2
- data/spec/client/role_mapper_client_spec.rb +1 -1
- data/spec/client/token_client_spec.rb +1 -1
- data/spec/client/user_client_spec.rb +5 -5
- data/spec/configuration_spec.rb +37 -0
- data/spec/spec_helper.rb +8 -1
- metadata +22 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a9c09a6765355416960bbc5c4564ad5dd5f526f65093b43d46ac10fc39d0ae7
|
|
4
|
+
data.tar.gz: b2dd1762448fad5a096b94adc3d18b30640235fe02efa1e77269797bfa3f9464
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3274b863ef8b7d295b82d5dd87a6d3340ac96ac08c1f38c822ad531b204f4fd5755704a905ccc308821e199d17cc8c73959c3dc1d39a5a13079ce133f1ed4854
|
|
7
|
+
data.tar.gz: 7c8d7b5f54eb523f27eaaa83893d8b5190d4647ed6d7424a6ac1b55168ffa02fb426171b424330db86d976e00c84e1644d3bab3cf4fd398d1eb9cf4deff2e679
|
|
@@ -0,0 +1,112 @@
|
|
|
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-admin 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
|
+
# The test suite includes an integration spec that talks to a real Keycloak
|
|
32
|
+
# (spec/integration/client_authorization_spec.rb, gated on ENV["GITHUB_ACTIONS"],
|
|
33
|
+
# which GitHub Actions sets on every run regardless of workflow). Keycloak is
|
|
34
|
+
# provisioned here the same way ci.yml does it, so 'bundle exec rspec' below is a
|
|
35
|
+
# real, complete gate rather than skipping that spec for lack of a server.
|
|
36
|
+
- name: Start Keycloak
|
|
37
|
+
run: |
|
|
38
|
+
docker run -d --name keycloak -p 8080:8080 \
|
|
39
|
+
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
|
|
40
|
+
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
|
|
41
|
+
-e KC_HOSTNAME=http://localhost:8080 \
|
|
42
|
+
-e KC_HOSTNAME_ADMIN=http://localhost:8080 \
|
|
43
|
+
-e KC_HTTP_ENABLED=true \
|
|
44
|
+
-e KC_DB=dev-file \
|
|
45
|
+
quay.io/keycloak/keycloak:26.7.0 \
|
|
46
|
+
start-dev
|
|
47
|
+
|
|
48
|
+
- name: Wait for Keycloak to be ready
|
|
49
|
+
run: |
|
|
50
|
+
for i in $(seq 1 30); do
|
|
51
|
+
if curl --silent --fail http://localhost:8080/realms/master > /dev/null; then
|
|
52
|
+
echo "Keycloak is ready"
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
sleep 5
|
|
56
|
+
done
|
|
57
|
+
echo "Keycloak did not become ready in time"
|
|
58
|
+
docker logs keycloak
|
|
59
|
+
exit 1
|
|
60
|
+
|
|
61
|
+
- name: create realm
|
|
62
|
+
run: |
|
|
63
|
+
TOKEN=$(curl --silent --location --request POST "http://localhost:8080/realms/master/protocol/openid-connect/token" \
|
|
64
|
+
--header 'Content-Type: application/x-www-form-urlencoded' \
|
|
65
|
+
--data-urlencode 'grant_type=password' \
|
|
66
|
+
--data-urlencode 'username=admin' \
|
|
67
|
+
--data-urlencode 'password=admin' \
|
|
68
|
+
--data-urlencode 'client_id=admin-cli' | jq -r '.access_token')
|
|
69
|
+
|
|
70
|
+
curl --silent --show-error -L -X POST "http://localhost:8080/admin/realms" \
|
|
71
|
+
--header "Content-Type: application/json" \
|
|
72
|
+
--header "Authorization: Bearer ${TOKEN}" \
|
|
73
|
+
--data '{"realm":"dummy","enabled":true}'
|
|
74
|
+
|
|
75
|
+
curl --silent --show-error --request POST 'http://localhost:8080/admin/realms/dummy/clients' \
|
|
76
|
+
--header 'Authorization: Bearer '$TOKEN \
|
|
77
|
+
--header 'Content-Type: application/json' \
|
|
78
|
+
--data-raw '{
|
|
79
|
+
"clientId":"dummy-client",
|
|
80
|
+
"enabled":true,
|
|
81
|
+
"consentRequired": false,
|
|
82
|
+
"attributes":{},
|
|
83
|
+
"serviceAccountsEnabled": true,
|
|
84
|
+
"protocol":"openid-connect",
|
|
85
|
+
"publicClient":false,
|
|
86
|
+
"authorizationServicesEnabled": true,
|
|
87
|
+
"clientAuthenticatorType":"client-secret",
|
|
88
|
+
"redirectUris":["http://localhost:8180/demo"]
|
|
89
|
+
}'
|
|
90
|
+
|
|
91
|
+
- uses: actions/checkout@v4
|
|
92
|
+
|
|
93
|
+
- name: Set up Ruby
|
|
94
|
+
uses: ruby/setup-ruby@v1
|
|
95
|
+
with:
|
|
96
|
+
ruby-version: '3.4'
|
|
97
|
+
bundler-cache: true
|
|
98
|
+
|
|
99
|
+
- name: Check that the tag matches KeycloakAdmin::VERSION
|
|
100
|
+
run: |
|
|
101
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
102
|
+
version="$(ruby -Ilib -r keycloak-admin/version -e 'print KeycloakAdmin::VERSION')"
|
|
103
|
+
if [ "$tag" != "$version" ]; then
|
|
104
|
+
echo "::error::Tag '${GITHUB_REF_NAME}' does not match KeycloakAdmin::VERSION '${version}'"
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
- name: Run tests
|
|
109
|
+
run: bundle exec rspec
|
|
110
|
+
|
|
111
|
+
# Builds the gem, pushes it to RubyGems.org and waits for it to be available.
|
|
112
|
+
- uses: rubygems/release-gem@v1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.1] - 2026-07-31
|
|
9
|
+
|
|
10
|
+
* [Feature] Requests are now logged through `config.logger` (method, URL, and response status) via Faraday's `:logger` middleware.
|
|
11
|
+
* [Feature] The access token is now cached and reused (tracking `expires_in`) instead of being fetched before nearly every call. Previously, caching lived on the `Client` instance, but a fresh `Client` subclass is created for almost every call (e.g. `KeycloakAdmin.realm(x).users` builds a new `UserClient`), so the cache was rarely hit in practice; it now lives on the shared `Configuration`.
|
|
12
|
+
* Publish the gem to RubyGems from Github Actions when a `v*` tag is pushed, using RubyGems' Trusted Publishing
|
|
13
|
+
|
|
8
14
|
## [2.0.0] - 2026-07-31
|
|
9
15
|
|
|
10
16
|
* [Breaking] Replaced `rest-client` with `Faraday` as the underlying HTTP library. This should be transparent for callers of this gem's own API, but `config.rest_client_options` is renamed to `config.faraday_options` and its shape changes from rest-client's flat hash to Faraday's connection options (e.g. `{ timeout: 5 }` becomes `{ request: { timeout: 5 } }`, `{ verify_ssl: false }` becomes `{ ssl: { verify: false } }`). See the `Configuration` section of the README.
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
keycloak-admin (2.0.
|
|
4
|
+
keycloak-admin (2.0.1)
|
|
5
5
|
base64
|
|
6
6
|
faraday (~> 2.0)
|
|
7
7
|
http-cookie (~> 1.0, >= 1.0.3)
|
|
@@ -25,6 +25,7 @@ GEM
|
|
|
25
25
|
logger (1.7.0)
|
|
26
26
|
net-http (0.9.1)
|
|
27
27
|
uri (>= 0.11.1)
|
|
28
|
+
rake (13.4.2)
|
|
28
29
|
rspec (3.13.2)
|
|
29
30
|
rspec-core (~> 3.13.0)
|
|
30
31
|
rspec-expectations (~> 3.13.0)
|
|
@@ -46,6 +47,7 @@ PLATFORMS
|
|
|
46
47
|
DEPENDENCIES
|
|
47
48
|
byebug (= 12.0.0)
|
|
48
49
|
keycloak-admin!
|
|
50
|
+
rake (>= 13.0)
|
|
49
51
|
rspec (= 3.13.2)
|
|
50
52
|
|
|
51
53
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ This gem *does not* require Rails.
|
|
|
16
16
|
For example, using `bundle`, add this line to your Gemfile.
|
|
17
17
|
|
|
18
18
|
```ruby
|
|
19
|
-
gem "keycloak-admin", "2.0.
|
|
19
|
+
gem "keycloak-admin", "2.0.1"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Login
|
|
@@ -885,3 +885,19 @@ From the `keycloak-admin-api` directory:
|
|
|
885
885
|
$ docker build . -t keycloak-admin:test
|
|
886
886
|
$ docker run -v `pwd`:/usr/src/app/ keycloak-admin:test rspec spec
|
|
887
887
|
```
|
|
888
|
+
|
|
889
|
+
## How to release a new version
|
|
890
|
+
|
|
891
|
+
Releases are published to [RubyGems](https://rubygems.org/gems/keycloak-admin) by GitHub Actions
|
|
892
|
+
(`.github/workflows/release.yml`), through [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/):
|
|
893
|
+
no API key is stored in this repository, the workflow exchanges a short-lived GitHub OIDC token for a
|
|
894
|
+
scoped RubyGems credential.
|
|
895
|
+
|
|
896
|
+
1. Update `KeycloakAdmin::VERSION` in `lib/keycloak-admin/version.rb` and the `CHANGELOG.md`
|
|
897
|
+
2. Commit and push these changes to `main`
|
|
898
|
+
3. Tag the commit and push the tag:
|
|
899
|
+
|
|
900
|
+
```
|
|
901
|
+
$ git tag -a v2.0.1 -m "Version 2.0.1"
|
|
902
|
+
$ git push origin v2.0.1
|
|
903
|
+
```
|
data/Rakefile
ADDED
data/keycloak-admin.gemspec
CHANGED
|
@@ -9,8 +9,11 @@ module KeycloakAdmin
|
|
|
9
9
|
@configuration.server_url&.sub(/\/+\z/, "")
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# Cached on @configuration, not on this instance, since a new Client subclass is
|
|
13
|
+
# created for nearly every call (e.g. KeycloakAdmin.realm(x).users creates a fresh
|
|
14
|
+
# UserClient) - caching here alone would fetch a new token on almost every request.
|
|
12
15
|
def current_token
|
|
13
|
-
@
|
|
16
|
+
@configuration.cached_token || @configuration.cache_token(fetch_token)
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def headers
|
|
@@ -49,8 +52,12 @@ module KeycloakAdmin
|
|
|
49
52
|
|
|
50
53
|
private
|
|
51
54
|
|
|
55
|
+
def fetch_token
|
|
56
|
+
KeycloakAdmin.create_client(@configuration, @configuration.client_realm_name).token.get
|
|
57
|
+
end
|
|
58
|
+
|
|
52
59
|
def resource(url)
|
|
53
|
-
Resource.new(url, @configuration.faraday_options)
|
|
60
|
+
Resource.new(url, @configuration.faraday_options, @configuration.logger)
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
def http_error(response)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "uri"
|
|
3
|
+
|
|
4
|
+
module KeycloakAdmin
|
|
5
|
+
# Thin facade over Faraday that mirrors RestClient::Resource/RestClient::Request's call
|
|
6
|
+
# shape, so every client class can build/send requests without depending on Faraday's API
|
|
7
|
+
# directly.
|
|
8
|
+
class Resource
|
|
9
|
+
MIME_TYPES = { json: "application/json" }.freeze
|
|
10
|
+
|
|
11
|
+
def self.execute(options)
|
|
12
|
+
options = options.dup
|
|
13
|
+
method = options.delete(:method)
|
|
14
|
+
url = options.delete(:url)
|
|
15
|
+
payload = options.delete(:payload)
|
|
16
|
+
headers = options.delete(:headers) || {}
|
|
17
|
+
logger = options.delete(:logger)
|
|
18
|
+
new(url, options, logger).send(:request, method, payload, headers)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.put(url, payload, headers = {}, logger = nil)
|
|
22
|
+
new(url, {}, logger).put(payload, headers)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(url, options = {}, logger = nil)
|
|
26
|
+
@url = url
|
|
27
|
+
@options = options || {}
|
|
28
|
+
@logger = logger
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def get(headers = {})
|
|
32
|
+
request(:get, nil, headers)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(headers = {})
|
|
36
|
+
request(:delete, nil, headers)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def post(payload, headers = {})
|
|
40
|
+
request(:post, payload, headers)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def put(payload, headers = {})
|
|
44
|
+
request(:put, payload, headers)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def request(method, payload, headers)
|
|
50
|
+
headers = headers || {}
|
|
51
|
+
query = headers[:params]
|
|
52
|
+
send_headers = build_headers(headers.reject { |key, _| key == :params })
|
|
53
|
+
body = encode_payload(payload)
|
|
54
|
+
|
|
55
|
+
# RestClient::Payload::UrlEncoded always stamped this Content-Type for Hash payloads,
|
|
56
|
+
# regardless of what the caller's headers said; token retrieval relies on it and never
|
|
57
|
+
# sets content_type itself.
|
|
58
|
+
if payload.is_a?(Hash) && !send_headers.key?("Content-Type")
|
|
59
|
+
send_headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
faraday_response = connection.public_send(method) do |req|
|
|
63
|
+
req.params.update(query) if query
|
|
64
|
+
send_headers.each { |name, value| req.headers[name] = value }
|
|
65
|
+
req.body = body unless body.nil?
|
|
66
|
+
end
|
|
67
|
+
Response.new(faraday_response)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def connection
|
|
71
|
+
Faraday.new(url: @url, **@options) do |f|
|
|
72
|
+
f.response :raise_error
|
|
73
|
+
# Added after :raise_error so it logs the raw response (status/duration) before
|
|
74
|
+
# :raise_error can raise, including on failures. Faraday's logger middleware logs
|
|
75
|
+
# headers by default, which would print the bearer token on every call; headers: false
|
|
76
|
+
# keeps this to method/url/status only. Bodies are already off by Faraday's own default.
|
|
77
|
+
f.response :logger, @logger, headers: false if @logger
|
|
78
|
+
f.adapter Faraday.default_adapter
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def encode_payload(payload)
|
|
83
|
+
return nil if payload.nil?
|
|
84
|
+
payload.is_a?(Hash) ? URI.encode_www_form(payload) : payload.to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_headers(headers)
|
|
88
|
+
headers.each_with_object({}) do |(key, value), result|
|
|
89
|
+
name = key.is_a?(Symbol) ? humanize(key) : key.to_s
|
|
90
|
+
result[name] = value.is_a?(Symbol) ? MIME_TYPES.fetch(value, value.to_s) : value
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def humanize(key)
|
|
95
|
+
key.to_s.split("_").map { |word| word.capitalize }.join("-")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module KeycloakAdmin
|
|
2
|
+
# Wraps a Faraday::Response the way RestClient::Response used to: as the body string
|
|
3
|
+
# itself (RestClient::Response < String), so existing call sites that do JSON.parse(response)
|
|
4
|
+
# or response.to_i keep working, while .body/.headers/.status/.code/.reason_phrase stay
|
|
5
|
+
# available for call sites that need response metadata.
|
|
6
|
+
class Response < String
|
|
7
|
+
attr_reader :status, :reason_phrase, :headers
|
|
8
|
+
|
|
9
|
+
def initialize(faraday_response)
|
|
10
|
+
super(faraday_response.body.to_s)
|
|
11
|
+
@status = faraday_response.status
|
|
12
|
+
@reason_phrase = faraday_response.reason_phrase
|
|
13
|
+
@headers = symbolize_headers(faraday_response.headers)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
alias_method :code, :status
|
|
17
|
+
alias_method :body, :to_s
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def symbolize_headers(faraday_headers)
|
|
22
|
+
faraday_headers.each_with_object({}) do |(key, value), result|
|
|
23
|
+
symbol_key = key.downcase.tr("-", "_").to_sym
|
|
24
|
+
result[symbol_key] = symbol_key == :set_cookie ? split_set_cookie(value) : value
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Net::HTTP (used by Faraday's default adapter) joins repeated Set-Cookie header lines with
|
|
29
|
+
# ", " (see Net::HTTPHeader#each_header), losing the one-header-per-cookie structure RestClient
|
|
30
|
+
# exposed as an Array. Re-split on cookie boundaries rather than on every comma, since a comma
|
|
31
|
+
# can also appear inside a cookie's own "Expires=Wed, 09 Jun 2027 ..." attribute.
|
|
32
|
+
def split_set_cookie(value)
|
|
33
|
+
value.split(/,(?=\s*[^;\s]+=)/).map(&:strip)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -28,7 +28,8 @@ module KeycloakAdmin
|
|
|
28
28
|
method: :put,
|
|
29
29
|
url: users_url(user_id),
|
|
30
30
|
payload: create_payload(user_representation_body),
|
|
31
|
-
headers: headers
|
|
31
|
+
headers: headers,
|
|
32
|
+
logger: @configuration.logger
|
|
32
33
|
)
|
|
33
34
|
)
|
|
34
35
|
end
|
|
@@ -39,7 +40,8 @@ module KeycloakAdmin
|
|
|
39
40
|
method: :put,
|
|
40
41
|
url: "#{users_url(user_id)}/groups/#{group_id}",
|
|
41
42
|
payload: create_payload({}),
|
|
42
|
-
headers: headers
|
|
43
|
+
headers: headers,
|
|
44
|
+
logger: @configuration.logger
|
|
43
45
|
)
|
|
44
46
|
)
|
|
45
47
|
end
|
|
@@ -49,7 +51,8 @@ module KeycloakAdmin
|
|
|
49
51
|
@configuration.faraday_options.merge(
|
|
50
52
|
method: :delete,
|
|
51
53
|
url: "#{users_url(user_id)}/groups/#{group_id}",
|
|
52
|
-
headers: headers
|
|
54
|
+
headers: headers,
|
|
55
|
+
logger: @configuration.logger
|
|
53
56
|
)
|
|
54
57
|
)
|
|
55
58
|
end
|
|
@@ -116,7 +119,8 @@ module KeycloakAdmin
|
|
|
116
119
|
method: :put,
|
|
117
120
|
url: reset_password_url(user_id),
|
|
118
121
|
payload: { type: "password", value: new_password, temporary: false }.to_json,
|
|
119
|
-
headers: headers
|
|
122
|
+
headers: headers,
|
|
123
|
+
logger: @configuration.logger
|
|
120
124
|
)
|
|
121
125
|
)
|
|
122
126
|
end
|
|
@@ -140,7 +144,7 @@ module KeycloakAdmin
|
|
|
140
144
|
lifespan_param = lifespan.nil? ? "" : "&lifespan=#{lifespan.seconds}"
|
|
141
145
|
redirect_uri_param = redirect_uri.nil? ? "" : "&redirect_uri=#{redirect_uri}"
|
|
142
146
|
client_id_param = client_id.nil? ? "" : "client_id=#{client_id}"
|
|
143
|
-
Resource.put("#{execute_actions_email_url(user_id)}?#{client_id_param}#{redirect_uri_param}#{lifespan_param}", create_payload(actions), headers)
|
|
147
|
+
Resource.put("#{execute_actions_email_url(user_id)}?#{client_id_param}#{redirect_uri_param}#{lifespan_param}", create_payload(actions), headers, @configuration.logger)
|
|
144
148
|
end
|
|
145
149
|
user_id
|
|
146
150
|
end
|
|
@@ -153,7 +157,8 @@ module KeycloakAdmin
|
|
|
153
157
|
method: :post,
|
|
154
158
|
url: impersonation.impersonation_url,
|
|
155
159
|
payload: impersonation.body.to_json,
|
|
156
|
-
headers: impersonation.headers
|
|
160
|
+
headers: impersonation.headers,
|
|
161
|
+
logger: @configuration.logger
|
|
157
162
|
)
|
|
158
163
|
)
|
|
159
164
|
end
|
|
@@ -177,7 +182,8 @@ module KeycloakAdmin
|
|
|
177
182
|
@configuration.faraday_options.merge(
|
|
178
183
|
method: :post,
|
|
179
184
|
url: logout_url(user_id),
|
|
180
|
-
headers: headers
|
|
185
|
+
headers: headers,
|
|
186
|
+
logger: @configuration.logger
|
|
181
187
|
)
|
|
182
188
|
)
|
|
183
189
|
end
|
|
@@ -200,7 +206,8 @@ module KeycloakAdmin
|
|
|
200
206
|
method: :post,
|
|
201
207
|
url: federated_identity_url(user_id, idp_id),
|
|
202
208
|
payload: fed_id_rep.to_json,
|
|
203
|
-
headers: headers
|
|
209
|
+
headers: headers,
|
|
210
|
+
logger: @configuration.logger
|
|
204
211
|
)
|
|
205
212
|
)
|
|
206
213
|
end
|
|
@@ -4,6 +4,28 @@ module KeycloakAdmin
|
|
|
4
4
|
class Configuration
|
|
5
5
|
attr_accessor :server_url, :server_domain, :client_id, :client_secret, :client_realm_name, :use_service_account, :username, :password, :logger, :faraday_options
|
|
6
6
|
|
|
7
|
+
# Treat a cached token as expired this many seconds before its real expiry, so it
|
|
8
|
+
# cannot go stale between the check below and the request that relies on it.
|
|
9
|
+
TOKEN_EXPIRY_SAFETY_MARGIN_SECONDS = 10
|
|
10
|
+
|
|
11
|
+
# Returns the cached TokenRepresentation, or nil if there is none or it is (about to be) expired.
|
|
12
|
+
def cached_token
|
|
13
|
+
return nil if @token_expires_at.nil? || Time.now >= @token_expires_at
|
|
14
|
+
@cached_token
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cache_token(token_representation)
|
|
18
|
+
return token_representation unless token_representation.expires_in.is_a?(Numeric)
|
|
19
|
+
@cached_token = token_representation
|
|
20
|
+
@token_expires_at = Time.now + token_representation.expires_in - TOKEN_EXPIRY_SAFETY_MARGIN_SECONDS
|
|
21
|
+
token_representation
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def clear_cached_token!
|
|
25
|
+
@cached_token = nil
|
|
26
|
+
@token_expires_at = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
7
29
|
def body_for_token_retrieval
|
|
8
30
|
if use_service_account
|
|
9
31
|
body_for_service_account
|
|
@@ -86,7 +86,7 @@ RSpec.describe KeycloakAdmin::ClientClient do
|
|
|
86
86
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
87
87
|
|
|
88
88
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
89
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/clients", faraday_options).and_call_original
|
|
89
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/clients", faraday_options, anything).and_call_original
|
|
90
90
|
|
|
91
91
|
clients = @client_client.list
|
|
92
92
|
expect(clients.length).to eq 1
|
|
@@ -126,7 +126,7 @@ RSpec.describe KeycloakAdmin::ClientClient do
|
|
|
126
126
|
faraday_options = {timeout: 10}
|
|
127
127
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
128
128
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
129
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/clients/95b45037-3980-404c-ba12-784fa1baf2c2", faraday_options).and_call_original
|
|
129
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/clients/95b45037-3980-404c-ba12-784fa1baf2c2", faraday_options, anything).and_call_original
|
|
130
130
|
@client_client.delete("95b45037-3980-404c-ba12-784fa1baf2c2")
|
|
131
131
|
end
|
|
132
132
|
end
|
|
@@ -37,7 +37,8 @@ RSpec.describe KeycloakAdmin::ClientRoleMappingsClient do
|
|
|
37
37
|
|
|
38
38
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
39
39
|
"http://auth.service.io/auth/admin/realms/valid-realm/users/test_user/role-mappings/clients/test_client/available",
|
|
40
|
-
faraday_options
|
|
40
|
+
faraday_options,
|
|
41
|
+
anything
|
|
41
42
|
).and_call_original
|
|
42
43
|
|
|
43
44
|
roles = @client_role_mappings_client.list_available
|
|
@@ -74,7 +75,7 @@ RSpec.describe KeycloakAdmin::ClientRoleMappingsClient do
|
|
|
74
75
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
75
76
|
|
|
76
77
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
77
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/users/test_user/role-mappings/clients/test_client", faraday_options).and_call_original
|
|
78
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/users/test_user/role-mappings/clients/test_client", faraday_options, anything).and_call_original
|
|
78
79
|
|
|
79
80
|
@client_role_mappings_client.save(role_list)
|
|
80
81
|
end
|
data/spec/client/client_spec.rb
CHANGED
|
@@ -23,6 +23,59 @@ RSpec.describe KeycloakAdmin::Client do
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
describe "#current_token" do
|
|
27
|
+
def token(expires_in)
|
|
28
|
+
KeycloakAdmin::TokenRepresentation.new(
|
|
29
|
+
"access_token", "bearer", expires_in, "refresh_token",
|
|
30
|
+
"refresh_expires_in", "id_token", "not_before_policy", "session_state"
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "fetches once and reuses the cached token across Client instances sharing the same configuration" do
|
|
35
|
+
configuration = KeycloakAdmin.config
|
|
36
|
+
configuration.clear_cached_token!
|
|
37
|
+
allow_any_instance_of(KeycloakAdmin::TokenClient).to receive(:get).and_return(token(3600))
|
|
38
|
+
|
|
39
|
+
first = KeycloakAdmin::Client.new(configuration).current_token
|
|
40
|
+
second = KeycloakAdmin::Client.new(configuration).current_token
|
|
41
|
+
|
|
42
|
+
expect(first).to be second
|
|
43
|
+
expect(second).to be second
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "fetches a new token once the cached one has expired" do
|
|
47
|
+
configuration = KeycloakAdmin.config
|
|
48
|
+
configuration.clear_cached_token!
|
|
49
|
+
# allow_any_instance_of's sequential and_return(a, b) tracks call count per instance,
|
|
50
|
+
# and a fresh TokenClient is built on every fetch, so a plain external counter is used
|
|
51
|
+
# instead to force the first fetch to be pre-expired and the second one not to be.
|
|
52
|
+
call_count = 0
|
|
53
|
+
allow_any_instance_of(KeycloakAdmin::TokenClient).to receive(:get) do
|
|
54
|
+
call_count += 1
|
|
55
|
+
call_count == 1 ? token(5) : token(3600)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
expired_immediately = KeycloakAdmin::Client.new(configuration).current_token
|
|
59
|
+
refreshed = KeycloakAdmin::Client.new(configuration).current_token
|
|
60
|
+
|
|
61
|
+
expect(expired_immediately).to_not be refreshed
|
|
62
|
+
expect(call_count).to eq 2
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe "#resource" do
|
|
67
|
+
it "builds a Resource with the configuration's faraday_options and logger" do
|
|
68
|
+
configuration = KeycloakAdmin::Configuration.new
|
|
69
|
+
configuration.faraday_options = { timeout: 5 }
|
|
70
|
+
configuration.logger = Logger.new(IO::NULL)
|
|
71
|
+
client = KeycloakAdmin::Client.new(configuration)
|
|
72
|
+
|
|
73
|
+
expect(KeycloakAdmin::Resource).to receive(:new).with("http://x", { timeout: 5 }, configuration.logger)
|
|
74
|
+
|
|
75
|
+
client.send(:resource, "http://x")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
26
79
|
describe "#execute_http" do
|
|
27
80
|
let(:realm_name) { "valid-realm" }
|
|
28
81
|
before(:each) do
|
|
@@ -43,7 +43,7 @@ RSpec.describe KeycloakAdmin::GroupClient do
|
|
|
43
43
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
44
44
|
|
|
45
45
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
46
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/groups/test_group_id", faraday_options).and_call_original
|
|
46
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/groups/test_group_id", faraday_options, anything).and_call_original
|
|
47
47
|
|
|
48
48
|
group = @group_client.get("test_group_id")
|
|
49
49
|
expect(group.id).to eq "test_group_id"
|
|
@@ -72,7 +72,7 @@ RSpec.describe KeycloakAdmin::GroupClient do
|
|
|
72
72
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
73
73
|
|
|
74
74
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
75
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/groups", faraday_options).and_call_original
|
|
75
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/groups", faraday_options, anything).and_call_original
|
|
76
76
|
|
|
77
77
|
groups = @group_client.list
|
|
78
78
|
expect(groups.length).to eq 1
|
|
@@ -102,7 +102,7 @@ RSpec.describe KeycloakAdmin::GroupClient do
|
|
|
102
102
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
103
103
|
|
|
104
104
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
105
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/groups/parent_group_id/children", faraday_options).and_call_original
|
|
105
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/groups/parent_group_id/children", faraday_options, anything).and_call_original
|
|
106
106
|
|
|
107
107
|
groups = @group_client.children("parent_group_id")
|
|
108
108
|
expect(groups.length).to eq 1
|
|
@@ -142,7 +142,7 @@ RSpec.describe KeycloakAdmin::GroupClient do
|
|
|
142
142
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
143
143
|
|
|
144
144
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
145
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/groups", faraday_options).and_call_original
|
|
145
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/groups", faraday_options, anything).and_call_original
|
|
146
146
|
|
|
147
147
|
@group_client.save(group)
|
|
148
148
|
end
|
|
@@ -172,7 +172,7 @@ RSpec.describe KeycloakAdmin::GroupClient do
|
|
|
172
172
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
173
173
|
|
|
174
174
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
175
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/groups/test_group_id", faraday_options).and_call_original
|
|
175
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/groups/test_group_id", faraday_options, anything).and_call_original
|
|
176
176
|
|
|
177
177
|
@group_client.save(group)
|
|
178
178
|
end
|
|
@@ -250,7 +250,7 @@ RSpec.describe KeycloakAdmin::GroupClient do
|
|
|
250
250
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
251
251
|
|
|
252
252
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
253
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/groups/test_group_id", faraday_options).and_raise("error")
|
|
253
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/groups/test_group_id", faraday_options, anything).and_raise("error")
|
|
254
254
|
|
|
255
255
|
expect { @group_client.delete("test_group_id") }.to raise_error("error")
|
|
256
256
|
end
|
|
@@ -82,7 +82,7 @@ RSpec.describe KeycloakAdmin::IdentityProviderClient do
|
|
|
82
82
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
83
83
|
|
|
84
84
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
85
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/identity-provider/instances", faraday_options).and_call_original
|
|
85
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/identity-provider/instances", faraday_options, anything).and_call_original
|
|
86
86
|
|
|
87
87
|
identity_providers = @identity_provider_client.list
|
|
88
88
|
expect(identity_providers.length).to eq 1
|
|
@@ -57,7 +57,7 @@ RSpec.describe KeycloakAdmin::OrganizationClient do
|
|
|
57
57
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
58
58
|
|
|
59
59
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
60
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/organizations?briefRepresentation=true", faraday_options).and_call_original
|
|
60
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/organizations?briefRepresentation=true", faraday_options, anything).and_call_original
|
|
61
61
|
|
|
62
62
|
organizations = @organization_client.list
|
|
63
63
|
expect(organizations.length).to eq 1
|
|
@@ -193,7 +193,7 @@ RSpec.describe KeycloakAdmin::OrganizationClient do
|
|
|
193
193
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
194
194
|
|
|
195
195
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
196
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/organizations/2904e1a1-e5f4-4143-8725-003e54cc8b58", faraday_options).and_raise("error")
|
|
196
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/organizations/2904e1a1-e5f4-4143-8725-003e54cc8b58", faraday_options, anything).and_raise("error")
|
|
197
197
|
|
|
198
198
|
expect { @organization_client.delete("2904e1a1-e5f4-4143-8725-003e54cc8b58") }.to raise_error("error")
|
|
199
199
|
end
|
|
@@ -336,7 +336,7 @@ RSpec.describe KeycloakAdmin::OrganizationClient do
|
|
|
336
336
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
337
337
|
|
|
338
338
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
339
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/organizations/8f6e474e-e688-4bec-99ba-5dc862594f4b", faraday_options).and_call_original
|
|
339
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/organizations/8f6e474e-e688-4bec-99ba-5dc862594f4b", faraday_options, anything).and_call_original
|
|
340
340
|
|
|
341
341
|
organization = @organization_client.get("8f6e474e-e688-4bec-99ba-5dc862594f4b")
|
|
342
342
|
expect(organization).to be
|
|
@@ -64,7 +64,7 @@ RSpec.describe KeycloakAdmin::RealmClient do
|
|
|
64
64
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
65
65
|
|
|
66
66
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
67
|
-
"http://auth.service.io/auth/admin/realms", faraday_options).and_call_original
|
|
67
|
+
"http://auth.service.io/auth/admin/realms", faraday_options, anything).and_call_original
|
|
68
68
|
|
|
69
69
|
realms = @realm_client.list
|
|
70
70
|
expect(realms.length).to eq 1
|
|
@@ -91,7 +91,7 @@ RSpec.describe KeycloakAdmin::RealmClient do
|
|
|
91
91
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
92
92
|
|
|
93
93
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
94
|
-
"http://auth.service.io/auth/admin/realms/valid-realm", faraday_options).and_call_original
|
|
94
|
+
"http://auth.service.io/auth/admin/realms/valid-realm", faraday_options, anything).and_call_original
|
|
95
95
|
|
|
96
96
|
expect(@realm_client.delete).to be_truthy
|
|
97
97
|
end
|
|
@@ -121,7 +121,7 @@ RSpec.describe KeycloakAdmin::RealmClient do
|
|
|
121
121
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
122
122
|
|
|
123
123
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
124
|
-
"http://auth.service.io/auth/admin/realms", faraday_options).and_call_original
|
|
124
|
+
"http://auth.service.io/auth/admin/realms", faraday_options, anything).and_call_original
|
|
125
125
|
|
|
126
126
|
@realm_client.save(realm)
|
|
127
127
|
end
|
|
@@ -147,7 +147,7 @@ RSpec.describe KeycloakAdmin::RealmClient do
|
|
|
147
147
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
148
148
|
|
|
149
149
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
150
|
-
"http://auth.service.io/auth/admin/realms/valid-realm", faraday_options).and_call_original
|
|
150
|
+
"http://auth.service.io/auth/admin/realms/valid-realm", faraday_options, anything).and_call_original
|
|
151
151
|
|
|
152
152
|
@realm_client.update(realm_json)
|
|
153
153
|
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
RSpec.describe KeycloakAdmin::Resource do
|
|
2
|
+
# A real, mutable stand-in for the Faraday::Request builder yielded by
|
|
3
|
+
# connection.get/post/put/delete { |req| ... }.
|
|
4
|
+
FakeRequest = Struct.new(:params, :headers) do
|
|
5
|
+
attr_accessor :body
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
let(:fake_request) { FakeRequest.new({}, {}) }
|
|
9
|
+
let(:fake_response) { double(status: 200, body: "ok", headers: {}, reason_phrase: "OK") }
|
|
10
|
+
|
|
11
|
+
def stub_connection(resource, verb)
|
|
12
|
+
connection = double
|
|
13
|
+
allow(connection).to receive(verb) do |&block|
|
|
14
|
+
block.call(fake_request) if block
|
|
15
|
+
fake_response
|
|
16
|
+
end
|
|
17
|
+
allow(resource).to receive(:connection).and_return(connection)
|
|
18
|
+
connection
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe "#connection (the real Faraday connection, not stubbed)" do
|
|
22
|
+
def middleware_classes(resource)
|
|
23
|
+
resource.send(:connection).builder.handlers
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "does not register the logger middleware when no logger is configured" do
|
|
27
|
+
resource = KeycloakAdmin::Resource.new("http://example.com")
|
|
28
|
+
expect(middleware_classes(resource)).to_not include(Faraday::Response::Logger)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "registers the logger middleware when a logger is configured" do
|
|
32
|
+
resource = KeycloakAdmin::Resource.new("http://example.com", {}, Logger.new(IO::NULL))
|
|
33
|
+
expect(middleware_classes(resource)).to include(Faraday::Response::Logger)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "orders :raise_error before :logger, so a response is logged even when it raises" do
|
|
37
|
+
resource = KeycloakAdmin::Resource.new("http://example.com", {}, Logger.new(IO::NULL))
|
|
38
|
+
handlers = middleware_classes(resource)
|
|
39
|
+
expect(handlers.index(Faraday::Response::RaiseError)).to be < handlers.index(Faraday::Response::Logger)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "logs method/url/status but never headers, since Faraday logs headers by default and one of them is the bearer token" do
|
|
43
|
+
log_output = StringIO.new
|
|
44
|
+
logger = Logger.new(log_output)
|
|
45
|
+
stubs = Faraday::Adapter::Test::Stubs.new
|
|
46
|
+
stubs.get(//) { [200, {}, "ok"] }
|
|
47
|
+
|
|
48
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/x", {}, logger)
|
|
49
|
+
allow(resource).to receive(:connection) do
|
|
50
|
+
Faraday.new(url: "http://example.com/x") do |f|
|
|
51
|
+
f.response :raise_error
|
|
52
|
+
f.response :logger, logger, headers: false
|
|
53
|
+
f.adapter :test, stubs
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
resource.get(Authorization: "Bearer super-secret-token")
|
|
58
|
+
|
|
59
|
+
expect(log_output.string).to include("request: GET")
|
|
60
|
+
expect(log_output.string).to include("response: Status 200")
|
|
61
|
+
expect(log_output.string).to_not include("super-secret-token")
|
|
62
|
+
expect(log_output.string).to_not include("Authorization")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe "#get" do
|
|
67
|
+
it "humanizes symbol header keys into HTTP header names" do
|
|
68
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/users")
|
|
69
|
+
stub_connection(resource, :get)
|
|
70
|
+
|
|
71
|
+
resource.get(Authorization: "Bearer token", content_type: :json, accept: :json)
|
|
72
|
+
|
|
73
|
+
expect(fake_request.headers).to eq(
|
|
74
|
+
"Authorization" => "Bearer token",
|
|
75
|
+
"Content-Type" => "application/json",
|
|
76
|
+
"Accept" => "application/json"
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "extracts :params into the query string instead of sending it as a header" do
|
|
81
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/users")
|
|
82
|
+
stub_connection(resource, :get)
|
|
83
|
+
|
|
84
|
+
resource.get(Authorization: "Bearer token", params: { search: "jean" })
|
|
85
|
+
|
|
86
|
+
expect(fake_request.params).to eq(search: "jean")
|
|
87
|
+
expect(fake_request.headers).to eq("Authorization" => "Bearer token")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "passes a string content_type through unchanged" do
|
|
91
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/users")
|
|
92
|
+
stub_connection(resource, :get)
|
|
93
|
+
|
|
94
|
+
resource.get(content_type: "application/x-www-form-urlencoded")
|
|
95
|
+
|
|
96
|
+
expect(fake_request.headers["Content-Type"]).to eq "application/x-www-form-urlencoded"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "does not set a body" do
|
|
100
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/users")
|
|
101
|
+
stub_connection(resource, :get)
|
|
102
|
+
|
|
103
|
+
resource.get({})
|
|
104
|
+
|
|
105
|
+
expect(fake_request.body).to be_nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "wraps the Faraday response in a KeycloakAdmin::Response" do
|
|
109
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/users")
|
|
110
|
+
stub_connection(resource, :get)
|
|
111
|
+
|
|
112
|
+
expect(resource.get({})).to be_a KeycloakAdmin::Response
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
describe "#post" do
|
|
117
|
+
it "sends a pre-serialized string payload as the body unchanged" do
|
|
118
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/users")
|
|
119
|
+
stub_connection(resource, :post)
|
|
120
|
+
|
|
121
|
+
resource.post('{"username":"jean"}', content_type: :json)
|
|
122
|
+
|
|
123
|
+
expect(fake_request.body).to eq '{"username":"jean"}'
|
|
124
|
+
expect(fake_request.headers["Content-Type"]).to eq "application/json"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "form-encodes a Hash payload" do
|
|
128
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/users")
|
|
129
|
+
stub_connection(resource, :post)
|
|
130
|
+
|
|
131
|
+
resource.post({ email: "a@b.com", firstName: "Jean Yves" }, {})
|
|
132
|
+
|
|
133
|
+
expect(fake_request.body).to eq "email=a%40b.com&firstName=Jean+Yves"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "stamps Content-Type: application/x-www-form-urlencoded for a Hash payload when none was set" do
|
|
137
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/users")
|
|
138
|
+
stub_connection(resource, :post)
|
|
139
|
+
|
|
140
|
+
resource.post({ id: "42" }, {})
|
|
141
|
+
|
|
142
|
+
expect(fake_request.headers["Content-Type"]).to eq "application/x-www-form-urlencoded"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "does not override an explicit Content-Type for a Hash payload" do
|
|
146
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/users")
|
|
147
|
+
stub_connection(resource, :post)
|
|
148
|
+
|
|
149
|
+
resource.post({ id: "42" }, content_type: "application/vnd.custom+form")
|
|
150
|
+
|
|
151
|
+
expect(fake_request.headers["Content-Type"]).to eq "application/vnd.custom+form"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe "#delete" do
|
|
156
|
+
it "sends no body and only the given headers" do
|
|
157
|
+
resource = KeycloakAdmin::Resource.new("http://example.com/users/1")
|
|
158
|
+
stub_connection(resource, :delete)
|
|
159
|
+
|
|
160
|
+
resource.delete(Authorization: "Bearer token")
|
|
161
|
+
|
|
162
|
+
expect(fake_request.body).to be_nil
|
|
163
|
+
expect(fake_request.headers).to eq("Authorization" => "Bearer token")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe ".execute (RestClient::Request.execute equivalent)" do
|
|
168
|
+
it "dispatches an arbitrary method with a payload, including DELETE with a body" do
|
|
169
|
+
resource_instance = KeycloakAdmin::Resource.new("http://example.com/mappings")
|
|
170
|
+
allow(KeycloakAdmin::Resource).to receive(:new).and_return(resource_instance)
|
|
171
|
+
stub_connection(resource_instance, :delete)
|
|
172
|
+
|
|
173
|
+
KeycloakAdmin::Resource.execute(
|
|
174
|
+
method: :delete,
|
|
175
|
+
url: "http://example.com/mappings",
|
|
176
|
+
payload: '[{"id":"1"}]',
|
|
177
|
+
headers: { content_type: :json }
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
expect(fake_request.body).to eq '[{"id":"1"}]'
|
|
181
|
+
expect(fake_request.headers["Content-Type"]).to eq "application/json"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "forwards non-method/url/payload/headers keys as connection options" do
|
|
185
|
+
resource_instance = KeycloakAdmin::Resource.new("http://example.com/x")
|
|
186
|
+
expected_options = { timeout: 5 }
|
|
187
|
+
stub_connection(resource_instance, :get)
|
|
188
|
+
expect(KeycloakAdmin::Resource).to receive(:new).with("http://example.com/x", expected_options, nil).and_return(resource_instance)
|
|
189
|
+
|
|
190
|
+
KeycloakAdmin::Resource.execute(method: :get, url: "http://example.com/x", headers: {}, timeout: 5)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "extracts :logger and passes it positionally instead of leaving it in connection options" do
|
|
194
|
+
resource_instance = KeycloakAdmin::Resource.new("http://example.com/x")
|
|
195
|
+
logger = Logger.new(IO::NULL)
|
|
196
|
+
stub_connection(resource_instance, :get)
|
|
197
|
+
expect(KeycloakAdmin::Resource).to receive(:new).with("http://example.com/x", {}, logger).and_return(resource_instance)
|
|
198
|
+
|
|
199
|
+
KeycloakAdmin::Resource.execute(method: :get, url: "http://example.com/x", headers: {}, logger: logger)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
describe ".put (RestClient.put shorthand)" do
|
|
204
|
+
it "posts without requiring any connection options" do
|
|
205
|
+
resource_instance = KeycloakAdmin::Resource.new("http://example.com/x")
|
|
206
|
+
allow(KeycloakAdmin::Resource).to receive(:new).and_return(resource_instance)
|
|
207
|
+
stub_connection(resource_instance, :put)
|
|
208
|
+
|
|
209
|
+
KeycloakAdmin::Resource.put("http://example.com/x", "body", {})
|
|
210
|
+
|
|
211
|
+
expect(fake_request.body).to eq "body"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "forwards the given logger to the underlying Resource" do
|
|
215
|
+
resource_instance = KeycloakAdmin::Resource.new("http://example.com/x")
|
|
216
|
+
logger = Logger.new(IO::NULL)
|
|
217
|
+
stub_connection(resource_instance, :put)
|
|
218
|
+
expect(KeycloakAdmin::Resource).to receive(:new).with("http://example.com/x", {}, logger).and_return(resource_instance)
|
|
219
|
+
|
|
220
|
+
KeycloakAdmin::Resource.put("http://example.com/x", "body", {}, logger)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
RSpec.describe KeycloakAdmin::Response do
|
|
2
|
+
def fake_faraday_response(status: 200, body: "", headers: {}, reason_phrase: "OK")
|
|
3
|
+
double(status: status, body: body, headers: headers, reason_phrase: reason_phrase)
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
describe "body-as-string behavior (RestClient::Response was a String subclass)" do
|
|
7
|
+
it "behaves like the raw body string" do
|
|
8
|
+
response = KeycloakAdmin::Response.new(fake_faraday_response(body: '{"a":1}'))
|
|
9
|
+
expect(response).to eq '{"a":1}'
|
|
10
|
+
expect(JSON.parse(response)).to eq({ "a" => 1 })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "supports String methods relied on by callers, e.g. #to_i" do
|
|
14
|
+
response = KeycloakAdmin::Response.new(fake_faraday_response(body: "42"))
|
|
15
|
+
expect(response.to_i).to eq 42
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "exposes #body returning the same content" do
|
|
19
|
+
response = KeycloakAdmin::Response.new(fake_faraday_response(body: "hello"))
|
|
20
|
+
expect(response.body).to eq "hello"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "#status / #code / #reason_phrase" do
|
|
25
|
+
it "exposes the HTTP status" do
|
|
26
|
+
response = KeycloakAdmin::Response.new(fake_faraday_response(status: 201))
|
|
27
|
+
expect(response.status).to eq 201
|
|
28
|
+
expect(response.code).to eq 201
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "exposes the reason phrase" do
|
|
32
|
+
response = KeycloakAdmin::Response.new(fake_faraday_response(reason_phrase: "Created"))
|
|
33
|
+
expect(response.reason_phrase).to eq "Created"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe "#headers" do
|
|
38
|
+
it "symbolizes header names the way RestClient did (downcased, dash to underscore)" do
|
|
39
|
+
response = KeycloakAdmin::Response.new(fake_faraday_response(headers: { "Location" => "/users/42", "Content-Type" => "application/json" }))
|
|
40
|
+
expect(response.headers[:location]).to eq "/users/42"
|
|
41
|
+
expect(response.headers[:content_type]).to eq "application/json"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "splits a single Set-Cookie header into a one-element array" do
|
|
45
|
+
response = KeycloakAdmin::Response.new(fake_faraday_response(headers: { "Set-Cookie" => "a=1; Path=/" }))
|
|
46
|
+
expect(response.headers[:set_cookie]).to eq ["a=1; Path=/"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "splits multiple Set-Cookie headers joined by Faraday's net_http adapter" do
|
|
50
|
+
joined = "KEYCLOAK_SESSION=abc; Path=/, KEYCLOAK_IDENTITY=def; Path=/, AUTH_SESSION_ID=ghi; Path=/"
|
|
51
|
+
response = KeycloakAdmin::Response.new(fake_faraday_response(headers: { "Set-Cookie" => joined }))
|
|
52
|
+
expect(response.headers[:set_cookie]).to eq [
|
|
53
|
+
"KEYCLOAK_SESSION=abc; Path=/",
|
|
54
|
+
"KEYCLOAK_IDENTITY=def; Path=/",
|
|
55
|
+
"AUTH_SESSION_ID=ghi; Path=/"
|
|
56
|
+
]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "does not split on a comma embedded in a cookie's own Expires attribute" do
|
|
60
|
+
joined = "a=1; Expires=Wed, 09 Jun 2027 10:18:14 GMT; Path=/, b=2; Path=/"
|
|
61
|
+
response = KeycloakAdmin::Response.new(fake_faraday_response(headers: { "Set-Cookie" => joined }))
|
|
62
|
+
expect(response.headers[:set_cookie]).to eq [
|
|
63
|
+
"a=1; Expires=Wed, 09 Jun 2027 10:18:14 GMT; Path=/",
|
|
64
|
+
"b=2; Path=/"
|
|
65
|
+
]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -39,7 +39,7 @@ RSpec.describe KeycloakAdmin::RoleClient do
|
|
|
39
39
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
40
40
|
|
|
41
41
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
42
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/roles", faraday_options).and_call_original
|
|
42
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/roles", faraday_options, anything).and_call_original
|
|
43
43
|
|
|
44
44
|
roles = @role_client.list
|
|
45
45
|
expect(roles.length).to eq 1
|
|
@@ -71,7 +71,7 @@ RSpec.describe KeycloakAdmin::RoleClient do
|
|
|
71
71
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
72
72
|
|
|
73
73
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
74
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/roles", faraday_options).and_call_original
|
|
74
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/roles", faraday_options, anything).and_call_original
|
|
75
75
|
|
|
76
76
|
@role_client.save(role)
|
|
77
77
|
end
|
|
@@ -60,7 +60,7 @@ RSpec.describe KeycloakAdmin::RoleMapperClient do
|
|
|
60
60
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
61
61
|
|
|
62
62
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
63
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/users/test_user/role-mappings/realm", faraday_options).and_call_original
|
|
63
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/users/test_user/role-mappings/realm", faraday_options, anything).and_call_original
|
|
64
64
|
|
|
65
65
|
@role_mapper_client.save_realm_level(role_list)
|
|
66
66
|
end
|
|
@@ -54,7 +54,7 @@ RSpec.describe KeycloakAdmin::TokenClient do
|
|
|
54
54
|
stub_post
|
|
55
55
|
|
|
56
56
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
57
|
-
"http://auth.service.io/auth/realms/valid-realm/protocol/openid-connect/token", faraday_options).and_call_original
|
|
57
|
+
"http://auth.service.io/auth/realms/valid-realm/protocol/openid-connect/token", faraday_options, anything).and_call_original
|
|
58
58
|
|
|
59
59
|
@token_client.get
|
|
60
60
|
end
|
|
@@ -145,7 +145,7 @@ RSpec.describe KeycloakAdmin::TokenClient do
|
|
|
145
145
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
146
146
|
|
|
147
147
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
148
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/users", faraday_options).and_call_original
|
|
148
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/users", faraday_options, anything).and_call_original
|
|
149
149
|
|
|
150
150
|
expect(@user_client.save(user)).to eq user
|
|
151
151
|
end
|
|
@@ -171,7 +171,7 @@ RSpec.describe KeycloakAdmin::TokenClient do
|
|
|
171
171
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
172
172
|
|
|
173
173
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
174
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/users/test_user_id", faraday_options).and_call_original
|
|
174
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/users/test_user_id", faraday_options, anything).and_call_original
|
|
175
175
|
|
|
176
176
|
user = @user_client.get('test_user_id')
|
|
177
177
|
expect(user.username).to eq 'test_username'
|
|
@@ -217,7 +217,7 @@ RSpec.describe KeycloakAdmin::TokenClient do
|
|
|
217
217
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
218
218
|
|
|
219
219
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
220
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/users", faraday_options).and_call_original
|
|
220
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/users", faraday_options, anything).and_call_original
|
|
221
221
|
|
|
222
222
|
users = @user_client.search("test_username")
|
|
223
223
|
expect(users.length).to eq 1
|
|
@@ -250,7 +250,7 @@ RSpec.describe KeycloakAdmin::TokenClient do
|
|
|
250
250
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
251
251
|
|
|
252
252
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
253
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/users", faraday_options).and_call_original
|
|
253
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/users", faraday_options, anything).and_call_original
|
|
254
254
|
|
|
255
255
|
users = @user_client.list
|
|
256
256
|
expect(users.length).to eq 1
|
|
@@ -277,7 +277,7 @@ RSpec.describe KeycloakAdmin::TokenClient do
|
|
|
277
277
|
allow_any_instance_of(KeycloakAdmin::Configuration).to receive(:faraday_options).and_return faraday_options
|
|
278
278
|
|
|
279
279
|
expect(KeycloakAdmin::Resource).to receive(:new).with(
|
|
280
|
-
"http://auth.service.io/auth/admin/realms/valid-realm/users/test_user_id", faraday_options).and_call_original
|
|
280
|
+
"http://auth.service.io/auth/admin/realms/valid-realm/users/test_user_id", faraday_options, anything).and_call_original
|
|
281
281
|
|
|
282
282
|
@user_client.delete('test_user_id')
|
|
283
283
|
end
|
data/spec/configuration_spec.rb
CHANGED
|
@@ -68,6 +68,43 @@ RSpec.describe KeycloakAdmin::RealmClient do
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
describe "#cached_token / #cache_token / #clear_cached_token!" do
|
|
72
|
+
def token(expires_in)
|
|
73
|
+
KeycloakAdmin::TokenRepresentation.new(
|
|
74
|
+
"access_token", "bearer", expires_in, "refresh_token",
|
|
75
|
+
"refresh_expires_in", "id_token", "not_before_policy", "session_state"
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "has no cached token initially" do
|
|
80
|
+
expect(@configuration.cached_token).to be_nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "returns the token it was given, while it is still within its expiry" do
|
|
84
|
+
cached = @configuration.cache_token(token(3600))
|
|
85
|
+
expect(cached.access_token).to eq "access_token"
|
|
86
|
+
expect(@configuration.cached_token).to eq cached
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "treats a token as expired a bit before its real expiry, not just after" do
|
|
90
|
+
# expires_in: 5, minus the 10s safety margin, is already in the past the moment it is cached.
|
|
91
|
+
@configuration.cache_token(token(5))
|
|
92
|
+
expect(@configuration.cached_token).to be_nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "does not cache a token with a non-numeric expires_in, but still returns it" do
|
|
96
|
+
returned = @configuration.cache_token(token("not-a-number"))
|
|
97
|
+
expect(returned.access_token).to eq "access_token"
|
|
98
|
+
expect(@configuration.cached_token).to be_nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "clears the cache" do
|
|
102
|
+
@configuration.cache_token(token(3600))
|
|
103
|
+
@configuration.clear_cached_token!
|
|
104
|
+
expect(@configuration.cached_token).to be_nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
71
108
|
describe "#body_for_token_retrieval" do
|
|
72
109
|
before(:each) do
|
|
73
110
|
@body = @configuration.body_for_token_retrieval
|
data/spec/spec_helper.rb
CHANGED
|
@@ -19,11 +19,18 @@ RSpec.configure do |config|
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
configure
|
|
22
|
+
|
|
23
|
+
# KeycloakAdmin.config is a global set up once for the whole suite (see 'configure'
|
|
24
|
+
# above), not per example, so a token cached in one example would otherwise leak into
|
|
25
|
+
# the next one and silently skip its stub_token_client expectations.
|
|
26
|
+
config.before(:each) do
|
|
27
|
+
KeycloakAdmin.config.clear_cached_token!
|
|
28
|
+
end
|
|
22
29
|
end
|
|
23
30
|
|
|
24
31
|
def stub_token_client
|
|
25
32
|
allow_any_instance_of(KeycloakAdmin::TokenClient).to receive(:get).and_return KeycloakAdmin::TokenRepresentation.new(
|
|
26
|
-
"test_access_token", "token_type",
|
|
33
|
+
"test_access_token", "token_type", 3600, "refresh_token",
|
|
27
34
|
"refresh_expires_in", "id_token", "not_before_policy", "session_state"
|
|
28
35
|
)
|
|
29
36
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keycloak-admin
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lorent Lempereur
|
|
@@ -85,6 +85,20 @@ dependencies:
|
|
|
85
85
|
- - '='
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
87
|
version: 12.0.0
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: rake
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '13.0'
|
|
95
|
+
type: :development
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '13.0'
|
|
88
102
|
description: Keycloak Admin REST API client written in Ruby
|
|
89
103
|
email:
|
|
90
104
|
- lorent.lempereur.dev@gmail.com
|
|
@@ -94,6 +108,7 @@ extra_rdoc_files: []
|
|
|
94
108
|
files:
|
|
95
109
|
- ".github/workflows/Dockerfile"
|
|
96
110
|
- ".github/workflows/ci.yml"
|
|
111
|
+
- ".github/workflows/release.yml"
|
|
97
112
|
- ".gitignore"
|
|
98
113
|
- ".rspec"
|
|
99
114
|
- CHANGELOG.md
|
|
@@ -102,6 +117,7 @@ files:
|
|
|
102
117
|
- Gemfile.lock
|
|
103
118
|
- MIT-LICENSE
|
|
104
119
|
- README.md
|
|
120
|
+
- Rakefile
|
|
105
121
|
- bin/console
|
|
106
122
|
- keycloak-admin.gemspec
|
|
107
123
|
- lib/keycloak-admin.rb
|
|
@@ -121,6 +137,8 @@ files:
|
|
|
121
137
|
- lib/keycloak-admin/client/identity_provider_client.rb
|
|
122
138
|
- lib/keycloak-admin/client/organization_client.rb
|
|
123
139
|
- lib/keycloak-admin/client/realm_client.rb
|
|
140
|
+
- lib/keycloak-admin/client/resource.rb
|
|
141
|
+
- lib/keycloak-admin/client/response.rb
|
|
124
142
|
- lib/keycloak-admin/client/role_client.rb
|
|
125
143
|
- lib/keycloak-admin/client/role_mapper_client.rb
|
|
126
144
|
- lib/keycloak-admin/client/token_client.rb
|
|
@@ -171,6 +189,8 @@ files:
|
|
|
171
189
|
- spec/client/identity_provider_client_spec.rb
|
|
172
190
|
- spec/client/organization_client_spec.rb
|
|
173
191
|
- spec/client/realm_client_spec.rb
|
|
192
|
+
- spec/client/resource_spec.rb
|
|
193
|
+
- spec/client/response_spec.rb
|
|
174
194
|
- spec/client/role_client_spec.rb
|
|
175
195
|
- spec/client/role_mapper_client_spec.rb
|
|
176
196
|
- spec/client/token_client_spec.rb
|
|
@@ -216,7 +236,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
216
236
|
- !ruby/object:Gem::Version
|
|
217
237
|
version: '0'
|
|
218
238
|
requirements: []
|
|
219
|
-
rubygems_version:
|
|
239
|
+
rubygems_version: 3.6.9
|
|
220
240
|
specification_version: 4
|
|
221
241
|
summary: Keycloak Admin REST API client written in Ruby
|
|
222
242
|
test_files: []
|