clonk 1.0.0 → 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.
- checksums.yaml +4 -4
- data/lib/clonk.rb +3 -89
- data/lib/clonk/client.rb +34 -124
- data/lib/clonk/connection.rb +165 -0
- data/lib/clonk/group.rb +29 -105
- data/lib/clonk/permission.rb +34 -55
- data/lib/clonk/policy.rb +11 -12
- data/lib/clonk/realm.rb +19 -41
- data/lib/clonk/role.rb +15 -57
- data/lib/clonk/user.rb +20 -93
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f8dab272e5def2a4916f0865f6829f47345371716ac09c235e4fdc9411127e7
|
4
|
+
data.tar.gz: f3dbefc497d92a22dd31ea11fd31482fb2b94fdc39b229eec6c902e8995f6faf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 703d6fa690a791364af18cc73c2b8eb391147cd3789683139203b5dec7809f1024f0478d82bd06fc673830b3b17d4b7215ff3be3debc7e625b70720505270ad2
|
7
|
+
data.tar.gz: a44c4a23a58c00d0eb43283013a9f4dd8de14900e112b1d779818ca67e86411abf974f200f89c87d45498437ada9aaa41024fe665a122496a5098223170ccf76
|
data/lib/clonk.rb
CHANGED
@@ -2,18 +2,13 @@
|
|
2
2
|
|
3
3
|
require 'faraday'
|
4
4
|
require 'faraday_middleware'
|
5
|
-
# require 'dotenv/load'
|
6
5
|
require 'json'
|
7
|
-
# require 'pp'
|
8
6
|
|
9
|
-
BASE_URL =
|
10
|
-
USERNAME = ENV.fetch('SSO_USERNAME')
|
11
|
-
PASSWORD = ENV.fetch('SSO_PASSWORD')
|
12
|
-
REALM = ENV.fetch('SSO_REALM')
|
7
|
+
BASE_URL = CGI.escape(ENV.fetch('SSO_BASE_URL'))
|
13
8
|
|
9
|
+
# Keycloak/Red Hat SSO API wrapper
|
14
10
|
module Clonk
|
15
11
|
class << self
|
16
|
-
|
17
12
|
##
|
18
13
|
# Defines a Faraday::Connection object linked to the SSO instance.
|
19
14
|
|
@@ -25,88 +20,6 @@ module Clonk
|
|
25
20
|
faraday.headers['Authorization'] = "Bearer #{token}" if token
|
26
21
|
end
|
27
22
|
end
|
28
|
-
|
29
|
-
##
|
30
|
-
# Returns the admin API root for the realm.
|
31
|
-
|
32
|
-
def realm_admin_root(realm = REALM)
|
33
|
-
"#{BASE_URL}/auth/admin/realms/#{realm}"
|
34
|
-
end
|
35
|
-
|
36
|
-
##
|
37
|
-
# Retrieves a token for the admin user.
|
38
|
-
|
39
|
-
def admin_token
|
40
|
-
data = {
|
41
|
-
username: USERNAME,
|
42
|
-
password: PASSWORD,
|
43
|
-
grant_type: 'password',
|
44
|
-
client_id: 'admin-cli'
|
45
|
-
}
|
46
|
-
|
47
|
-
JSON.parse(
|
48
|
-
connection(json: false)
|
49
|
-
.post('/auth/realms/master/protocol/openid-connect/token', data).body
|
50
|
-
)['access_token']
|
51
|
-
end
|
52
|
-
|
53
|
-
##
|
54
|
-
# Returns a Faraday::Response for an API call via the given method.
|
55
|
-
# Always uses an admin token.
|
56
|
-
#--
|
57
|
-
# FIXME: Rename protocol to method - more descriptive
|
58
|
-
#++
|
59
|
-
|
60
|
-
def response(method: :get, path: '/', data: nil, token: admin_token)
|
61
|
-
return unless %i[get post put delete].include?(method)
|
62
|
-
|
63
|
-
conn = connection(token: token).public_send(method, path, data)
|
64
|
-
end
|
65
|
-
|
66
|
-
##
|
67
|
-
# Returns a parsed JSON response for an API call via the given method.
|
68
|
-
# Useful in instances where only the data is necessary, and not
|
69
|
-
# HTTP status confirmation that the desired effect was caused.
|
70
|
-
# Always uses an admin token.
|
71
|
-
#--
|
72
|
-
# FIXME: Rename protocol to method - more descriptive
|
73
|
-
#++
|
74
|
-
|
75
|
-
def parsed_response(method: :get, path: '/', data: nil, token: admin_token)
|
76
|
-
resp = response(method: method, path: path, data: data, token: token)
|
77
|
-
|
78
|
-
JSON.parse(resp.body)
|
79
|
-
rescue JSON::ParserError
|
80
|
-
resp.body
|
81
|
-
end
|
82
|
-
|
83
|
-
##
|
84
|
-
# Enables permissions for the given object.
|
85
|
-
#--
|
86
|
-
# TODO: Add this method to other models that need it, if any
|
87
|
-
#++
|
88
|
-
|
89
|
-
def set_permissions(object: nil, type: nil, enabled: true, realm: REALM)
|
90
|
-
parsed_response(
|
91
|
-
method: :put,
|
92
|
-
path: "#{realm_admin_root(realm)}/#{type}s/#{object['id']}/management/permissions",
|
93
|
-
data: { enabled: enabled },
|
94
|
-
token: @token
|
95
|
-
)
|
96
|
-
end
|
97
|
-
|
98
|
-
##
|
99
|
-
# Returns the data for the permission with the given ID.
|
100
|
-
#--
|
101
|
-
# TODO: Move this method into Permission
|
102
|
-
#++
|
103
|
-
|
104
|
-
def get_permission(id: nil, realm: REALM)
|
105
|
-
parsed_response(
|
106
|
-
token: @token,
|
107
|
-
path: "#{client_url(client: @realm_management, realm: realm)}/authz/resource-server/permission/scope/#{id}"
|
108
|
-
)
|
109
|
-
end
|
110
23
|
end
|
111
24
|
end
|
112
25
|
|
@@ -117,3 +30,4 @@ require 'clonk/policy'
|
|
117
30
|
require 'clonk/role'
|
118
31
|
require 'clonk/realm'
|
119
32
|
require 'clonk/permission'
|
33
|
+
require 'clonk/connection'
|
data/lib/clonk/client.rb
CHANGED
@@ -1,150 +1,66 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
module Clonk
|
3
4
|
##
|
4
|
-
# This class represents a client within SSO. A client allows a user to
|
5
|
-
|
5
|
+
# This class represents a client within SSO. A client allows a user to
|
6
|
+
# authenticate against SSO with their credentials.
|
6
7
|
class Client
|
7
8
|
attr_accessor :id
|
8
9
|
attr_reader :name
|
9
10
|
|
10
|
-
def initialize(clients_response
|
11
|
+
def initialize(clients_response)
|
11
12
|
@id = clients_response['id']
|
12
13
|
@name = clients_response['clientId']
|
13
|
-
@realm = realm
|
14
14
|
end
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
{
|
21
|
-
fullScopeAllowed: false
|
22
|
-
}
|
23
|
-
end
|
24
|
-
|
25
|
-
##
|
26
|
-
# Creates a client within SSO and returns the created client as a Client.
|
27
|
-
# Note: 'dag' stands for Direct Access Grants
|
28
|
-
|
29
|
-
def self.create(realm: REALM, name: nil, public_client: true, dag_enabled: true)
|
30
|
-
# TODO: Client with a secret
|
31
|
-
response = Clonk.response(
|
32
|
-
method: :post,
|
33
|
-
path: "#{Clonk.realm_admin_root(realm)}/clients",
|
34
|
-
data: defaults.merge(
|
35
|
-
clientId: name,
|
36
|
-
publicClient: public_client,
|
37
|
-
directAccessGrantsEnabled: dag_enabled
|
38
|
-
)
|
39
|
-
)
|
40
|
-
new_client_id = response.headers[:location].split('/')[-1]
|
41
|
-
new_from_id(new_client_id, realm)
|
17
|
+
# Defines a connection to SSO.
|
18
|
+
class Connection
|
19
|
+
def clients
|
20
|
+
objects(type: 'Client')
|
42
21
|
end
|
43
22
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
# You may be able to extend Clonk's functionality by using Clonk.response
|
49
|
-
# or Clonk.parsed_response with routes in the SSO API alongside this data.
|
50
|
-
|
51
|
-
def self.get_config(id, realm = REALM)
|
52
|
-
Clonk.parsed_response(
|
53
|
-
path: "#{Clonk.realm_admin_root(realm)}/clients/#{id}"
|
23
|
+
def create_client(**data)
|
24
|
+
create_object(
|
25
|
+
type: 'Client',
|
26
|
+
data: { fullScopeAllowed: false }.merge(data)
|
54
27
|
)
|
55
28
|
end
|
56
29
|
|
57
|
-
##
|
58
|
-
# Gets the config inside SSO for this client.
|
59
|
-
|
60
|
-
def config
|
61
|
-
self.class.get_config(@id, @realm)
|
62
|
-
end
|
63
|
-
|
64
|
-
##
|
65
|
-
# Creates a new Client instance from a client that exists in SSO
|
66
|
-
|
67
|
-
def self.new_from_id(id, realm = REALM)
|
68
|
-
new(get_config(id, realm), realm)
|
69
|
-
end
|
70
|
-
|
71
|
-
##
|
72
|
-
# Returns an array of the clients that exist in the given realm.
|
73
|
-
|
74
|
-
def self.all(realm: REALM)
|
75
|
-
Clonk.parsed_response(
|
76
|
-
path: "#{Clonk.realm_admin_root(realm)}/clients"
|
77
|
-
).map { |client| new_from_id(client['id'], realm) }
|
78
|
-
end
|
79
|
-
|
80
|
-
##
|
81
|
-
# Searches for clients with the given name in the given realm.
|
82
|
-
|
83
|
-
def self.where(name: nil, realm: REALM)
|
84
|
-
all(realm: realm).select { |client| client.name == name }
|
85
|
-
end
|
86
|
-
|
87
|
-
##
|
88
|
-
# Searches for exactly one client with the given name in the given realm.
|
89
|
-
|
90
|
-
def self.find_by(name: nil, realm: REALM)
|
91
|
-
where(name: name, realm: realm)&.first
|
92
|
-
end
|
93
|
-
|
94
|
-
##
|
95
|
-
# Returns the URL that can be used to fetch this client's data from the API.
|
96
|
-
|
97
|
-
def url
|
98
|
-
"#{Clonk.realm_admin_root(@realm)}/clients/#{@id}"
|
99
|
-
end
|
100
|
-
|
101
30
|
##
|
102
31
|
# Maps the given role into the scope of the client. If a user has that role,
|
103
32
|
# it will be visible in tokens given by this client during authentication.
|
33
|
+
# FIXME: Write test!
|
104
34
|
|
105
|
-
def map_scope(client
|
106
|
-
|
35
|
+
def map_scope(client:, role:)
|
36
|
+
response(
|
107
37
|
method: :post,
|
108
|
-
data: [role
|
109
|
-
path: "#{
|
38
|
+
data: [config(role)],
|
39
|
+
path: "#{url_for(client)}/scope-mappings/clients/#{role.container_id}"
|
110
40
|
)
|
111
41
|
end
|
112
42
|
|
113
|
-
##
|
114
|
-
# Creates a role within this client.
|
115
|
-
# it will be visible in tokens given by this client during authentication,
|
116
|
-
# as it is already in scope.
|
117
|
-
|
118
|
-
def create_role(realm: REALM, name: nil, description: nil, scope_param_required: false)
|
119
|
-
# TODO: Create realm roles
|
120
|
-
response = Clonk.response(method: :post,
|
121
|
-
path: "#{url}/roles",
|
122
|
-
data: {
|
123
|
-
name: name,
|
124
|
-
description: description,
|
125
|
-
scopeParamRequired: scope_param_required
|
126
|
-
})
|
127
|
-
Role.find_by(name: name, client: self)
|
128
|
-
end
|
129
|
-
|
130
43
|
##
|
131
44
|
# Lists the client's permission IDs, if permissions are enabled.
|
132
45
|
# These will be returned as either a boolean (false) if disabled,
|
133
46
|
# or a hash of permission types and IDs.
|
47
|
+
# FIXME: Move to RHSSO so that permissions can actually be used!
|
48
|
+
# FIXME: Write test!
|
134
49
|
|
135
|
-
def permissions
|
136
|
-
|
137
|
-
path: "#{
|
50
|
+
def permissions(client:)
|
51
|
+
parsed_response(
|
52
|
+
path: "#{url_for(client)}/management/permissions"
|
138
53
|
)['scopePermissions'] || false
|
139
54
|
end
|
140
55
|
|
141
56
|
##
|
142
|
-
# Enables or disables permissions for
|
57
|
+
# Enables or disables permissions for some object
|
58
|
+
# FIXME: Write test!
|
143
59
|
|
144
|
-
def set_permissions(enabled: true)
|
145
|
-
|
60
|
+
def set_permissions(object:, enabled: true)
|
61
|
+
parsed_response(
|
146
62
|
method: :put,
|
147
|
-
path: "#{
|
63
|
+
path: "#{url_for(object)}/management/permissions",
|
148
64
|
data: {
|
149
65
|
enabled: enabled
|
150
66
|
}
|
@@ -153,18 +69,12 @@ module Clonk
|
|
153
69
|
|
154
70
|
##
|
155
71
|
# Returns the client's secret
|
72
|
+
# FIXME: Write test!
|
156
73
|
|
157
|
-
def secret
|
158
|
-
|
159
|
-
path: "#{
|
74
|
+
def secret(client:)
|
75
|
+
parsed_response(
|
76
|
+
path: "#{url_for(client)}/client-secret"
|
160
77
|
)['value']
|
161
78
|
end
|
162
|
-
|
163
|
-
def delete
|
164
|
-
Clonk.response(
|
165
|
-
method: :delete,
|
166
|
-
path: url
|
167
|
-
)
|
168
|
-
end
|
169
79
|
end
|
170
|
-
end
|
80
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'client'
|
4
|
+
|
5
|
+
module Clonk
|
6
|
+
##
|
7
|
+
# Defines a connection to SSO.
|
8
|
+
class Connection
|
9
|
+
attr_writer :realm
|
10
|
+
|
11
|
+
def initialize(base_url:, realm_id:, username:, password:, client_id: nil)
|
12
|
+
@base_url = base_url
|
13
|
+
@client_id = client_id
|
14
|
+
initial_access_token(
|
15
|
+
username: username, password: password, realm_id: realm_id,
|
16
|
+
client_id: client_id
|
17
|
+
)
|
18
|
+
@realm = create_instance_of(
|
19
|
+
'Realm', parsed_response(path: "/auth/realms/#{realm_id}")
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Methods common to most/all kinds of objects in SSO
|
24
|
+
####################################################
|
25
|
+
|
26
|
+
##
|
27
|
+
# Creates an object and returns an instance of it in SSO. Wrapped for each
|
28
|
+
# type.
|
29
|
+
def create_object(
|
30
|
+
type:, path: "/#{type.downcase}s", root: realm_admin_root, data: {}
|
31
|
+
)
|
32
|
+
creation_response = response(
|
33
|
+
method: :post, path: root + path, data: data
|
34
|
+
)
|
35
|
+
create_instance_of(
|
36
|
+
type,
|
37
|
+
parsed_response(
|
38
|
+
path: creation_response.headers[:location]
|
39
|
+
)
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Returns all objects in the realm of that type. Wrapped for each type.
|
45
|
+
def objects(type:, path: "/#{type.downcase}s", root: realm_admin_root)
|
46
|
+
parsed_response(path: root + path).map do |object_response|
|
47
|
+
create_instance_of(type, object_response)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_instance_of(class_name, response)
|
52
|
+
Object.const_get('Clonk').const_get(class_name).new(response) || response
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete(object)
|
56
|
+
response(path: url_for(object), method: :delete)
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Returns the config in SSO for an object.
|
61
|
+
def config(object)
|
62
|
+
class_name = object.class.name.split('::').last.downcase + 's'
|
63
|
+
class_name = 'roles-by-id' if class_name == 'roles'
|
64
|
+
route = realm_admin_root + "/#{class_name}/#{object.id}"
|
65
|
+
parsed_response(path: route)
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Map a role to another object.
|
70
|
+
# Common to groups and users
|
71
|
+
def map_role(role:, target:)
|
72
|
+
client_path = case role.container_id
|
73
|
+
when @realm
|
74
|
+
'realm'
|
75
|
+
else
|
76
|
+
"clients/#{role.container_id}"
|
77
|
+
end
|
78
|
+
parsed_response(
|
79
|
+
method: :post, data: [config(role)],
|
80
|
+
path: "#{url_for(target)}/role-mappings/#{client_path}"
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Connection detail
|
85
|
+
####################
|
86
|
+
|
87
|
+
##
|
88
|
+
# Retrieves an initial access token for the user in the given realm.
|
89
|
+
def initial_access_token(
|
90
|
+
username: @username, password: @password, client_id: @client_id,
|
91
|
+
realm_id: @realm.name
|
92
|
+
)
|
93
|
+
@access_token = parsed_response(
|
94
|
+
method: :post,
|
95
|
+
path: "/auth/realms/#{realm_id}/protocol/openid-connect/token",
|
96
|
+
connection_params: { json: false, raise_error: true },
|
97
|
+
data: {
|
98
|
+
username: username, password: password, grant_type: 'password',
|
99
|
+
client_id: client_id
|
100
|
+
}
|
101
|
+
)['access_token']
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Defines a Faraday::Connection object linked to the SSO instance.
|
106
|
+
def connection(raise_error: true, json: true, token: @access_token)
|
107
|
+
Faraday.new(url: @base_url) do |faraday|
|
108
|
+
faraday.request(json ? :json : :url_encoded)
|
109
|
+
faraday.use Faraday::Response::RaiseError if raise_error
|
110
|
+
faraday.adapter Faraday.default_adapter
|
111
|
+
faraday.headers['Authorization'] = "Bearer #{token}" unless token.nil?
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Returns a Faraday::Response for an API call via the given method.
|
117
|
+
def response(method: :get, path: '/', data: nil, connection_params: {})
|
118
|
+
return unless %i[get post put delete].include?(method)
|
119
|
+
|
120
|
+
connection(connection_params).public_send(method, path, data)
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Returns a parsed JSON response for an API call via the given method.
|
125
|
+
# Useful in instances where only the data is necessary, and not
|
126
|
+
# HTTP status confirmation that the desired effect was caused.
|
127
|
+
def parsed_response(
|
128
|
+
method: :get, path: '/', data: nil, connection_params: {}
|
129
|
+
)
|
130
|
+
resp = response(
|
131
|
+
method: method, path: path, data: data,
|
132
|
+
connection_params: connection_params
|
133
|
+
)
|
134
|
+
|
135
|
+
JSON.parse(resp.body)
|
136
|
+
rescue JSON::ParserError
|
137
|
+
resp.body
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Returns the admin API root for the realm.
|
142
|
+
def realm_admin_root(realm = @realm)
|
143
|
+
"#{@base_url}/auth/admin/realms/#{realm&.name}"
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# Returns the URL for the given object.
|
148
|
+
# Argument is necessary as permissions are sometimes treated as policies
|
149
|
+
# within SSO for some reason, especially when fetching scopes, resources and
|
150
|
+
# policies.
|
151
|
+
# FIXME: Does not work with realms - realm_admin_root does, though.
|
152
|
+
def url_for(target, prefix: 'permision/scope')
|
153
|
+
class_name = target.class.name.split('::').last.downcase
|
154
|
+
url_for_permission(target, prefix: prefix) if class_name == 'permission'
|
155
|
+
"#{realm_admin_root}/#{class_name}s/#{target.id}"
|
156
|
+
end
|
157
|
+
|
158
|
+
def url_for_permission(permission, prefix: 'permission/scope')
|
159
|
+
client_url = url_for(
|
160
|
+
clients.find { |client| client.name == 'realm-management' }
|
161
|
+
)
|
162
|
+
"#{client_url}/authz/resource-server/#{prefix}/#{permission.id}"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
data/lib/clonk/group.rb
CHANGED
@@ -1,136 +1,60 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Clonk
|
4
|
+
# Represents a group or subgroup within SSO.
|
4
5
|
class Group
|
5
|
-
|
6
6
|
attr_accessor :id
|
7
7
|
attr_reader :name
|
8
8
|
|
9
|
-
def initialize(group_response
|
9
|
+
def initialize(group_response)
|
10
10
|
@name = group_response['name']
|
11
11
|
@id = group_response['id']
|
12
|
-
@realm = realm
|
13
|
-
end
|
14
|
-
|
15
|
-
# Gets config inside SSO for group with ID in realm
|
16
|
-
def self.get_config(id, realm = REALM)
|
17
|
-
Clonk.parsed_response(
|
18
|
-
path: "#{Clonk.realm_admin_root(realm)}/groups/#{id}"
|
19
|
-
)
|
20
|
-
end
|
21
|
-
|
22
|
-
def config
|
23
|
-
self.class.get_config(@id, @realm)
|
24
|
-
end
|
25
|
-
|
26
|
-
# Creates a new Group instance from a group that exists in SSO
|
27
|
-
def self.new_from_id(id, realm = REALM)
|
28
|
-
new(get_config(id, realm), realm)
|
29
|
-
end
|
30
|
-
|
31
|
-
# Creates a new group in SSO and casts it to an instance
|
32
|
-
def self.create(name: nil, realm: REALM)
|
33
|
-
new_group = new({ 'name' => name }, realm)
|
34
|
-
response = new_group.save(realm)
|
35
|
-
new_group.id = response.headers[:location].split('/')[-1]
|
36
|
-
new_group
|
37
|
-
end
|
38
|
-
|
39
|
-
def save(realm = REALM)
|
40
|
-
if @id
|
41
|
-
Clonk.parsed_response(
|
42
|
-
method: :put,
|
43
|
-
path: "#{Clonk.realm_admin_root(@realm)}/groups/#{@id}",
|
44
|
-
data: config.merge('name' => @name)
|
45
|
-
)
|
46
|
-
else
|
47
|
-
Clonk.response(
|
48
|
-
method: :post,
|
49
|
-
path: "#{Clonk.realm_admin_root(realm)}/groups",
|
50
|
-
data: { name: @name }
|
51
|
-
)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def create_subgroup(name: nil)
|
56
|
-
response = Clonk.parsed_response(
|
57
|
-
method: :post,
|
58
|
-
path: "#{url}/children",
|
59
|
-
data: { name: name }
|
60
|
-
)
|
61
|
-
self.class.new_from_id(response['id'], @realm)
|
62
12
|
end
|
13
|
+
end
|
63
14
|
|
64
|
-
|
65
|
-
|
66
|
-
|
15
|
+
# Defines a connection to SSO.
|
16
|
+
class Connection
|
17
|
+
# Lists groups in the realm.
|
18
|
+
def groups(user: nil)
|
19
|
+
return objects(type: 'Group') unless user
|
67
20
|
|
68
|
-
|
69
|
-
response = Clonk.parsed_response(
|
70
|
-
method: :get,
|
71
|
-
path: "#{Clonk.realm_admin_root(realm)}/groups",
|
72
|
-
)
|
73
|
-
response += response.map { |group| group['subGroups'] } if flattened
|
74
|
-
response.flatten
|
75
|
-
.map { |group| new_from_id(group['id'], realm) }
|
21
|
+
objects(type: 'Group', path: "/users/#{user.id}/groups")
|
76
22
|
end
|
77
23
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
end
|
24
|
+
# Lists subgroups of a given group.
|
25
|
+
def subgroups(group)
|
26
|
+
subgroups = config(group)['subGroups']
|
27
|
+
return [] if subgroups.nil?
|
83
28
|
|
84
|
-
|
85
|
-
where(name: name, realm: realm)&.first
|
29
|
+
subgroups.map { |subgroup| create_instance_of('Group', subgroup) }
|
86
30
|
end
|
87
31
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
92
|
-
end
|
32
|
+
# Creates a group in SSO and returns its representation as a Clonk::Group.
|
33
|
+
def create_group(**data)
|
34
|
+
return if data[:name].nil? # Breaks things in SSO!
|
93
35
|
|
94
|
-
|
95
|
-
"#{Clonk.realm_admin_root(@realm)}/groups/#{@id}"
|
36
|
+
create_object(type: 'Group', data: data)
|
96
37
|
end
|
97
38
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
path: "#{url}/role-mappings/#{client_path}"
|
39
|
+
# Creates a subgroup in SSO and returns its representation as a
|
40
|
+
# Clonk::Group.
|
41
|
+
def create_subgroup(group:, **data)
|
42
|
+
create_object(
|
43
|
+
type: 'Group', path: "/groups/#{group.id}/children", data: data
|
104
44
|
)
|
105
45
|
end
|
106
46
|
|
107
|
-
|
108
|
-
|
47
|
+
# Adds a user to a group.
|
48
|
+
def add_to_group(user:, group:)
|
49
|
+
response(
|
109
50
|
method: :put,
|
110
|
-
path: "#{user.
|
51
|
+
path: "#{realm_admin_root}/users/#{user.id}/groups/#{group.id}",
|
111
52
|
data: {
|
112
|
-
groupId: @id,
|
113
53
|
userId: user.id,
|
114
|
-
|
115
|
-
|
116
|
-
)
|
117
|
-
end
|
118
|
-
|
119
|
-
def set_permissions(enabled: true)
|
120
|
-
Clonk.parsed_response(
|
121
|
-
method: :put,
|
122
|
-
path: "#{url}/management/permissions",
|
123
|
-
data: {
|
124
|
-
enabled: enabled
|
54
|
+
groupId: group.id,
|
55
|
+
realm: @realm.name
|
125
56
|
}
|
126
57
|
)
|
127
58
|
end
|
128
|
-
|
129
|
-
def delete
|
130
|
-
Clonk.response(
|
131
|
-
method: :delete,
|
132
|
-
path: url
|
133
|
-
)
|
134
|
-
end
|
135
59
|
end
|
136
60
|
end
|
data/lib/clonk/permission.rb
CHANGED
@@ -1,82 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Clonk
|
2
4
|
class Permission
|
3
5
|
def initialize(permission_response, realm)
|
4
6
|
@id = permission_response['id']
|
5
7
|
@realm = realm
|
6
8
|
end
|
9
|
+
end
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
# policies.
|
13
|
-
|
14
|
-
def url(prefix: 'permission/scope')
|
15
|
-
client_url = Clonk::Client.find_by(realm: @realm, name: 'realm-management').url
|
16
|
-
"#{client_url}/authz/resource-server/#{prefix}/#{@id}"
|
17
|
-
end
|
18
|
-
|
19
|
-
##
|
20
|
-
# Creates a new Permission instance from a given permission ID and realm.
|
21
|
-
|
22
|
-
def self.new_from_id(id: nil, realm: REALM)
|
23
|
-
client_url = Clonk::Client.find_by(realm: realm, name: 'realm-management').url
|
24
|
-
response = Clonk.parsed_response(
|
25
|
-
path: "#{client_url}/authz/resource-server/permission/scope/#{id}"
|
26
|
-
)
|
27
|
-
new(response, realm)
|
11
|
+
# Defines a connection to SSO.
|
12
|
+
class Connection
|
13
|
+
def permissions
|
14
|
+
clients.find { |client| client.name == 'realm-management' }
|
28
15
|
end
|
29
|
-
|
30
16
|
##
|
31
|
-
# Returns the
|
32
|
-
|
33
|
-
def
|
34
|
-
|
35
|
-
path: "#{
|
17
|
+
# Returns the policy IDs associated with a permission.
|
18
|
+
# FIXME: untested!
|
19
|
+
def policies(permission)
|
20
|
+
parsed_response(
|
21
|
+
path: "#{url_for(permission, prefix: 'policy')}/associatedPolicies"
|
36
22
|
)
|
37
23
|
end
|
38
24
|
|
39
|
-
##
|
40
|
-
# Returns the policy IDs associated with this permission.
|
41
|
-
|
42
|
-
def policies
|
43
|
-
Clonk.parsed_response(
|
44
|
-
path: "#{url(prefix: 'policy')}/associatedPolicies"
|
45
|
-
).map { |policy| policy['id'] }
|
46
|
-
end
|
47
|
-
|
48
25
|
##
|
49
26
|
# Returns the resource IDs associated with this permission.
|
50
|
-
|
51
|
-
def resources
|
52
|
-
|
53
|
-
path: "#{
|
54
|
-
)
|
27
|
+
# FIXME: untested!
|
28
|
+
def resources(permission)
|
29
|
+
parsed_response(
|
30
|
+
path: "#{url_for(permission, prefix: 'policy')}/resources"
|
31
|
+
)
|
55
32
|
end
|
56
33
|
|
57
34
|
##
|
58
35
|
# Returns the scope IDs associated with this permission.
|
59
|
-
|
60
|
-
def scopes
|
61
|
-
|
62
|
-
path: "#{
|
63
|
-
)
|
36
|
+
# FIXME: untested
|
37
|
+
def scopes(permission)
|
38
|
+
parsed_response(
|
39
|
+
path: "#{url_for(permission, prefix: 'policy')}/scopes"
|
40
|
+
)
|
64
41
|
end
|
65
42
|
|
66
43
|
##
|
67
44
|
# Adds the given policy/resource/scope IDs to this permission in SSO.
|
68
|
-
|
69
|
-
def
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
45
|
+
# FIXME: untested
|
46
|
+
def update_permission(
|
47
|
+
permission:, policies: [], resources: [], scopes: []
|
48
|
+
)
|
49
|
+
data = config(permission).merge(
|
50
|
+
policies: policies(permission) + policies,
|
51
|
+
resources: resources(permission) + resources,
|
52
|
+
scopes: scopes(permission) + scopes
|
53
|
+
)
|
54
|
+
parsed_response(
|
55
|
+
path: url_for(permission),
|
77
56
|
data: data,
|
78
57
|
method: :put
|
79
58
|
)
|
80
59
|
end
|
81
60
|
end
|
82
|
-
end
|
61
|
+
end
|
data/lib/clonk/policy.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Clonk
|
2
4
|
class Policy
|
3
|
-
|
4
5
|
attr_accessor :id
|
5
6
|
attr_reader :name
|
6
7
|
|
@@ -12,6 +13,7 @@ module Clonk
|
|
12
13
|
|
13
14
|
##
|
14
15
|
# Gets config inside SSO for policy with ID in realm.
|
16
|
+
# FIXME: move to connection class
|
15
17
|
|
16
18
|
def self.get_config(id, realm = REALM)
|
17
19
|
Clonk.parsed_response(
|
@@ -19,15 +21,9 @@ module Clonk
|
|
19
21
|
)
|
20
22
|
end
|
21
23
|
|
22
|
-
##
|
23
|
-
# Gets config inside SSO for policy.
|
24
|
-
|
25
|
-
def config
|
26
|
-
self.class.get_config(@id, @realm)
|
27
|
-
end
|
28
|
-
|
29
24
|
##
|
30
25
|
# Creates a new Policy instance from a policy that exists in SSO
|
26
|
+
# FIXME: move to connection class
|
31
27
|
|
32
28
|
def self.new_from_id(id, realm = REALM)
|
33
29
|
new(get_config(id, realm), realm)
|
@@ -37,6 +33,7 @@ module Clonk
|
|
37
33
|
# Returns defaults for a policy.
|
38
34
|
# I've found no reason to override these, but then again, I'm not 100% sure
|
39
35
|
# how they work. Overrides will be added to necessary methods if requested.
|
36
|
+
# FIXME: move to connection class
|
40
37
|
|
41
38
|
def self.defaults
|
42
39
|
{
|
@@ -51,6 +48,7 @@ module Clonk
|
|
51
48
|
#--
|
52
49
|
# TODO: Expand to allow for other policy types
|
53
50
|
# TODO: Don't assume role as default type
|
51
|
+
# FIXME: move to connection class
|
54
52
|
#++
|
55
53
|
|
56
54
|
def self.define(type: :role, name: nil, objects: [], description: nil, groups_claim: nil)
|
@@ -60,16 +58,17 @@ module Clonk
|
|
60
58
|
roles: (objects.map { |role| { id: role.id, required: true } } if type == :role),
|
61
59
|
groups: (objects.map { |group| { id: group.id, extendChildren: false } } if type == :group),
|
62
60
|
groupsClaim: (groups_claim if type == :group),
|
63
|
-
clients: (objects.map
|
61
|
+
clients: (objects.map(&:id) if type == :client),
|
64
62
|
description: description
|
65
|
-
).delete_if { |
|
63
|
+
).delete_if { |_k, v| v.nil? }
|
66
64
|
end
|
67
65
|
|
68
66
|
##
|
69
67
|
# Defines and creates a policy in SSO.
|
68
|
+
# FIXME: move to connection class
|
70
69
|
|
71
70
|
def self.create(type: :role, name: nil, objects: [], description: nil, groups_claim: nil, realm: REALM)
|
72
|
-
data =
|
71
|
+
data = define(type: type, name: name, objects: objects, description: description, groups_claim: groups_claim)
|
73
72
|
realm_management_url = Clonk::Client.find_by(name: 'realm-management', realm: realm).url
|
74
73
|
Clonk.parsed_response(
|
75
74
|
method: :post,
|
@@ -78,4 +77,4 @@ module Clonk
|
|
78
77
|
)
|
79
78
|
end
|
80
79
|
end
|
81
|
-
end
|
80
|
+
end
|
data/lib/clonk/realm.rb
CHANGED
@@ -1,52 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Clonk
|
4
|
+
# Represents a realm within SSO.
|
2
5
|
class Realm
|
3
|
-
|
4
|
-
@name = realm_response['id']
|
5
|
-
end
|
6
|
-
|
7
|
-
##
|
8
|
-
# Creates a realm with the given name, returning it as a Realm object
|
9
|
-
def self.create(name: nil)
|
10
|
-
Clonk.parsed_response(
|
11
|
-
method: :post,
|
12
|
-
path: '/auth/admin/realms',
|
13
|
-
data: { id: name, realm: name, enabled: true }
|
14
|
-
)
|
15
|
-
new_from_id(id: name)
|
16
|
-
end
|
6
|
+
attr_reader :name
|
17
7
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def self.all
|
22
|
-
Clonk.parsed_response(
|
23
|
-
path: '/auth/admin/realms'
|
24
|
-
).map { |realm| new_from_id(id: realm['id'])}
|
8
|
+
def initialize(realm_response)
|
9
|
+
@name = realm_response['realm'] || realm_response['id']
|
25
10
|
end
|
11
|
+
end
|
26
12
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def
|
31
|
-
|
32
|
-
path: "/auth/admin/realms/#{name}"
|
33
|
-
)
|
13
|
+
# Defines a connection to SSO.
|
14
|
+
class Connection
|
15
|
+
# Lists all realms in SSO.
|
16
|
+
def realms
|
17
|
+
objects(type: 'Realm', path: '', root: realm_admin_root(nil))
|
34
18
|
end
|
35
19
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
20
|
+
# Creates a new realm with the given data.
|
21
|
+
def create_realm(**data)
|
22
|
+
create_object(
|
23
|
+
type: 'Realm',
|
24
|
+
path: '',
|
25
|
+
root: realm_admin_root(nil),
|
26
|
+
data: { enabled: true, id: data['realm'] }.merge(data)
|
42
27
|
)
|
43
28
|
end
|
44
|
-
|
45
|
-
##
|
46
|
-
# Creates a new Realm object from a given realm ID.
|
47
|
-
|
48
|
-
def self.new_from_id(id: nil)
|
49
|
-
new(find_by(name: id))
|
50
|
-
end
|
51
29
|
end
|
52
30
|
end
|
data/lib/clonk/role.rb
CHANGED
@@ -1,74 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Clonk
|
4
|
+
# Represents a role within SSO.
|
2
5
|
class Role
|
3
6
|
attr_accessor :id
|
4
7
|
attr_accessor :container_id
|
5
8
|
attr_reader :name
|
6
9
|
|
7
|
-
def initialize(role_response
|
10
|
+
def initialize(role_response)
|
8
11
|
@id = role_response['id']
|
9
|
-
@realm = realm
|
10
12
|
@container_id = role_response['containerId']
|
11
13
|
@name = role_response['name']
|
12
14
|
end
|
15
|
+
end
|
13
16
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
# FIXME: target_type should really be inherited from classname of incoming
|
19
|
-
# object
|
20
|
-
#++
|
21
|
-
|
22
|
-
def self.all(client: nil, target: nil, target_type: nil, realm: REALM)
|
23
|
-
# need this to work with realms too
|
24
|
-
case target
|
25
|
-
when nil
|
26
|
-
path = "#{Clonk.realm_admin_root(realm)}/clients/#{client.id}/roles"
|
27
|
-
else
|
28
|
-
path = "#{Clonk.realm_admin_root(realm)}/#{target_type}s/#{target.id}/role-mappings/clients/#{client.id}/available"
|
29
|
-
end
|
30
|
-
Clonk.parsed_response(
|
31
|
-
path: path
|
32
|
-
).map { |role| new_from_id(role['id'], realm) }
|
33
|
-
end
|
34
|
-
|
35
|
-
##
|
36
|
-
# Returns all roles with the given name.
|
37
|
-
|
38
|
-
def self.where(client: nil, target: nil, target_type: nil, realm: REALM, name: nil)
|
39
|
-
all(client: client, target: target, target_type: target_type, realm: realm)
|
40
|
-
.select { |role| role.name == name }
|
41
|
-
end
|
42
|
-
|
43
|
-
|
44
|
-
##
|
45
|
-
# Returns the first role with the given name.
|
46
|
-
|
47
|
-
def self.find_by(client: nil, target: nil, target_type: nil, realm: REALM, name: nil)
|
48
|
-
where(client: client, target: target, target_type: target_type, realm: realm, name: name)&.first
|
49
|
-
end
|
50
|
-
|
51
|
-
##
|
52
|
-
# Gets config inside SSO for role with ID in realm
|
53
|
-
|
54
|
-
def self.get_config(id, realm = REALM)
|
55
|
-
Clonk.parsed_response(
|
56
|
-
path: "#{Clonk.realm_admin_root(realm)}/roles-by-id/#{id}"
|
57
|
-
)
|
58
|
-
end
|
59
|
-
|
60
|
-
##
|
61
|
-
# Gets config inside SSO for this role
|
62
|
-
|
63
|
-
def config
|
64
|
-
self.class.get_config(@id, @realm)
|
17
|
+
# Defines a connection to SSO.
|
18
|
+
class Connection
|
19
|
+
def roles(client:)
|
20
|
+
objects(type: 'Role', root: url_for(client))
|
65
21
|
end
|
66
22
|
|
67
23
|
##
|
68
|
-
# Creates a
|
24
|
+
# Creates a role within the given client.
|
25
|
+
# it will be visible in tokens given by this client during authentication,
|
26
|
+
# as it is already in scope.
|
69
27
|
|
70
|
-
def
|
71
|
-
|
28
|
+
def create_role(client:, **data)
|
29
|
+
create_object(type: 'Role', root: url_for(client), data: data)
|
72
30
|
end
|
73
31
|
end
|
74
|
-
end
|
32
|
+
end
|
data/lib/clonk/user.rb
CHANGED
@@ -1,114 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Clonk
|
4
|
+
##
|
5
|
+
# Represents a user in SSO.
|
2
6
|
class User
|
3
7
|
attr_accessor :id
|
4
8
|
attr_reader :username
|
5
9
|
|
6
|
-
def initialize(user_response
|
10
|
+
def initialize(user_response)
|
7
11
|
@username = user_response['username']
|
8
12
|
@id = user_response['id']
|
9
|
-
@realm = realm
|
10
|
-
end
|
11
|
-
|
12
|
-
##
|
13
|
-
# Gets config inside SSO for user with ID in realm
|
14
|
-
|
15
|
-
def self.get_config(id, realm = REALM)
|
16
|
-
Clonk.parsed_response(
|
17
|
-
path: "#{Clonk.realm_admin_root(realm)}/users/#{id}"
|
18
|
-
)
|
19
|
-
end
|
20
|
-
|
21
|
-
##
|
22
|
-
# Gets config inside SSO for this user
|
23
|
-
|
24
|
-
def config
|
25
|
-
self.class.get_config(@id, @realm)
|
26
13
|
end
|
14
|
+
end
|
27
15
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def
|
32
|
-
|
33
|
-
end
|
34
|
-
|
35
|
-
##
|
36
|
-
# Creates a user in SSO, returning a User instance with their ID and
|
37
|
-
# username
|
38
|
-
|
39
|
-
def self.create(realm: REALM, username: nil, enabled: true)
|
40
|
-
response = Clonk.response(method: :post,
|
41
|
-
path: "#{Clonk.realm_admin_root(realm)}/users",
|
42
|
-
data: { username: username, enabled: enabled }
|
43
|
-
)
|
44
|
-
self.new_from_id(response.headers[:location].split('/')[-1], realm)
|
45
|
-
end
|
46
|
-
|
47
|
-
##
|
48
|
-
# Returns all users in the given realm
|
49
|
-
|
50
|
-
def self.all(realm: REALM)
|
51
|
-
Clonk.parsed_response(
|
52
|
-
path: "#{Clonk.realm_admin_root(realm)}/users"
|
53
|
-
).map { |user| new_from_id(user['id'], realm) }
|
54
|
-
end
|
55
|
-
|
56
|
-
##
|
57
|
-
# Returns all users in the given realm with the given username
|
58
|
-
|
59
|
-
def self.where(username: nil, realm: REALM)
|
60
|
-
all(realm: realm).select { |user| user.username == username }
|
61
|
-
end
|
62
|
-
|
63
|
-
##
|
64
|
-
# returns a user in the given realm with the given username
|
65
|
-
|
66
|
-
def self.find_by(username: nil, realm: REALM)
|
67
|
-
where(username: username, realm: realm)&.first
|
68
|
-
end
|
69
|
-
|
70
|
-
##
|
71
|
-
# Returns the API URL from which the user is accessible.
|
72
|
-
|
73
|
-
def url
|
74
|
-
"#{Clonk.realm_admin_root(@realm)}/users/#{@id}"
|
16
|
+
# Defines a connection to SSO.
|
17
|
+
class Connection
|
18
|
+
# Lists all users in the realm.
|
19
|
+
def users
|
20
|
+
objects(type: 'User')
|
75
21
|
end
|
76
22
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
def map_role(role: nil)
|
81
|
-
client_path = role.container_id == @realm ? 'realm' : "clients/#{role.container_id}"
|
82
|
-
response = Clonk.parsed_response(
|
83
|
-
method: :post,
|
84
|
-
data: [role.config],
|
85
|
-
path: "#{url}/role-mappings/#{client_path}"
|
86
|
-
)
|
23
|
+
# Creates a new user in SSO and returns its representation as a Clonk::User.
|
24
|
+
def create_user(**data)
|
25
|
+
create_object(type: 'User', data: { enabled: true }.merge(data))
|
87
26
|
end
|
88
27
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
# FIXME: Currently always a permanent password Make that temporary flag do things.
|
93
|
-
#++
|
94
|
-
|
95
|
-
def set_password(password: nil, temporary: false)
|
96
|
-
Clonk.parsed_response(
|
28
|
+
# Sets the password for a user.
|
29
|
+
def set_password_for(user:, password: nil, temporary: false)
|
30
|
+
response(
|
97
31
|
method: :put,
|
98
32
|
data: {
|
99
33
|
type: 'password',
|
100
34
|
value: password,
|
101
|
-
temporary:
|
35
|
+
temporary: temporary
|
102
36
|
},
|
103
|
-
path: "#{
|
104
|
-
)
|
105
|
-
end
|
106
|
-
|
107
|
-
def delete
|
108
|
-
Clonk.response(
|
109
|
-
method: :delete,
|
110
|
-
path: url
|
37
|
+
path: "#{url_for(user)}/reset-password"
|
111
38
|
)
|
112
39
|
end
|
113
40
|
end
|
114
|
-
end
|
41
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clonk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Simon Fish
|
@@ -60,6 +60,7 @@ extra_rdoc_files: []
|
|
60
60
|
files:
|
61
61
|
- lib/clonk.rb
|
62
62
|
- lib/clonk/client.rb
|
63
|
+
- lib/clonk/connection.rb
|
63
64
|
- lib/clonk/group.rb
|
64
65
|
- lib/clonk/permission.rb
|
65
66
|
- lib/clonk/policy.rb
|