clonk 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|