masks 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +179 -31
- data/app/controllers/masks/rails/avatars_controller.rb +1 -1
- data/app/controllers/masks/rails/handshakes_controller.rb +9 -1
- data/app/controllers/masks/rails/sessions_controller.rb +5 -3
- data/lib/masks/client/device.rb +47 -0
- data/lib/masks/client/embedded.rb +125 -0
- data/lib/masks/client/enrollment.rb +33 -0
- data/lib/masks/client/errors.rb +6 -0
- data/lib/masks/client/http.rb +89 -15
- data/lib/masks/client/issuer.rb +2 -1
- data/lib/masks/client/key.rb +100 -0
- data/lib/masks/client/nonces.rb +31 -0
- data/lib/masks/client/proof.rb +7 -1
- data/lib/masks/client/rack.rb +8 -0
- data/lib/masks/client/registration.rb +32 -15
- data/lib/masks/client/session.rb +120 -11
- data/lib/masks/client/tokens.rb +4 -0
- data/lib/masks/client/transport.rb +114 -0
- data/lib/masks/client.rb +21 -0
- data/lib/masks/rails/authentication.rb +84 -93
- data/lib/masks/rails/configuration.rb +42 -10
- data/lib/masks/rails/masked.rb +126 -0
- data/lib/masks/rails/privileged.rb +29 -0
- data/lib/masks/rails/protected_resource.rb +37 -13
- data/lib/masks/rails.rb +2 -0
- data/lib/masks/version.rb +1 -1
- metadata +9 -1
data/lib/masks/client/tokens.rb
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Client
|
|
3
|
+
class Transport
|
|
4
|
+
VERBS = {
|
|
5
|
+
"GET" => Net::HTTP::Get, "HEAD" => Net::HTTP::Head, "POST" => Net::HTTP::Post, "PUT" => Net::HTTP::Put,
|
|
6
|
+
"PATCH" => Net::HTTP::Patch, "DELETE" => Net::HTTP::Delete
|
|
7
|
+
}.freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :origins, :key
|
|
10
|
+
|
|
11
|
+
def initialize(*origins, tokens: nil, key: nil, &renew)
|
|
12
|
+
raise ArgumentError, "a transport needs the origins it may send its tokens to" if origins.empty?
|
|
13
|
+
raise ArgumentError, "a transport needs tokens or a way to fetch them" if tokens.nil? && renew.nil?
|
|
14
|
+
|
|
15
|
+
@origins = origins.flatten.map { |origin| Nonces.origin(HTTP.checked(URI.parse(origin.to_s))) }.uniq.freeze
|
|
16
|
+
@tokens = tokens
|
|
17
|
+
@key = key && Key.from(key)
|
|
18
|
+
@renew = renew
|
|
19
|
+
@lock = Mutex.new
|
|
20
|
+
@nonces = Nonces.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def tokens
|
|
24
|
+
@lock.synchronize do
|
|
25
|
+
@tokens = @renew.call(@tokens) if @renew && (@tokens.nil? || @tokens.expired?)
|
|
26
|
+
@tokens
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get(url, headers: {})
|
|
31
|
+
request("GET", url, headers: headers)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def head(url, headers: {})
|
|
35
|
+
request("HEAD", url, headers: headers)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def delete(url, headers: {})
|
|
39
|
+
request("DELETE", url, headers: headers)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def post(url, body: nil, headers: {})
|
|
43
|
+
request("POST", url, body: body, headers: headers)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def put(url, body: nil, headers: {})
|
|
47
|
+
request("PUT", url, body: body, headers: headers)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def patch(url, body: nil, headers: {})
|
|
51
|
+
request("PATCH", url, body: body, headers: headers)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def request(method, url, body: nil, headers: {}, limit: nil)
|
|
55
|
+
uri = URI.parse(url.to_s)
|
|
56
|
+
verb = VERBS.fetch(method.to_s.upcase) { raise ArgumentError, "#{method} is not a method a transport sends" }
|
|
57
|
+
|
|
58
|
+
unless origins.include?(Nonces.origin(uri))
|
|
59
|
+
raise Foreign, "this transport holds tokens only for #{origins.join(', ')}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
held = tokens
|
|
63
|
+
nonce = @nonces[uri] if held.bound?
|
|
64
|
+
response = HTTP.deliver(signed(verb, uri, body, headers, held, nonce), limit: limit)
|
|
65
|
+
|
|
66
|
+
return response unless held.bound?
|
|
67
|
+
|
|
68
|
+
wanted = @nonces.learn(uri, response)
|
|
69
|
+
|
|
70
|
+
return response unless wanted && wanted != nonce && nonce_asked?(response)
|
|
71
|
+
|
|
72
|
+
HTTP.deliver(signed(verb, uri, body, headers, held, wanted), limit: limit)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def inspect
|
|
76
|
+
"#<#{self.class.name} #{origins.join(', ')}>"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def signed(verb, uri, body, headers, held, nonce)
|
|
82
|
+
request = verb.new(uri, HTTP.default_headers.merge(headers))
|
|
83
|
+
|
|
84
|
+
if held.bound?
|
|
85
|
+
raise Error, "a DPoP token needs the key it is held to" if key.nil?
|
|
86
|
+
|
|
87
|
+
request["Authorization"] = "#{Proof::SCHEME} #{held.access_token}"
|
|
88
|
+
request["DPoP"] = key.proof(request.method, uri, access_token: held.access_token, nonce: nonce)
|
|
89
|
+
else
|
|
90
|
+
request["Authorization"] = "Bearer #{held.access_token}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
written(request, body)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def written(request, body)
|
|
97
|
+
return request if body.nil?
|
|
98
|
+
|
|
99
|
+
if body.is_a?(String)
|
|
100
|
+
request.body = body
|
|
101
|
+
else
|
|
102
|
+
request.body = JSON.generate(body)
|
|
103
|
+
request["Content-Type"] ||= "application/json"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
request
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def nonce_asked?(response)
|
|
110
|
+
response.code.to_i == 401 && response["WWW-Authenticate"].to_s.include?("use_dpop_nonce")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/masks/client.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "base64"
|
|
2
|
+
require "ipaddr"
|
|
2
3
|
require "json"
|
|
3
4
|
require "jwt"
|
|
4
5
|
require "net/http"
|
|
@@ -9,14 +10,20 @@ require "uri"
|
|
|
9
10
|
require_relative "version"
|
|
10
11
|
require_relative "client/errors"
|
|
11
12
|
require_relative "client/http"
|
|
13
|
+
require_relative "client/embedded"
|
|
12
14
|
require_relative "client/pkce"
|
|
13
15
|
require_relative "client/stores"
|
|
14
16
|
require_relative "client/tracker"
|
|
17
|
+
require_relative "client/nonces"
|
|
18
|
+
require_relative "client/key"
|
|
15
19
|
require_relative "client/issuer"
|
|
16
20
|
require_relative "client/registry"
|
|
17
21
|
require_relative "client/tokens"
|
|
18
22
|
require_relative "client/claims"
|
|
19
23
|
require_relative "client/introspection"
|
|
24
|
+
require_relative "client/enrollment"
|
|
25
|
+
require_relative "client/device"
|
|
26
|
+
require_relative "client/transport"
|
|
20
27
|
require_relative "client/session"
|
|
21
28
|
require_relative "client/verifier"
|
|
22
29
|
require_relative "client/proof"
|
|
@@ -44,6 +51,20 @@ module Masks
|
|
|
44
51
|
# state of its own beyond the discovery cache in ::registry.
|
|
45
52
|
module Client
|
|
46
53
|
class << self
|
|
54
|
+
attr_writer :plain_http
|
|
55
|
+
|
|
56
|
+
def plain_http?
|
|
57
|
+
@plain_http == true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def embed(app, &serves)
|
|
61
|
+
@embedded = app && Embedded.new(app, &serves)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def embedded_for(uri)
|
|
65
|
+
@embedded if @embedded&.serves?(uri)
|
|
66
|
+
end
|
|
67
|
+
|
|
47
68
|
# The process-wide cache of resolved issuers, so discovery is fetched
|
|
48
69
|
# once rather than per request.
|
|
49
70
|
#
|
|
@@ -3,24 +3,19 @@ module Masks
|
|
|
3
3
|
module Authentication
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
include Masks::Rails::Configurable
|
|
6
|
+
include Masks::Rails::Privileged
|
|
6
7
|
|
|
7
8
|
included do
|
|
8
9
|
if respond_to?(:helper_method)
|
|
9
|
-
helper_method :
|
|
10
|
-
:masks_claims, :masks_organization, :masks_organizations, :masks_role?,
|
|
11
|
-
:masks_login_url
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
class_methods do
|
|
16
|
-
def masks_members_only!(role: nil, organization: nil, **options)
|
|
17
|
-
before_action(-> { authorize_masks_member!(*Array(role), organization: organization) },
|
|
18
|
-
**options.slice(:only, :except))
|
|
10
|
+
helper_method :masked, :masks_login_url
|
|
19
11
|
end
|
|
20
12
|
end
|
|
21
13
|
|
|
22
14
|
REQUESTS = "masks_requests".freeze
|
|
23
15
|
HANDSHAKES = "masks_handshakes".freeze
|
|
16
|
+
FAREWELLS = "masks_farewells".freeze
|
|
17
|
+
STEPPED_UP = "masks_stepped_up".freeze
|
|
18
|
+
STEP_UP_WITHIN = 300
|
|
24
19
|
|
|
25
20
|
def masks_session
|
|
26
21
|
@masks_session ||= masks_config.session_for(request)
|
|
@@ -34,6 +29,10 @@ module Masks
|
|
|
34
29
|
@masks_handshakes ||= masks_tracker(HANDSHAKES)
|
|
35
30
|
end
|
|
36
31
|
|
|
32
|
+
def masks_farewells
|
|
33
|
+
masks_tracker(FAREWELLS)
|
|
34
|
+
end
|
|
35
|
+
|
|
37
36
|
def masks_tracker(key)
|
|
38
37
|
Masks::Client::Tracker.new(Masks::Client::Stores::Session.new(session, key))
|
|
39
38
|
end
|
|
@@ -50,8 +49,7 @@ module Masks
|
|
|
50
49
|
|
|
51
50
|
session[masks_config.session_key] = held.compact
|
|
52
51
|
@masks_tokens = tokens
|
|
53
|
-
@
|
|
54
|
-
@masks_claims = nil
|
|
52
|
+
@masked = nil
|
|
55
53
|
end
|
|
56
54
|
|
|
57
55
|
def masks_held
|
|
@@ -73,8 +71,19 @@ module Masks
|
|
|
73
71
|
def masks_forget
|
|
74
72
|
session.delete(masks_config.session_key)
|
|
75
73
|
@masks_tokens = nil
|
|
76
|
-
@
|
|
77
|
-
|
|
74
|
+
@masked = nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def masked
|
|
78
|
+
@masked ||= if masks_tokens
|
|
79
|
+
Masked.new(
|
|
80
|
+
claims: Masks::Client::Claims.new(masks_held["identity"] || {}), scopes: masks_tokens.scopes,
|
|
81
|
+
config: masks_config, signed_in: masks_signed_in?, access_token: masks_tokens.access_token,
|
|
82
|
+
expires_at: masks_tokens.expires_at, account_url: masks_account_url
|
|
83
|
+
)
|
|
84
|
+
else
|
|
85
|
+
Masked.nobody(masks_config)
|
|
86
|
+
end
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
def masks_signed_in?
|
|
@@ -119,46 +128,6 @@ module Masks
|
|
|
119
128
|
Masks::Rails::Engine.routes.url_helpers.handshake_path
|
|
120
129
|
end
|
|
121
130
|
|
|
122
|
-
def masks_identity
|
|
123
|
-
return @masks_identity if defined?(@masks_identity)
|
|
124
|
-
|
|
125
|
-
@masks_identity = masks_held["identity"]
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def masks_tenant
|
|
129
|
-
masks_identity&.dig("tenant")
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def masks_claims
|
|
133
|
-
@masks_claims ||= Masks::Client::Claims.new(masks_identity || {})
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def masks_organization
|
|
137
|
-
held = masks_claims.organization
|
|
138
|
-
|
|
139
|
-
held.present? ? held : nil
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def masks_organizations
|
|
143
|
-
masks_claims.organizations
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def masks_role?(*roles)
|
|
147
|
-
masks_organization&.role?(*roles) || false
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def masks_scopes
|
|
151
|
-
masks_tokens&.scopes || []
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def masks_permits?(scope)
|
|
155
|
-
masks_scopes.include?(scope.to_s)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def masks_access_token
|
|
159
|
-
masks_tokens&.access_token
|
|
160
|
-
end
|
|
161
|
-
|
|
162
131
|
def masks_refresh!
|
|
163
132
|
return false unless masks_configured?
|
|
164
133
|
return false if masks_tokens&.refresh_token.nil?
|
|
@@ -188,52 +157,28 @@ module Masks
|
|
|
188
157
|
profile.empty? ? nil : held.merge(profile.slice(*IDENTITY))
|
|
189
158
|
end
|
|
190
159
|
|
|
191
|
-
def masks_logout_url(
|
|
160
|
+
def masks_logout_url(state:)
|
|
192
161
|
return nil unless masks_configured?
|
|
193
162
|
|
|
194
163
|
masks_session.end_session_url(
|
|
195
|
-
post_logout_redirect_uri:
|
|
196
|
-
state:
|
|
164
|
+
post_logout_redirect_uri: masks_config.return_to_for(request),
|
|
165
|
+
state: state
|
|
197
166
|
)
|
|
198
167
|
rescue Masks::Client::Error
|
|
199
168
|
nil
|
|
200
169
|
end
|
|
201
170
|
|
|
202
|
-
def
|
|
203
|
-
"#{request.base_url}#{masks_local_path(masks_config.after_sign_out) || '/'}"
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def masks_login_url(return_to: nil, organization: nil)
|
|
171
|
+
def masks_login_url(return_to: nil, organization: nil, privilege: nil)
|
|
207
172
|
path = Masks::Rails::Engine.routes.url_helpers.start_path
|
|
208
173
|
query = {
|
|
209
174
|
"return_to" => masks_local_path(return_to),
|
|
210
|
-
"organization" => Masks::Client::Session.organization_key(organization)
|
|
175
|
+
"organization" => Masks::Client::Session.organization_key(organization),
|
|
176
|
+
"privilege" => Array(privilege).join(" ").presence
|
|
211
177
|
}.compact
|
|
212
178
|
|
|
213
179
|
query.empty? ? path : "#{path}?#{URI.encode_www_form(query)}"
|
|
214
180
|
end
|
|
215
181
|
|
|
216
|
-
def masks_account
|
|
217
|
-
identity = masks_identity || {}
|
|
218
|
-
|
|
219
|
-
{
|
|
220
|
-
"signed_in" => masks_signed_in?,
|
|
221
|
-
"subject" => identity["sub"],
|
|
222
|
-
"name" => identity["name"],
|
|
223
|
-
"nickname" => identity["preferred_username"],
|
|
224
|
-
"email" => identity["email"],
|
|
225
|
-
"email_verified" => identity["email_verified"],
|
|
226
|
-
"picture" => masks_claims.picture,
|
|
227
|
-
"avatars" => masks_claims.avatars.to_h.presence,
|
|
228
|
-
"tenant" => masks_tenant,
|
|
229
|
-
"organization" => masks_organization&.to_h,
|
|
230
|
-
"organizations" => masks_organizations.map(&:to_h).presence,
|
|
231
|
-
"scopes" => masks_scopes,
|
|
232
|
-
"expires_at" => masks_tokens&.expires_at,
|
|
233
|
-
"account_url" => masks_account_url
|
|
234
|
-
}.compact
|
|
235
|
-
end
|
|
236
|
-
|
|
237
182
|
def masks_account_url
|
|
238
183
|
"#{masks_config.issuer_for(request).to_s.chomp('/')}/"
|
|
239
184
|
rescue Masks::Rails::Configuration::Unconfigured
|
|
@@ -247,17 +192,63 @@ module Masks
|
|
|
247
192
|
false
|
|
248
193
|
end
|
|
249
194
|
|
|
250
|
-
|
|
251
|
-
return false unless authenticate_masks!
|
|
195
|
+
private
|
|
252
196
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
rescue Masks::Client::Forbidden => e
|
|
256
|
-
masks_forbid(e)
|
|
257
|
-
false
|
|
258
|
-
end
|
|
197
|
+
def masks_sign_out_upstream
|
|
198
|
+
return nil unless masks_configured?
|
|
259
199
|
|
|
260
|
-
|
|
200
|
+
farewell = masks_farewells.open
|
|
201
|
+
url = masks_logout_url(state: farewell.id)
|
|
202
|
+
|
|
203
|
+
masks_farewells.clear! if url.nil?
|
|
204
|
+
url
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def masks_after_sign_out_path
|
|
208
|
+
masks_local_path(masks_config.after_sign_out) || "/"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def masks_authorize_session!(wanted)
|
|
212
|
+
return false unless authenticate_masks!
|
|
213
|
+
|
|
214
|
+
wanted.each { |privilege| masked.member!(privilege) }
|
|
215
|
+
|
|
216
|
+
missing = wanted.reject { |privilege| (privilege.scopes - masked.scopes).empty? }.map(&:name)
|
|
217
|
+
return true if missing.empty?
|
|
218
|
+
|
|
219
|
+
if masks_asked_already?(session.delete(STEPPED_UP), missing)
|
|
220
|
+
raise Masks::Client::Forbidden.new(
|
|
221
|
+
"insufficient_scope", "this account does not hold the #{missing.join(' and ')} privilege",
|
|
222
|
+
scope: missing.flat_map { |name| masks_config.privilege(name).scopes }.join(" ")
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
masks_ask_again(missing)
|
|
227
|
+
false
|
|
228
|
+
rescue Masks::Client::Forbidden => e
|
|
229
|
+
masks_forbid(e)
|
|
230
|
+
false
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def masks_asked_already?(asked, missing)
|
|
234
|
+
asked.is_a?(Hash) && (missing - Array(asked["privileges"])).empty? &&
|
|
235
|
+
asked["at"].to_i > Time.now.to_i - STEP_UP_WITHIN
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def masks_ask_again(missing)
|
|
239
|
+
return_to = request.get? ? request.fullpath : masks_referring_path
|
|
240
|
+
url = masks_login_url(return_to: return_to, privilege: missing)
|
|
241
|
+
|
|
242
|
+
session[STEPPED_UP] = { "privileges" => missing, "at" => Time.now.to_i }
|
|
243
|
+
|
|
244
|
+
if masks_wants_json?
|
|
245
|
+
response.headers["Cache-Control"] = "no-store"
|
|
246
|
+
render json: { "error" => "privilege_required", "privileges" => missing, "login_url" => url },
|
|
247
|
+
status: :forbidden
|
|
248
|
+
else
|
|
249
|
+
redirect_to url
|
|
250
|
+
end
|
|
251
|
+
end
|
|
261
252
|
|
|
262
253
|
def masks_forbid(error)
|
|
263
254
|
response.headers["Cache-Control"] = "no-store"
|
|
@@ -3,14 +3,19 @@ module Masks
|
|
|
3
3
|
class Configuration
|
|
4
4
|
class Unconfigured < Masks::Client::Error; end
|
|
5
5
|
|
|
6
|
+
Privilege = Struct.new(:name, :scopes, :roles, :organization, keyword_init: true)
|
|
7
|
+
PRIVILEGE_KEYS = %w[scope role organization].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :privileges
|
|
6
10
|
attr_accessor :scope, :namespace, :resource, :resource_scopes, :after_sign_in,
|
|
7
11
|
:after_sign_out, :session_key, :sign_out_of_issuer, :parent_controller,
|
|
8
12
|
:credentials_path, :authenticate_everything, :delegates
|
|
9
13
|
attr_writer :issuer, :redirect_uri, :name, :credentials, :store, :forget, :logged_out,
|
|
10
|
-
:delegation_redirect_uri, :organization, :manages
|
|
14
|
+
:delegation_redirect_uri, :organization, :manages, :replay
|
|
11
15
|
|
|
12
16
|
def initialize
|
|
13
17
|
@scope = Masks::Client::Session::DEFAULT_SCOPE
|
|
18
|
+
@privileges = {}
|
|
14
19
|
@resource_scopes = []
|
|
15
20
|
@after_sign_in = "/"
|
|
16
21
|
@after_sign_out = "/"
|
|
@@ -89,12 +94,12 @@ module Masks
|
|
|
89
94
|
true
|
|
90
95
|
end
|
|
91
96
|
|
|
92
|
-
def manages?(request,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
def manages?(request, masked = nil)
|
|
98
|
+
case @manages
|
|
99
|
+
when Symbol, String then masked&.privilege?(@manages) || false
|
|
100
|
+
when nil then false
|
|
101
|
+
else (@manages.arity == 1 ? @manages.call(request) : @manages.call(request, masked)) ? true : false
|
|
102
|
+
end
|
|
98
103
|
end
|
|
99
104
|
|
|
100
105
|
def can_forget?
|
|
@@ -134,10 +139,15 @@ module Masks
|
|
|
134
139
|
Masks::Client::Resource.new(
|
|
135
140
|
issuer: issuer_for(request),
|
|
136
141
|
url: url,
|
|
137
|
-
scopes: resource_scopes
|
|
142
|
+
scopes: resource_scopes,
|
|
143
|
+
replay: replay
|
|
138
144
|
)
|
|
139
145
|
end
|
|
140
146
|
|
|
147
|
+
def replay
|
|
148
|
+
@replay || Masks::Client::Proof.memory
|
|
149
|
+
end
|
|
150
|
+
|
|
141
151
|
def handshake_for(request)
|
|
142
152
|
Masks::Client::Handshake.new(
|
|
143
153
|
issuer_for(request),
|
|
@@ -151,12 +161,34 @@ module Masks
|
|
|
151
161
|
)
|
|
152
162
|
end
|
|
153
163
|
|
|
164
|
+
def privileges=(value)
|
|
165
|
+
@privileges = value.to_h.to_h do |name, spec|
|
|
166
|
+
held = spec.is_a?(Hash) ? spec.transform_keys(&:to_s) : { "scope" => spec }
|
|
167
|
+
unknown = held.keys - PRIVILEGE_KEYS
|
|
168
|
+
|
|
169
|
+
raise ArgumentError, "the #{name} privilege names #{unknown.join(', ')}, which is not #{PRIVILEGE_KEYS.join(', ')}" if unknown.any?
|
|
170
|
+
|
|
171
|
+
[ name.to_s, Privilege.new(name: name.to_s, scopes: Array(held["scope"]).map(&:to_s),
|
|
172
|
+
roles: Array(held["role"]).map(&:to_s), organization: held["organization"]&.to_s) ]
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def privilege(name)
|
|
177
|
+
privileges.fetch(name.to_s) { raise ArgumentError, "Masks::Rails.config.privileges has no #{name}" }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def scope_for(requested)
|
|
181
|
+
wanted = (requested.to_s.split & privileges.keys).flat_map { |name| privileges[name].scopes }
|
|
182
|
+
wanted.empty? ? nil : (Array(scope) + wanted).uniq
|
|
183
|
+
end
|
|
184
|
+
|
|
154
185
|
def approved_scope
|
|
155
186
|
delegated = delegates ? [ Masks::Client::Delegations::SCOPE ] : []
|
|
156
187
|
|
|
157
|
-
|
|
188
|
+
asked = (Array(scope) + privileges.values.flat_map(&:scopes)).uniq
|
|
189
|
+
return asked + delegated if namespace.blank?
|
|
158
190
|
|
|
159
|
-
outside =
|
|
191
|
+
outside = asked.reject { |name| name.to_s.start_with?(namespace) }
|
|
160
192
|
|
|
161
193
|
outside + [ namespace ] + delegated
|
|
162
194
|
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Rails
|
|
3
|
+
class Masked
|
|
4
|
+
attr_reader :claims, :scopes, :access_token, :expires_at, :account_url
|
|
5
|
+
|
|
6
|
+
def self.nobody(config)
|
|
7
|
+
new(claims: Masks::Client::Claims.new({}), scopes: [], config: config, signed_in: false)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(claims:, scopes:, config:, signed_in:, access_token: nil, expires_at: nil, account_url: nil)
|
|
11
|
+
@claims = claims
|
|
12
|
+
@scopes = Array(scopes).map(&:to_s).freeze
|
|
13
|
+
@config = config
|
|
14
|
+
@signed_in = signed_in
|
|
15
|
+
@access_token = access_token
|
|
16
|
+
@expires_at = expires_at
|
|
17
|
+
@account_url = account_url
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def signed_in?
|
|
21
|
+
@signed_in
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def [](claim)
|
|
25
|
+
claims[claim]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def subject
|
|
29
|
+
claims.subject
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def name
|
|
33
|
+
claims["name"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def nickname
|
|
37
|
+
claims["preferred_username"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def email
|
|
41
|
+
claims["email"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def email_verified?
|
|
45
|
+
claims["email_verified"] == true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def picture
|
|
49
|
+
claims.picture
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def avatars
|
|
53
|
+
claims.avatars
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def tenant
|
|
57
|
+
claims.tenant
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def organization
|
|
61
|
+
held = claims.organization
|
|
62
|
+
|
|
63
|
+
held.present? ? held : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def organizations
|
|
67
|
+
claims.organizations
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def role?(*roles)
|
|
71
|
+
organization&.role?(*roles) || false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def permits?(scope)
|
|
75
|
+
scopes.include?(scope.to_s)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def privileges
|
|
79
|
+
return [] unless signed_in?
|
|
80
|
+
|
|
81
|
+
@config.privileges.values.select { |privilege| holds?(privilege) }.map(&:name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def privilege?(*names)
|
|
85
|
+
wanted = names.flatten.map { |name| @config.privilege(name) }
|
|
86
|
+
|
|
87
|
+
signed_in? && wanted.all? { |privilege| holds?(privilege) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def holds?(privilege)
|
|
91
|
+
return false unless (privilege.scopes - scopes).empty?
|
|
92
|
+
|
|
93
|
+
member!(privilege)
|
|
94
|
+
true
|
|
95
|
+
rescue Masks::Client::Forbidden
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def member!(privilege)
|
|
100
|
+
return if privilege.roles.empty? && privilege.organization.nil?
|
|
101
|
+
|
|
102
|
+
claims.member!(*privilege.roles, organization: privilege.organization)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def to_h
|
|
106
|
+
{
|
|
107
|
+
"signed_in" => signed_in?,
|
|
108
|
+
"subject" => subject,
|
|
109
|
+
"name" => name,
|
|
110
|
+
"nickname" => nickname,
|
|
111
|
+
"email" => email,
|
|
112
|
+
"email_verified" => claims["email_verified"],
|
|
113
|
+
"picture" => picture,
|
|
114
|
+
"avatars" => avatars.to_h.presence,
|
|
115
|
+
"tenant" => claims["tenant"],
|
|
116
|
+
"organization" => organization&.to_h,
|
|
117
|
+
"organizations" => organizations.map(&:to_h).presence,
|
|
118
|
+
"scopes" => scopes,
|
|
119
|
+
"privileges" => privileges,
|
|
120
|
+
"expires_at" => expires_at,
|
|
121
|
+
"account_url" => account_url
|
|
122
|
+
}.compact
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Rails
|
|
3
|
+
module Privileged
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
include Masks::Rails::Configurable
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
def masked!(*privileges, **options)
|
|
9
|
+
before_action(-> { authorize_masks!(*privileges) }, **options.slice(:only, :except))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def authorize_masks!(*names)
|
|
14
|
+
privileges = names.flatten.map { |name| masks_config.privilege(name) }
|
|
15
|
+
|
|
16
|
+
masks_by_token? ? masks_authorize_token!(privileges) : masks_authorize_session!(privileges)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def masks_by_token?
|
|
22
|
+
return false unless respond_to?(:masks_authorize_token!, true)
|
|
23
|
+
return true if request.authorization.present? || !respond_to?(:masks_authorize_session!, true)
|
|
24
|
+
|
|
25
|
+
masks_tokens.nil?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|