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 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