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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 745227bb1c30916ca73bcb74eb7a6f5b41389e635810b51d04a09dc4fe52d870
4
- data.tar.gz: c94c13fed88b877aa0c5c663ed81cdf6df6571e5974399921b0d108836219bfb
3
+ metadata.gz: 8f8dab272e5def2a4916f0865f6829f47345371716ac09c235e4fdc9411127e7
4
+ data.tar.gz: f3dbefc497d92a22dd31ea11fd31482fb2b94fdc39b229eec6c902e8995f6faf
5
5
  SHA512:
6
- metadata.gz: '0813ad9d28ca9d8af7d5fc70c0e8fb40ec0a98068d083b54cb153ae170cdcbb03b95406b962ca8eb820a4f23d2e06d93a8639c42f5690130eb54233b919398e2'
7
- data.tar.gz: 2a2393987b54c02da7596651e95587d204d92f9db6b0c54fc2673e738f9bb035fc4d25662641e79dfcbf4595e7fb0b7b32c48c0dfaca771947856d419ffa989e
6
+ metadata.gz: 703d6fa690a791364af18cc73c2b8eb391147cd3789683139203b5dec7809f1024f0478d82bd06fc673830b3b17d4b7215ff3be3debc7e625b70720505270ad2
7
+ data.tar.gz: a44c4a23a58c00d0eb43283013a9f4dd8de14900e112b1d779818ca67e86411abf974f200f89c87d45498437ada9aaa41024fe665a122496a5098223170ccf76
@@ -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 = URI.encode(ENV.fetch('SSO_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'
@@ -1,150 +1,66 @@
1
- module Clonk
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 authenticate against SSO with their credentials.
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, realm)
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
- # Returns the defaults for some fields. The rest should be defined on a relevant call.
18
-
19
- def self.defaults
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
- # Gets the config inside SSO for a particular client.
46
- # This allows for access to many attributes that Clonk might not directly
47
- # handle yet.
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: nil, role: nil, realm: REALM)
106
- Clonk.parsed_response(
35
+ def map_scope(client:, role:)
36
+ response(
107
37
  method: :post,
108
- data: [role.config],
109
- path: "#{url}/scope-mappings/clients/#{client.id}"
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
- Clonk.parsed_response(
137
- path: "#{url}/management/permissions"
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 a client
57
+ # Enables or disables permissions for some object
58
+ # FIXME: Write test!
143
59
 
144
- def set_permissions(enabled: true)
145
- Clonk.parsed_response(
60
+ def set_permissions(object:, enabled: true)
61
+ parsed_response(
146
62
  method: :put,
147
- path: "#{url}/management/permissions",
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
- Clonk.parsed_response(
159
- path: "#{url}/client-secret"
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
@@ -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, realm = REALM)
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
- def subgroups
65
- config["subGroups"].map { |group| self.class.new_from_id(group['id'], @realm) }
66
- end
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
- def self.all(realm: REALM, flattened: false)
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
- def self.where(name: nil, realm: REALM)
79
- all(flattened: true, realm: realm)
80
- .select { |group| group['name'] == name }
81
- .map { |group| new_from_id(group['id'], realm) }
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
- def self.find_by(name: nil, realm: REALM)
85
- where(name: name, realm: realm)&.first
29
+ subgroups.map { |subgroup| create_instance_of('Group', subgroup) }
86
30
  end
87
31
 
88
- def self.find(id: nil, realm: REALM)
89
- if all.find { |group| group['id'] == id }
90
- new_from_id(id, realm)
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
- def url
95
- "#{Clonk.realm_admin_root(@realm)}/groups/#{@id}"
36
+ create_object(type: 'Group', data: data)
96
37
  end
97
38
 
98
- def map_role(role: nil)
99
- client_path = role.container_id == @realm ? 'realm' : "clients/#{role.container_id}"
100
- response = Clonk.parsed_response(
101
- method: :post,
102
- data: [role.config],
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
- def add_user(user: nil, realm: REALM)
108
- Clonk.parsed_response(
47
+ # Adds a user to a group.
48
+ def add_to_group(user:, group:)
49
+ response(
109
50
  method: :put,
110
- path: "#{user.url}/groups/#{@id}",
51
+ path: "#{realm_admin_root}/users/#{user.id}/groups/#{group.id}",
111
52
  data: {
112
- groupId: @id,
113
53
  userId: user.id,
114
- realm: @realm
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
@@ -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
- # Returns the API URL for this permission.
10
- # Argument is necessary as permissions are sometimes treated as policies
11
- # within SSO for some reason, especially when fetching scopes, resources and
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 config within SSO for this permission.
32
-
33
- def config
34
- Clonk.parsed_response(
35
- path: "#{url}"
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
- Clonk.parsed_response(
53
- path: "#{url(prefix: 'policy')}/resources"
54
- ).map { |resource| resource['_id'] }
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
- Clonk.parsed_response(
62
- path: "#{url(prefix: 'policy')}/scopes"
63
- ).map { |scope| scope['id'] }
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 update(policies: [], resources: [], scopes: [])
70
- data = config.merge(
71
- policies: self.policies + policies,
72
- resources: self.resources + resources,
73
- scopes: self.scopes + scopes
74
- )
75
- Clonk.parsed_response(
76
- path: url,
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
@@ -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 { |client| client.id } if type == :client),
61
+ clients: (objects.map(&:id) if type == :client),
64
62
  description: description
65
- ).delete_if { |k,v| v.nil?}
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 = self.define(type: type, name: name, objects: objects, description: description, groups_claim: groups_claim)
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
@@ -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
- def initialize(realm_response)
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
- # Returns all realms in this instance of SSO.
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
- # Returns the realm with the given name.
29
-
30
- def self.find_by(name: nil)
31
- Clonk.parsed_response(
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
- # Gets the config for this realm in SSO.
38
-
39
- def config
40
- Clonk.parsed_response(
41
- path: "/auth/admin/realms/#{@name}"
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
@@ -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, realm)
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
- # Returns all roles for the given client. Can also return all roles
16
- # applicable to a specific target object.
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 new Role instance from a role that exists in SSO
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 self.new_from_id(id, realm = REALM)
71
- new(get_config(id, realm), realm)
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
@@ -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, realm)
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
- # Creates a new User instance from a user that exists in SSO
30
-
31
- def self.new_from_id(id, realm = REALM)
32
- new(get_config(id, realm), realm)
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
- # Maps a role to a user.
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
- # Sets the user's password.
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: false
35
+ temporary: temporary
102
36
  },
103
- path: "#{url}/reset-password"
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: 1.0.0
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