Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ All options have a default value. However, all of them can be changed in your in
* Execute actions emails
* Send forgot passsword mail
* Client Authorization, create, update, get, delete Resource, Scope, Policy, Permission, Policy Enforcer
* Get list of protocol mappers for a client scope, create/update/get/delete a protocol mapper
* Get list of organizations, create/update/get/delete an organization
* Get list of members of an organization, add/remove members
* Invite new or existing users to an organization
Expand Down Expand Up @@ -757,6 +758,62 @@ KeycloakAdmin.realm("realm_a").authz_permissions(client.id, 'scope').delete(scop
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, 'resource').delete(resource_permission.id)
```

### Manage Protocol Mappers for a Client Scope

Protocol mappers allow you to transform tokens and assertions. The following operations are available on the protocol mappers of a given client scope.

### List protocol mappers for a client scope

Returns an array of `KeycloakAdmin::ProtocolMapperRepresentation`.

```ruby
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).list
```

### Get a protocol mapper by its id

Returns an instance of `KeycloakAdmin::ProtocolMapperRepresentation`.

```ruby
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
mapper_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).get(mapper_id)
```

### Create a protocol mapper for a client scope

Takes `mapper_representation` of type `KeycloakAdmin::ProtocolMapperRepresentation`. Returns `true` on success.

```ruby
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
mapper = KeycloakAdmin::ProtocolMapperRepresentation.new
mapper.name = "my-mapper"
mapper.protocol = "openid-connect"
mapper.protocolMapper = "oidc-usermodel-attribute-mapper"
mapper.config = { "user.attribute" => "locale", "claim.name" => "locale", "jsonType.label" => "String", "id.token.claim" => "true", "access.token.claim" => "true", "userinfo.token.claim" => "true" }
KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).create!(mapper)
```

### Update a protocol mapper for a client scope

Takes `mapper_representation` of type `KeycloakAdmin::ProtocolMapperRepresentation` (must include its `id`). Returns `true` on success.

```ruby
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
mapper = KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).get(mapper_id)
mapper.config["claim.name"] = "updated_claim"
KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).update(mapper)
```

### Delete a protocol mapper from a client scope

```ruby
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
mapper_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).delete(mapper_id)
```

## How to execute library tests

From the `keycloak-admin-api` directory:
Expand Down
1 change: 1 addition & 0 deletions lib/keycloak-admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require_relative "keycloak-admin/client/configurable_token_client"
require_relative "keycloak-admin/client/attack_detection_client"
require_relative "keycloak-admin/client/client_authz_scope_client"
require_relative "keycloak-admin/client/client_scope_protocol_mapper_client"
require_relative "keycloak-admin/client/client_authz_resource_client"
require_relative "keycloak-admin/client/client_authz_policy_client"
require_relative "keycloak-admin/client/client_authz_permission_client"
Expand Down
62 changes: 62 additions & 0 deletions lib/keycloak-admin/client/client_scope_protocol_mapper_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module KeycloakAdmin
class ClientScopeProtocolMapperClient < Client
def initialize(configuration, realm_client, client_scope_id)
super(configuration)

raise ArgumentError.new("realm must be defined") unless realm_client.name_defined?

@realm_client = realm_client
@client_scope_id = client_scope_id
end

def list
response = execute_http do
RestClient::Resource.new(protocol_mappers_url, @configuration.rest_client_options).get(headers)
end

JSON.parse(response).map { |h| ProtocolMapperRepresentation.from_hash(h) }
end

def get(mapper_id)
response = execute_http do
RestClient::Resource.new(protocol_mappers_url(mapper_id), @configuration.rest_client_options).get(headers)
end

ProtocolMapperRepresentation.from_hash(JSON.parse(response))
end

def create!(mapper_representation)
execute_http do
RestClient::Resource.new(protocol_mappers_url, @configuration.rest_client_options).post(
create_payload(mapper_representation), headers
)
end

true
end

def update(mapper_representation)
execute_http do
RestClient::Resource.new(protocol_mappers_url(mapper_representation.id), @configuration.rest_client_options).put(
create_payload(mapper_representation), headers
)
end

true
end

def delete(mapper_id)
execute_http do
RestClient::Resource.new(protocol_mappers_url(mapper_id), @configuration.rest_client_options).delete(headers)
end

true
end

def protocol_mappers_url(mapper_id = nil)
base = "#{@realm_client.realm_admin_url}/client-scopes/#{@client_scope_id}/protocol-mappers/models"

mapper_id ? "#{base}/#{mapper_id}" : base
end
end
end
4 changes: 4 additions & 0 deletions lib/keycloak-admin/client/realm_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ def user(user_id)
UserResource.new(@configuration, self, user_id)
end

def client_scope_protocol_mappers(client_scope_id)
ClientScopeProtocolMapperClient.new(@configuration, self, client_scope_id)
end

def authz_scopes(client_id, resource_id = nil)
ClientAuthzScopeClient.new(@configuration, self, client_id, resource_id)
end
Expand Down
230 changes: 230 additions & 0 deletions spec/client/client_scope_protocol_mapper_client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
RSpec.describe KeycloakAdmin::ClientScopeProtocolMapperClient do
let(:realm_name) { "valid-realm" }
let(:client_scope_id) { "valid-scope-id" }
let(:mapper_id) { "valid-mapper-id" }

let(:mapper_json) do
<<~JSON
{"id":"valid-mapper-id","name":"my-claim","protocol":"openid-connect","protocolMapper":"oidc-hardcoded-claim-mapper","config":{"claim.name":"my_claim","claim.value":"bar","access.token.claim":"true"}}
JSON
end

let(:audience_mapper_json) do
<<~JSON
{"protocol":"openid-connect","protocolMapper":"oidc-audience-mapper","name":"audience-config-rvw-123","config":{"included.client.audience":"","included.custom.audience":"https://api.example.com","id.token.claim":"false","access.token.claim":"true","lightweight.claim":"false","introspection.token.claim":"true"}}
JSON
end

describe "#initialize" do
context "when realm_name is defined" do
it "does not raise any error" do
expect { KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) }.to_not raise_error
end
end

context "when realm_name is not defined" do
it "raises an argument error" do
expect { KeycloakAdmin.realm(nil).client_scope_protocol_mappers(client_scope_id) }.to raise_error(ArgumentError)
end
end
end

describe "#list" do
before(:each) do
@client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id)
stub_token_client
allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response
end

context "with a hardcoded claim mapper" do
let(:stub_response) { "[#{mapper_json}]" }

it "returns one mapper" do
expect(@client.list.size).to eq 1
end

it "returns the correct mapper attributes" do
expect(@client.list.first).to have_attributes(id: "valid-mapper-id", name: "my-claim", protocol: "openid-connect", protocolMapper: "oidc-hardcoded-claim-mapper")
end
end

context "with an audience mapper" do
let(:stub_response) { "[#{audience_mapper_json}]" }

it "returns one mapper" do
expect(@client.list.size).to eq 1
end

it "returns the correct mapper attributes" do
expect(@client.list.first).to have_attributes(name: "audience-config-rvw-123", protocol: "openid-connect", protocolMapper: "oidc-audience-mapper")
end
end

context "with multiple mappers" do
let(:stub_response) { "[#{mapper_json},#{audience_mapper_json}]" }

it "returns two mappers" do
expect(@client.list.size).to eq 2
end

it "includes both mapper names" do
expect(@client.list.map(&:name)).to include("my-claim", "audience-config-rvw-123")
end
end
end

describe "#get" do
before(:each) do
@client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id)
stub_token_client
allow_any_instance_of(RestClient::Resource).to receive(:get).and_return stub_response
end

context "with a hardcoded claim mapper" do
let(:stub_response) { mapper_json }

it "returns the correct id" do
expect(@client.get(mapper_id).id).to eq "valid-mapper-id"
end

it "returns the correct name" do
expect(@client.get(mapper_id).name).to eq "my-claim"
end

it "returns the correct protocol" do
expect(@client.get(mapper_id).protocol).to eq "openid-connect"
end

it "returns the correct protocolMapper" do
expect(@client.get(mapper_id).protocolMapper).to eq "oidc-hardcoded-claim-mapper"
end
end

context "with an audience mapper" do
let(:stub_response) { audience_mapper_json }

it "returns the correct name" do
expect(@client.get(mapper_id).name).to eq "audience-config-rvw-123"
end

it "returns the correct protocol" do
expect(@client.get(mapper_id).protocol).to eq "openid-connect"
end

it "returns the correct protocolMapper" do
expect(@client.get(mapper_id).protocolMapper).to eq "oidc-audience-mapper"
end

it "returns the correct config" do
expect(@client.get(mapper_id).config).to include(
"included.custom.audience" => "https://api.example.com",
"access.token.claim" => "true",
"introspection.token.claim" => "true",
"id.token.claim" => "false"
)
end
end
end

describe "#create!" do
before(:each) do
@client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id)
stub_token_client
allow_any_instance_of(RestClient::Resource).to receive(:post).and_return stub_response
end

context "with a hardcoded claim mapper" do
let(:stub_response) { mapper_json }
let(:mapper_representation) do
mapper = KeycloakAdmin::ProtocolMapperRepresentation.new
mapper.name = "my-claim"
mapper.protocol = "openid-connect"
mapper.protocolMapper = "oidc-hardcoded-claim-mapper"
mapper.config = { "claim.name" => "my_claim", "claim.value" => "bar", "access.token.claim" => "true" }
mapper
end

it "creates successfully" do
expect(@client.create!(mapper_representation)).to be true
end
end

context "with an audience mapper" do
let(:stub_response) { audience_mapper_json }
let(:mapper_representation) do
mapper = KeycloakAdmin::ProtocolMapperRepresentation.new
mapper.name = "audience-config-rvw-123"
mapper.protocol = "openid-connect"
mapper.protocolMapper = "oidc-audience-mapper"
mapper.config = {
"included.client.audience" => "",
"included.custom.audience" => "https://api.example.com",
"id.token.claim" => "false",
"access.token.claim" => "true",
"lightweight.claim" => "false",
"introspection.token.claim" => "true"
}
mapper
end

it "creates successfully" do
expect(@client.create!(mapper_representation)).to be true
end
end
end

describe "#update" do
before(:each) do
@client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id)
stub_token_client
allow_any_instance_of(RestClient::Resource).to receive(:put).and_return ""
end

context "with a hardcoded claim mapper" do
let(:mapper_representation) { KeycloakAdmin::ProtocolMapperRepresentation.from_hash(JSON.parse(mapper_json)) }

it "calls put on the mapper url" do
expect_any_instance_of(RestClient::Resource).to receive(:put).with(anything, anything)
@client.update(mapper_representation)
end
end

context "with an audience mapper" do
let(:mapper_representation) { KeycloakAdmin::ProtocolMapperRepresentation.from_hash(JSON.parse(audience_mapper_json)) }

it "calls put on the mapper url" do
expect_any_instance_of(RestClient::Resource).to receive(:put).with(anything, anything)
@client.update(mapper_representation)
end
end
end

describe "#delete" do
before(:each) do
@client = KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id)
stub_token_client
allow_any_instance_of(RestClient::Resource).to receive(:delete).and_return ""
end

it "returns true" do
expect(@client.delete(mapper_id)).to eq true
end
end

describe "#protocol_mappers_url" do
let(:client) { KeycloakAdmin.realm(realm_name).client_scope_protocol_mappers(client_scope_id) }
let(:base_url) { "http://auth.service.io/auth/admin/realms/valid-realm/client-scopes/valid-scope-id/protocol-mappers/models" }

context "without a mapper_id" do
it "returns the base url" do
expect(client.protocol_mappers_url).to eq base_url
end
end

context "with a mapper_id" do
it "returns the url with mapper_id appended" do
expect(client.protocol_mappers_url(mapper_id)).to eq "#{base_url}/valid-mapper-id"
end
end
end
end
Loading