clonk 1.0.0alpha3 → 1.0.0alpha4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardoc/checksums +8 -0
- data/.yardoc/complete +0 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/Gemfile.lock +24 -0
- data/README.md +59 -0
- data/clonk.gemspec +15 -0
- data/lib/clonk/client.rb +162 -0
- data/lib/clonk/group.rb +129 -0
- data/lib/clonk/permission.rb +82 -0
- data/lib/clonk/policy.rb +81 -0
- data/lib/clonk/realm.rb +52 -0
- data/lib/clonk/role.rb +74 -0
- data/lib/clonk/user.rb +107 -0
- metadata +25 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f7aa0b12c57241a41d20426c06220f03c74247ae3d685c25a1062052ed2297a
|
4
|
+
data.tar.gz: 16fe259d548ca99777ca99442894377c2ef99bce191ce069af0c507afd85df78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 794fd86cc27312327b5b4f42739d151d29dc3c489d28b215c28cca805502e35f0a87ed64447bf358e1791a5327e45fa4e8de814574206bc869d0cc1b542b67e6
|
7
|
+
data.tar.gz: 7a0643e05af7c708f404194d310ab6301848fa1054f8cee66b6344bbd5613ca6015dfc9145cc9dc13bd2bbbabc0bc07c83920c77444adc80b1a1bf0639d1fc52
|
data/.yardoc/checksums
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
lib/clonk.rb 20e9972dadb66fb1771722d9b06e3ccdad9e1022
|
2
|
+
lib/clonk/role.rb 56ea266896bcb2b45cc48ee633c83639d2135296
|
3
|
+
lib/clonk/user.rb a7da95bf320c5aaa7cc7708d9fe2ffd10f4eda23
|
4
|
+
lib/clonk/group.rb 29edd3c17b7babce87bb544333d42455ec1a5c2e
|
5
|
+
lib/clonk/realm.rb 0f933b0fd76545c049a5cb228706f13117fbcbb3
|
6
|
+
lib/clonk/client.rb 41af98a3b30b4adff27b5d62c6b26559335b369f
|
7
|
+
lib/clonk/policy.rb 8b8840f6763a1a320759a3c9d052758f25b17463
|
8
|
+
lib/clonk/permission.rb 4466f7c1d3bef61521df03ee0b92c85f7be6cb66
|
data/.yardoc/complete
ADDED
File without changes
|
Binary file
|
Binary file
|
data/.yardoc/proxy_types
ADDED
Binary file
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
clonk (1.0.0alpha3)
|
5
|
+
faraday
|
6
|
+
faraday_middleware
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
faraday (0.15.3)
|
12
|
+
multipart-post (>= 1.2, < 3)
|
13
|
+
faraday_middleware (0.12.2)
|
14
|
+
faraday (>= 0.7.4, < 1.0)
|
15
|
+
multipart-post (2.0.0)
|
16
|
+
|
17
|
+
PLATFORMS
|
18
|
+
ruby
|
19
|
+
|
20
|
+
DEPENDENCIES
|
21
|
+
clonk!
|
22
|
+
|
23
|
+
BUNDLED WITH
|
24
|
+
1.16.2
|
data/README.md
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# Clonk [![Gem Version](https://badge.fury.io/rb/clonk.svg)](https://badge.fury.io/rb/clonk)
|
2
|
+
|
3
|
+
This is a gem that'll help you seed an instance of Red Hat SSO. Right now, it's still a work-in-progress, but it shouldn't be long before it's got everything I think it needs.
|
4
|
+
|
5
|
+
It makes some assumptions about what's in your environment at the moment, using `dotenv`. While developing, I'm keeping the following variables in there.
|
6
|
+
|
7
|
+
```
|
8
|
+
SSO_REALM="master"
|
9
|
+
SSO_BASE_URL="http://localhost:8080/auth"
|
10
|
+
SSO_USERNAME="user"
|
11
|
+
SSO_PASSWORD="password"
|
12
|
+
```
|
13
|
+
|
14
|
+
It's recommended to spin up an SSO instance alongside this to see what effects you're having on it. Here, it's important that the `preview` profile is used, so that we can use the `realm-management` client to take care of permissions in the realm.
|
15
|
+
|
16
|
+
```
|
17
|
+
docker run --rm -p 8080:8080 -e JAVA_OPTS_APPEND="-Dkeycloak.profile=preview" -e SSO_ADMIN_USERNAME=user -e SSO_ADMIN_PASSWORD=password registry.access.redhat.com/redhat-sso-7/sso72-openshift
|
18
|
+
```
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
Clonk exposes SSO objects as ActiveRecord-esque models. As a short demonstration:
|
23
|
+
|
24
|
+
```
|
25
|
+
2.5.1 :003 > Clonk::Group.all
|
26
|
+
=> [#<Clonk::Group:0x00007fe7139ecda0 @name="another-test", @id="42b28060-7f4f-4b6d-82fd-6af031881a9e", @realm="master">, #<Clonk::Group:0x00007fe713808228 @name="chaos-chaos", @id="05af3f68-44d6-4973-8834-e957822e43ef", @realm="master">]
|
27
|
+
2.5.1 :004 > Clonk::Group.all.first.config
|
28
|
+
=> {"id"=>"42b28060-7f4f-4b6d-82fd-6af031881a9e", "name"=>"another-test", "path"=>"/another-test", "attributes"=>{}, "realmRoles"=>[], "clientRoles"=>{}, "subGroups"=>[], "access"=>{"view"=>true, "manage"=>true, "manageMembership"=>true}}
|
29
|
+
```
|
30
|
+
|
31
|
+
`Group.all` casts all groups in the realm to `Clonk::Group` objects...
|
32
|
+
|
33
|
+
...but you can access their plain JSON config with `Group#config`.
|
34
|
+
|
35
|
+
Right now, you can check the `lib` folder for details of which methods are exposed, but I'm looking to add documentation in the very near future. Think of this as a soft launch.
|
36
|
+
|
37
|
+
## Why?
|
38
|
+
|
39
|
+
When developing against Red Hat SSO and Keycloak, I've personally struggled to deal with the documentation. There are a lot of assumptions that are made about what you know and what you don't. So I've made this gem...
|
40
|
+
|
41
|
+
### ...to help devs use SSO platforms with their Rails apps
|
42
|
+
|
43
|
+
Sometimes, Devise just doesn't cut it, especially if you want to allow users to sign into multiple apps with the same credentials. Running a SSO instance that all your apps can call off to can make things more flexible!
|
44
|
+
|
45
|
+
### ...to document API endpoints that are documented either confusingly or not at all
|
46
|
+
|
47
|
+
SSO is a huge project, so it's sort of understandable that perhaps its documentation isn't too easy to understand at first glance...especially if you've never used an SSO backend before. Some API endpoints that'll be used in this gem aren't even documented.
|
48
|
+
|
49
|
+
### ...to better integrate Ruby/Rails with SSO
|
50
|
+
|
51
|
+
I guess this comes back to the first one. SSO integration with Rails apps could be really powerful. Especially if it's wrapped in that familiar ActiveRecord style.
|
52
|
+
|
53
|
+
### ...to make seeding SSO a lot more readable in future
|
54
|
+
|
55
|
+
I seeded SSO with more `curl` requests than you could shake a stick at. Suffice to say, it didn't look like the prettiest piece of code...
|
56
|
+
|
57
|
+
### ...to transfer what I've learned
|
58
|
+
|
59
|
+
This gem goes hand-in-hand with a blog post I'm writing, but I'm hoping it'll go further than just that tutorial. Fingers crossed this gem will have its tendrils deep in a lot of SSO instances out there...which sounds a little ominous, but...yeah, you get the gist!
|
data/clonk.gemspec
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = %q{clonk}
|
6
|
+
s.version = "1.0.0alpha4"
|
7
|
+
s.authors = ["Simon Fish"]
|
8
|
+
s.date = %q{2018-11-19}
|
9
|
+
s.summary = %q{Keycloak/RHSSO admin API client}
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.require_paths = Dir.glob("lib/**/*")
|
12
|
+
s.add_runtime_dependency 'faraday'
|
13
|
+
s.add_runtime_dependency 'faraday_middleware'
|
14
|
+
s.metadata["yard.run"] = "yardoc"
|
15
|
+
end
|
data/lib/clonk/client.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
module Clonk
|
2
|
+
|
3
|
+
##
|
4
|
+
# This class represents a client within SSO. A client allows a user to authenticate against SSO with their credentials.
|
5
|
+
|
6
|
+
class Client
|
7
|
+
attr_accessor :id
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(clients_response, realm)
|
11
|
+
@id = clients_response['id']
|
12
|
+
@name = clients_response['clientId']
|
13
|
+
@realm = realm
|
14
|
+
end
|
15
|
+
|
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
|
+
protocol: :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)
|
42
|
+
end
|
43
|
+
|
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}"
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
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
|
+
##
|
102
|
+
# Maps the given role into the scope of the client. If a user has that role,
|
103
|
+
# it will be visible in tokens given by this client during authentication.
|
104
|
+
|
105
|
+
def map_scope(client: nil, role: nil, realm: REALM)
|
106
|
+
Clonk.parsed_response(
|
107
|
+
protocol: :post,
|
108
|
+
data: [role.config],
|
109
|
+
path: "#{url}/scope-mappings/clients/#{client.id}"
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
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
|
+
Clonk.parsed_response(protocol: :post,
|
121
|
+
path: "#{url}/roles",
|
122
|
+
data: {
|
123
|
+
name: name,
|
124
|
+
description: description,
|
125
|
+
scopeParamRequired: scope_param_required
|
126
|
+
})
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Lists the client's permission IDs, if permissions are enabled.
|
131
|
+
# These will be returned as either a boolean (false) if disabled,
|
132
|
+
# or a hash of permission types and IDs.
|
133
|
+
|
134
|
+
def permissions
|
135
|
+
Clonk.parsed_response(
|
136
|
+
path: "#{url}/management/permissions"
|
137
|
+
)['scopePermissions'] || false
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Enables or disables permissions for a client
|
142
|
+
|
143
|
+
def set_permissions(enabled: true)
|
144
|
+
Clonk.parsed_response(
|
145
|
+
protocol: :put,
|
146
|
+
path: "#{url}/management/permissions",
|
147
|
+
data: {
|
148
|
+
enabled: enabled
|
149
|
+
}
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Returns the client's secret
|
155
|
+
|
156
|
+
def secret
|
157
|
+
Clonk.parsed_response(
|
158
|
+
path: "#{url}/client-secret"
|
159
|
+
)['value']
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
data/lib/clonk/group.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clonk
|
4
|
+
class Group
|
5
|
+
|
6
|
+
attr_accessor :id
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
def initialize(group_response, realm = REALM)
|
10
|
+
@name = group_response['name']
|
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
|
+
protocol: :put,
|
43
|
+
path: "#{Clonk.realm_admin_root(@realm)}/groups/#{@id}",
|
44
|
+
data: config.merge('name' => @name)
|
45
|
+
)
|
46
|
+
else
|
47
|
+
Clonk.response(
|
48
|
+
protocol: :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
|
+
protocol: :post,
|
58
|
+
path: "#{url}/children",
|
59
|
+
data: { name: name }
|
60
|
+
)
|
61
|
+
self.class.new_from_id(response['id'], @realm)
|
62
|
+
end
|
63
|
+
|
64
|
+
def subgroups
|
65
|
+
config["subGroups"].map { |group| self.class.new_from_id(group['id'], @realm) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.all(realm: REALM, flattened: false)
|
69
|
+
response = Clonk.parsed_response(
|
70
|
+
protocol: :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) }
|
76
|
+
end
|
77
|
+
|
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
|
83
|
+
|
84
|
+
def self.find_by(name: nil, realm: REALM)
|
85
|
+
where(name: name, realm: realm)&.first
|
86
|
+
end
|
87
|
+
|
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
|
93
|
+
|
94
|
+
def url
|
95
|
+
"#{Clonk.realm_admin_root(@realm)}/groups/#{@id}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def map_role(role: nil)
|
99
|
+
client_path = role.container_id == @realm ? 'realm' : "clients/#{role.container_id}"
|
100
|
+
response = Clonk.parsed_response(
|
101
|
+
protocol: :post,
|
102
|
+
data: [role.config],
|
103
|
+
path: "#{url}/role-mappings/#{client_path}"
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_user(user: nil, realm: REALM)
|
108
|
+
Clonk.parsed_response(
|
109
|
+
protocol: :put,
|
110
|
+
path: "#{user.url}/groups/#{@id}",
|
111
|
+
data: {
|
112
|
+
groupId: @id,
|
113
|
+
userId: user.id,
|
114
|
+
realm: @realm
|
115
|
+
}
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_permissions(enabled: true)
|
120
|
+
Clonk.parsed_response(
|
121
|
+
protocol: :put,
|
122
|
+
path: "#{url}/management/permissions",
|
123
|
+
data: {
|
124
|
+
enabled: enabled
|
125
|
+
}
|
126
|
+
)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Clonk
|
2
|
+
class Permission
|
3
|
+
def initialize(permission_response, realm)
|
4
|
+
@id = permission_response['id']
|
5
|
+
@realm = realm
|
6
|
+
end
|
7
|
+
|
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)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Returns the config within SSO for this permission.
|
32
|
+
|
33
|
+
def config
|
34
|
+
Clonk.parsed_response(
|
35
|
+
path: "#{url}"
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
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
|
+
##
|
49
|
+
# 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'] }
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# 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'] }
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# 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,
|
77
|
+
data: data,
|
78
|
+
protocol: :put
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/clonk/policy.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Clonk
|
2
|
+
class Policy
|
3
|
+
|
4
|
+
attr_accessor :id
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(policy_response, realm)
|
8
|
+
@id = policy_response['id']
|
9
|
+
@name = policy_response['name']
|
10
|
+
@realm = realm
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Gets config inside SSO for policy with ID in realm.
|
15
|
+
|
16
|
+
def self.get_config(id, realm = REALM)
|
17
|
+
Clonk.parsed_response(
|
18
|
+
path: "#{Clonk.realm_admin_root(realm)}/clients/#{Clonk::Client.find_by(name: 'realm-management').id}/authz/resource-server/policy/role/#{id}"
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Gets config inside SSO for policy.
|
24
|
+
|
25
|
+
def config
|
26
|
+
self.class.get_config(@id, @realm)
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Creates a new Policy instance from a policy that exists in SSO
|
31
|
+
|
32
|
+
def self.new_from_id(id, realm = REALM)
|
33
|
+
new(get_config(id, realm), realm)
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Returns defaults for a policy.
|
38
|
+
# I've found no reason to override these, but then again, I'm not 100% sure
|
39
|
+
# how they work. Overrides will be added to necessary methods if requested.
|
40
|
+
|
41
|
+
def self.defaults
|
42
|
+
{
|
43
|
+
logic: 'POSITIVE',
|
44
|
+
decisionStrategy: 'UNANIMOUS'
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Returns a policy definition that can then be used to create a policy in SSO.
|
50
|
+
# Only defines role, group and client policies
|
51
|
+
#--
|
52
|
+
# TODO: Expand to allow for other policy types
|
53
|
+
# TODO: Don't assume role as default type
|
54
|
+
#++
|
55
|
+
|
56
|
+
def self.define(type: :role, name: nil, objects: [], description: nil, groups_claim: nil)
|
57
|
+
defaults.merge(
|
58
|
+
type: type,
|
59
|
+
name: name,
|
60
|
+
roles: (objects.map { |role| { id: role.id, required: true } } if type == :role),
|
61
|
+
groups: (objects.map { |group| { id: group.id, extendChildren: false } } if type == :group),
|
62
|
+
groupsClaim: (groups_claim if type == :group),
|
63
|
+
clients: (objects.map { |client| client.id } if type == :client),
|
64
|
+
description: description
|
65
|
+
).delete_if { |k,v| v.nil?}
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Defines and creates a policy in SSO.
|
70
|
+
|
71
|
+
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)
|
73
|
+
realm_management_url = Clonk::Client.find_by(name: 'realm-management', realm: realm).url
|
74
|
+
Clonk.parsed_response(
|
75
|
+
protocol: :post,
|
76
|
+
path: "#{realm_management_url}/authz/resource-server/policy/#{type}",
|
77
|
+
data: data
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/clonk/realm.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Clonk
|
2
|
+
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
|
+
protocol: :post,
|
12
|
+
path: '/auth/admin/realms',
|
13
|
+
data: { id: name, realm: name, enabled: true }
|
14
|
+
)
|
15
|
+
new_from_id(id: name)
|
16
|
+
end
|
17
|
+
|
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'])}
|
25
|
+
end
|
26
|
+
|
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
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Gets the config for this realm in SSO.
|
38
|
+
|
39
|
+
def config
|
40
|
+
Clonk.parsed_response(
|
41
|
+
path: "/auth/admin/realms/#{@name}"
|
42
|
+
)
|
43
|
+
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
|
+
end
|
52
|
+
end
|
data/lib/clonk/role.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
module Clonk
|
2
|
+
class Role
|
3
|
+
attr_accessor :id
|
4
|
+
attr_accessor :container_id
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(role_response, realm)
|
8
|
+
@id = role_response['id']
|
9
|
+
@realm = realm
|
10
|
+
@container_id = role_response['containerId']
|
11
|
+
@name = role_response['name']
|
12
|
+
end
|
13
|
+
|
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)
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Creates a new Role instance from a role that exists in SSO
|
69
|
+
|
70
|
+
def self.new_from_id(id, realm = REALM)
|
71
|
+
new(get_config(id, realm), realm)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/clonk/user.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
module Clonk
|
2
|
+
class User
|
3
|
+
attr_accessor :id
|
4
|
+
attr_reader :username
|
5
|
+
|
6
|
+
def initialize(user_response, realm)
|
7
|
+
@username = user_response['username']
|
8
|
+
@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
|
+
end
|
27
|
+
|
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(protocol: :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}"
|
75
|
+
end
|
76
|
+
|
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
|
+
protocol: :post,
|
84
|
+
data: [role.config],
|
85
|
+
path: "#{url}/role-mappings/#{client_path}"
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
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(
|
97
|
+
protocol: :put,
|
98
|
+
data: {
|
99
|
+
type: 'password',
|
100
|
+
value: password,
|
101
|
+
temporary: false
|
102
|
+
},
|
103
|
+
path: "#{url}/reset-password"
|
104
|
+
)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
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.
|
4
|
+
version: 1.0.0alpha4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Simon Fish
|
@@ -44,8 +44,23 @@ executables: []
|
|
44
44
|
extensions: []
|
45
45
|
extra_rdoc_files: []
|
46
46
|
files:
|
47
|
+
- ".yardoc/checksums"
|
48
|
+
- ".yardoc/complete"
|
49
|
+
- ".yardoc/object_types"
|
50
|
+
- ".yardoc/objects/root.dat"
|
51
|
+
- ".yardoc/proxy_types"
|
47
52
|
- Gemfile
|
53
|
+
- Gemfile.lock
|
54
|
+
- README.md
|
55
|
+
- clonk.gemspec
|
48
56
|
- lib/clonk.rb
|
57
|
+
- lib/clonk/client.rb
|
58
|
+
- lib/clonk/group.rb
|
59
|
+
- lib/clonk/permission.rb
|
60
|
+
- lib/clonk/policy.rb
|
61
|
+
- lib/clonk/realm.rb
|
62
|
+
- lib/clonk/role.rb
|
63
|
+
- lib/clonk/user.rb
|
49
64
|
homepage:
|
50
65
|
licenses: []
|
51
66
|
metadata:
|
@@ -53,7 +68,15 @@ metadata:
|
|
53
68
|
post_install_message:
|
54
69
|
rdoc_options: []
|
55
70
|
require_paths:
|
56
|
-
- lib
|
71
|
+
- lib/clonk.rb
|
72
|
+
- lib/clonk
|
73
|
+
- lib/clonk/realm.rb
|
74
|
+
- lib/clonk/group.rb
|
75
|
+
- lib/clonk/policy.rb
|
76
|
+
- lib/clonk/role.rb
|
77
|
+
- lib/clonk/client.rb
|
78
|
+
- lib/clonk/permission.rb
|
79
|
+
- lib/clonk/user.rb
|
57
80
|
required_ruby_version: !ruby/object:Gem::Requirement
|
58
81
|
requirements:
|
59
82
|
- - ">="
|