machina-auth 0.1.5 → 0.1.7
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/lib/machina/middleware/authentication.rb +15 -7
- data/lib/machina/permission_sync.rb +3 -4
- data/lib/machina/version.rb +1 -1
- data/lib/machina.rb +2 -1
- data/spec/machina/current_spec.rb +2 -2
- data/spec/machina/middleware/authentication_spec.rb +60 -0
- data/spec/machina/permission_sync_spec.rb +9 -49
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 376d280c6510f8afa38049e22a0c73df81355c57571b83ef571d547e340ee7c3
|
|
4
|
+
data.tar.gz: 13ce052358975310f67e3b3a65daee423fb48ddc9e2490632b548c66aa0f32d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 271245b4d9c4e3a28d4feafe688d9c3e1985d28566f5f86037ab68ef76fa246f266c67b5dad94ad0d23b111844f9c9e9224e656f97bdeaf9ef76efaf4f18311b
|
|
7
|
+
data.tar.gz: f719e7340acaab6a85ea6b96408c639e0608c797dcbd64f57fa9f5fa76921c5a6ccaac3d0c2751932d77de0ca1019fd4f127479f9b461686b6b0b36c99206c95
|
|
@@ -23,10 +23,7 @@ module Machina
|
|
|
23
23
|
# Token present but invalid/expired: clear the stale cache entry and
|
|
24
24
|
# fall through with nil authorized. API callers get 401 from the
|
|
25
25
|
# controller; browser users get redirected to login by authenticate!.
|
|
26
|
-
unless session_data
|
|
27
|
-
Machina.cache.delete(cache_key(token))
|
|
28
|
-
return @app.call(env)
|
|
29
|
-
end
|
|
26
|
+
return handle_expired_token(env, request, token) unless session_data
|
|
30
27
|
|
|
31
28
|
Machina::Current.authorized = Machina::Authorized.new(session_data)
|
|
32
29
|
|
|
@@ -56,11 +53,23 @@ module Machina
|
|
|
56
53
|
end
|
|
57
54
|
end
|
|
58
55
|
|
|
56
|
+
# Clears the cache entry for the expired token and, when the token
|
|
57
|
+
# originated from the session cookie, deletes that cookie so the
|
|
58
|
+
# browser stops sending it on subsequent requests. This prevents a
|
|
59
|
+
# redirect loop where an expired cookie shadows fresh callback tokens.
|
|
60
|
+
def handle_expired_token(env, request, token)
|
|
61
|
+
Machina.cache.delete(cache_key(token))
|
|
62
|
+
|
|
63
|
+
status, headers, body = @app.call(env)
|
|
64
|
+
Rack::Utils.delete_cookie_header!(headers, 'machina_session') if request.cookies['machina_session'] == token
|
|
65
|
+
[status, headers, body]
|
|
66
|
+
end
|
|
67
|
+
|
|
59
68
|
def extract_token(request)
|
|
60
|
-
request.
|
|
69
|
+
request.params['token']
|
|
70
|
+
|| request.cookies['machina_session']
|
|
61
71
|
|| extract_bearer(request)
|
|
62
72
|
|| request.headers['X-Api-Key']
|
|
63
|
-
|| request.params['token']
|
|
64
73
|
end
|
|
65
74
|
|
|
66
75
|
def extract_bearer(request)
|
|
@@ -117,7 +126,6 @@ module Machina
|
|
|
117
126
|
def cache_key(token)
|
|
118
127
|
"machina:session:#{token}"
|
|
119
128
|
end
|
|
120
|
-
|
|
121
129
|
end
|
|
122
130
|
end
|
|
123
131
|
end
|
|
@@ -14,13 +14,12 @@ module Machina
|
|
|
14
14
|
def call!
|
|
15
15
|
manifest = load_manifest
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
raise Machina::ConfigurationError, 'product_id is required for permission sync (set in machina.yml or Machina.config)'
|
|
17
|
+
if Machina.config.product_id.blank?
|
|
18
|
+
raise Machina::ConfigurationError, 'product_id is required for permission sync (set Machina.config.product_id)'
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
Machina.identity_client.sync_permissions(
|
|
23
|
-
product_id
|
|
22
|
+
product_id: Machina.config.product_id,
|
|
24
23
|
policies: manifest.fetch(:policies, []),
|
|
25
24
|
permissions: manifest.fetch(:permissions),
|
|
26
25
|
)
|
data/lib/machina/version.rb
CHANGED
data/lib/machina.rb
CHANGED
|
@@ -101,7 +101,8 @@ module Machina
|
|
|
101
101
|
callback = config.identity_callback_uri
|
|
102
102
|
|
|
103
103
|
if callback.blank?
|
|
104
|
-
raise ConfigurationError,
|
|
104
|
+
raise ConfigurationError,
|
|
105
|
+
'identity_callback_uri must be configured to use authorize_url without an explicit redirect_to'
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
return_to.present? ? "#{callback}?return_to=#{CGI.escape(return_to)}" : callback
|
|
@@ -19,10 +19,10 @@ RSpec.describe Machina::Current do
|
|
|
19
19
|
# but each CurrentAttributes subclass has its own thread-local storage.
|
|
20
20
|
described_class.authorized = authorized
|
|
21
21
|
|
|
22
|
-
expect(
|
|
22
|
+
expect(Current.authorized).to be_nil
|
|
23
23
|
expect(described_class.authorized).to eq(authorized)
|
|
24
24
|
ensure
|
|
25
|
-
|
|
25
|
+
Current.reset
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
it 'returns nil when nothing is set' do
|
|
@@ -100,6 +100,66 @@ RSpec.describe Machina::Middleware::Authentication do
|
|
|
100
100
|
expect(Machina.cache.read(cache_key)).to be_nil
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
context 'when a stale cookie coexists with a fresh callback token' do
|
|
104
|
+
let(:stale_token) { 'ps_expired_cookie' }
|
|
105
|
+
let(:fresh_token) { 'ps_fresh_callback' }
|
|
106
|
+
|
|
107
|
+
before do
|
|
108
|
+
allow(identity_client).to receive(:resolve_session).with(stale_token).and_return(
|
|
109
|
+
Machina::IdentityClient::Response.new(status: 404, body: '{}'),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
allow(identity_client).to receive(:resolve_session).with(fresh_token).and_return(
|
|
113
|
+
Machina::IdentityClient::Response.new(status: 200, body: MockResponses.session_resolution),
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'prefers the query param token over the stale cookie' do
|
|
118
|
+
env = Rack::MockRequest.env_for(
|
|
119
|
+
'/auth/machina/callback?token=ps_fresh_callback',
|
|
120
|
+
'HTTP_COOKIE' => 'machina_session=ps_expired_cookie',
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
status, _headers, body = middleware.call(env)
|
|
124
|
+
|
|
125
|
+
expect(status).to eq(200)
|
|
126
|
+
parsed = JSON.parse(body.first)
|
|
127
|
+
expect(parsed['user_id']).to eq(MockResponses.session_resolution['data']['user']['id'])
|
|
128
|
+
expect(identity_client).not_to have_received(:resolve_session).with(stale_token)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'deletes the session cookie when a cookie-sourced token is expired' do
|
|
133
|
+
allow(identity_client).to receive(:resolve_session).with('ps_expired_cookie').and_return(
|
|
134
|
+
Machina::IdentityClient::Response.new(status: 404, body: '{}'),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
env = Rack::MockRequest.env_for(
|
|
138
|
+
'/resource',
|
|
139
|
+
'HTTP_COOKIE' => 'machina_session=ps_expired_cookie',
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
_status, headers, _body = middleware.call(env)
|
|
143
|
+
|
|
144
|
+
expect(headers['set-cookie']).to include('machina_session=')
|
|
145
|
+
expect(headers['set-cookie']).to include('max-age=0')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'does not delete the cookie when a bearer token fails resolution' do
|
|
149
|
+
allow(identity_client).to receive(:resolve_session).with('ps_expired_bearer').and_return(
|
|
150
|
+
Machina::IdentityClient::Response.new(status: 404, body: '{}'),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
env = Rack::MockRequest.env_for(
|
|
154
|
+
'/resource',
|
|
155
|
+
'HTTP_AUTHORIZATION' => 'Bearer ps_expired_bearer',
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
_status, headers, _body = middleware.call(env)
|
|
159
|
+
|
|
160
|
+
expect(headers['set-cookie']).to be_nil
|
|
161
|
+
end
|
|
162
|
+
|
|
103
163
|
it 'uses the cache on subsequent requests' do
|
|
104
164
|
response = Machina::IdentityClient::Response.new(
|
|
105
165
|
status: 200,
|
|
@@ -4,9 +4,10 @@ require_relative '../rails_helper'
|
|
|
4
4
|
|
|
5
5
|
RSpec.describe Machina::PermissionSync do
|
|
6
6
|
let(:client) { instance_double(Machina::IdentityClient) }
|
|
7
|
+
let(:config_product_id) { 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' }
|
|
7
8
|
|
|
8
9
|
before do
|
|
9
|
-
Machina.config.product_id =
|
|
10
|
+
Machina.config.product_id = config_product_id
|
|
10
11
|
allow(Machina).to receive(:identity_client).and_return(client)
|
|
11
12
|
allow(client).to receive(:sync_permissions)
|
|
12
13
|
end
|
|
@@ -15,7 +16,7 @@ RSpec.describe Machina::PermissionSync do
|
|
|
15
16
|
described_class.call!
|
|
16
17
|
|
|
17
18
|
expect(client).to have_received(:sync_permissions).with(
|
|
18
|
-
product_id:
|
|
19
|
+
product_id: config_product_id,
|
|
19
20
|
permissions: [{ key: 'sessions.view', description: 'View sessions' }],
|
|
20
21
|
policies: [{ name: 'Viewer', api_name: 'viewer', permissions: ['sessions.view'] }],
|
|
21
22
|
)
|
|
@@ -31,8 +32,6 @@ RSpec.describe Machina::PermissionSync do
|
|
|
31
32
|
let(:tmpfile) do
|
|
32
33
|
file = Tempfile.new(['machina', '.yml'])
|
|
33
34
|
file.write(<<~YAML)
|
|
34
|
-
product_id: flat-product-id
|
|
35
|
-
|
|
36
35
|
permissions:
|
|
37
36
|
- key: items.view
|
|
38
37
|
description: View items
|
|
@@ -54,7 +53,7 @@ RSpec.describe Machina::PermissionSync do
|
|
|
54
53
|
described_class.call!
|
|
55
54
|
|
|
56
55
|
expect(client).to have_received(:sync_permissions).with(
|
|
57
|
-
product_id:
|
|
56
|
+
product_id: config_product_id,
|
|
58
57
|
permissions: [{ key: 'items.view', description: 'View items' }],
|
|
59
58
|
policies: [{ name: 'Reader', api_name: 'reader', permissions: ['items.view'] }],
|
|
60
59
|
)
|
|
@@ -66,14 +65,12 @@ RSpec.describe Machina::PermissionSync do
|
|
|
66
65
|
file = Tempfile.new(['machina', '.yml'])
|
|
67
66
|
file.write(<<~YAML)
|
|
68
67
|
test:
|
|
69
|
-
product_id: test-product-id
|
|
70
68
|
permissions:
|
|
71
69
|
- key: reports.view
|
|
72
70
|
description: View reports
|
|
73
71
|
policies: []
|
|
74
72
|
|
|
75
73
|
production:
|
|
76
|
-
product_id: prod-product-id
|
|
77
74
|
permissions:
|
|
78
75
|
- key: reports.view
|
|
79
76
|
description: View reports
|
|
@@ -85,18 +82,14 @@ RSpec.describe Machina::PermissionSync do
|
|
|
85
82
|
file
|
|
86
83
|
end
|
|
87
84
|
|
|
88
|
-
before
|
|
89
|
-
Machina.config.manifest = tmpfile.path
|
|
90
|
-
Machina.config.product_id = nil
|
|
91
|
-
end
|
|
92
|
-
|
|
85
|
+
before { Machina.config.manifest = tmpfile.path }
|
|
93
86
|
after { tmpfile.unlink }
|
|
94
87
|
|
|
95
88
|
it 'loads the manifest scoped to the current Rails environment' do
|
|
96
89
|
described_class.call!
|
|
97
90
|
|
|
98
91
|
expect(client).to have_received(:sync_permissions).with(
|
|
99
|
-
product_id:
|
|
92
|
+
product_id: config_product_id,
|
|
100
93
|
permissions: [{ key: 'reports.view', description: 'View reports' }],
|
|
101
94
|
policies: [],
|
|
102
95
|
)
|
|
@@ -107,8 +100,6 @@ RSpec.describe Machina::PermissionSync do
|
|
|
107
100
|
let(:tmpfile) do
|
|
108
101
|
file = Tempfile.new(['machina', '.yml'])
|
|
109
102
|
file.write(<<~YAML)
|
|
110
|
-
product_id: <%= "erb-product-id" %>
|
|
111
|
-
|
|
112
103
|
permissions:
|
|
113
104
|
- key: tasks.view
|
|
114
105
|
description: View tasks
|
|
@@ -119,51 +110,20 @@ RSpec.describe Machina::PermissionSync do
|
|
|
119
110
|
file
|
|
120
111
|
end
|
|
121
112
|
|
|
122
|
-
before
|
|
123
|
-
Machina.config.manifest = tmpfile.path
|
|
124
|
-
Machina.config.product_id = nil
|
|
125
|
-
end
|
|
126
|
-
|
|
113
|
+
before { Machina.config.manifest = tmpfile.path }
|
|
127
114
|
after { tmpfile.unlink }
|
|
128
115
|
|
|
129
116
|
it 'evaluates ERB before parsing YAML' do
|
|
130
117
|
described_class.call!
|
|
131
118
|
|
|
132
119
|
expect(client).to have_received(:sync_permissions).with(
|
|
133
|
-
product_id:
|
|
120
|
+
product_id: config_product_id,
|
|
134
121
|
permissions: [{ key: 'tasks.view', description: 'View tasks' }],
|
|
135
122
|
policies: [],
|
|
136
123
|
)
|
|
137
124
|
end
|
|
138
125
|
end
|
|
139
126
|
|
|
140
|
-
context 'when manifest product_id is absent but config product_id is set' do
|
|
141
|
-
let(:tmpfile) do
|
|
142
|
-
file = Tempfile.new(['machina', '.yml'])
|
|
143
|
-
file.write(<<~YAML)
|
|
144
|
-
permissions:
|
|
145
|
-
- key: items.view
|
|
146
|
-
description: View items
|
|
147
|
-
policies: []
|
|
148
|
-
YAML
|
|
149
|
-
file.close
|
|
150
|
-
file
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
before { Machina.config.manifest = tmpfile.path }
|
|
154
|
-
after { tmpfile.unlink }
|
|
155
|
-
|
|
156
|
-
it 'falls back to Machina.config.product_id' do
|
|
157
|
-
described_class.call!
|
|
158
|
-
|
|
159
|
-
expect(client).to have_received(:sync_permissions).with(
|
|
160
|
-
product_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
161
|
-
permissions: [{ key: 'items.view', description: 'View items' }],
|
|
162
|
-
policies: [],
|
|
163
|
-
)
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
127
|
context 'when policies key is omitted from manifest' do
|
|
168
128
|
let(:tmpfile) do
|
|
169
129
|
file = Tempfile.new(['machina', '.yml'])
|
|
@@ -183,7 +143,7 @@ RSpec.describe Machina::PermissionSync do
|
|
|
183
143
|
described_class.call!
|
|
184
144
|
|
|
185
145
|
expect(client).to have_received(:sync_permissions).with(
|
|
186
|
-
product_id:
|
|
146
|
+
product_id: config_product_id,
|
|
187
147
|
permissions: [{ key: 'items.view', description: 'View items' }],
|
|
188
148
|
policies: [],
|
|
189
149
|
)
|