machina-auth 0.1.2 → 0.1.4
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/configuration.rb +2 -1
- data/lib/machina/controller_helpers.rb +1 -1
- data/lib/machina/middleware/authentication.rb +9 -7
- data/lib/machina/permission_sync.rb +2 -1
- data/lib/machina/version.rb +1 -1
- data/lib/machina.rb +26 -4
- data/spec/machina/authorize_url_spec.rb +61 -0
- data/spec/machina/configuration_spec.rb +6 -0
- data/spec/machina/controller_helpers_spec.rb +4 -1
- data/spec/machina/middleware/authentication_spec.rb +46 -0
- data/spec/machina/permission_sync_spec.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8b85487ba807943451cf345d17e7aec7ee18a9b61a233adcdb213f86134cde0
|
|
4
|
+
data.tar.gz: 49908d36c4fb879354aa6c3b9ae96e33eaed76d98df2cd3dbfb8a51ce72b03a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97716d4c519933b65a8410615655adcf7e70a4013d9595e7ef5ca4a96ae2a57c44574640b33de5dc1adcd675b626c2ae39d09ffd0c5cf4fbcbb2932924988be9
|
|
7
|
+
data.tar.gz: 868b463264e64e8551c938704bcfad481714e0d6a9f4aa8dcc6152eb4d8df1bda0c6a01ff2ab55f89e534ceeb03b1d53abed3792dc2afaa1de660c6726dd2e2c
|
|
@@ -34,7 +34,7 @@ module Machina
|
|
|
34
34
|
if request.format.json?
|
|
35
35
|
render json: { error: 'unauthorized' }, status: :unauthorized
|
|
36
36
|
else
|
|
37
|
-
redirect_to Machina.authorize_url(
|
|
37
|
+
redirect_to Machina.authorize_url(return_to: request.original_url), allow_other_host: true
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -11,11 +11,11 @@ module Machina
|
|
|
11
11
|
|
|
12
12
|
def call(env)
|
|
13
13
|
request = ActionDispatch::Request.new(env)
|
|
14
|
-
|
|
14
|
+
# Skip paths allow the developer to disable the middleware from validating
|
|
15
|
+
# any token or headers on any matched URL or Path.
|
|
15
16
|
return @app.call(env) if skip_path?(request)
|
|
16
17
|
|
|
17
18
|
token = extract_token(request)
|
|
18
|
-
|
|
19
19
|
return @app.call(env) if token.blank?
|
|
20
20
|
|
|
21
21
|
session_data = resolve_session(token)
|
|
@@ -53,10 +53,7 @@ module Machina
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def extract_token(request)
|
|
56
|
-
request.cookies['machina_session'] ||
|
|
57
|
-
extract_bearer(request) ||
|
|
58
|
-
request.headers['X-Api-Key'] ||
|
|
59
|
-
request.params['token']
|
|
56
|
+
request.cookies['machina_session'] || extract_bearer(request) || request.headers['X-Api-Key'] || request.params['token']
|
|
60
57
|
end
|
|
61
58
|
|
|
62
59
|
def extract_bearer(request)
|
|
@@ -76,7 +73,11 @@ module Machina
|
|
|
76
73
|
|
|
77
74
|
def fetch_from_identity_service(token)
|
|
78
75
|
response = Machina.identity_client.resolve_session(token)
|
|
79
|
-
|
|
76
|
+
|
|
77
|
+
unless response.success?
|
|
78
|
+
Machina.cache.delete(cache_key(token))
|
|
79
|
+
return nil
|
|
80
|
+
end
|
|
80
81
|
|
|
81
82
|
data = unwrap_payload(response.parsed)
|
|
82
83
|
Machina.cache.write(cache_key(token), data, expires_in: Machina.config.cache_ttl)
|
|
@@ -91,6 +92,7 @@ module Machina
|
|
|
91
92
|
def cache_workspace_ref(data)
|
|
92
93
|
workspace = data['workspace']
|
|
93
94
|
organization = data['organization']
|
|
95
|
+
|
|
94
96
|
return unless workspace.is_a?(Hash) && organization.is_a?(Hash)
|
|
95
97
|
return unless defined?(Machina::WorkspaceRef) && Machina::WorkspaceRef.table_exists?
|
|
96
98
|
|
|
@@ -16,7 +16,8 @@ module Machina
|
|
|
16
16
|
|
|
17
17
|
product_id = manifest[:product_id] || Machina.config.product_id
|
|
18
18
|
if product_id.blank?
|
|
19
|
-
raise Machina::ConfigurationError,
|
|
19
|
+
raise Machina::ConfigurationError,
|
|
20
|
+
'product_id is required for permission sync (set in machina.yml or Machina.config)'
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
Machina.identity_client.sync_permissions(
|
data/lib/machina/version.rb
CHANGED
data/lib/machina.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Machina
|
|
|
32
32
|
|
|
33
33
|
# Test helpers are opt-in; load with `require "machina/test_helpers"` or autoload
|
|
34
34
|
autoload :TestHelpers, 'machina/test_helpers'
|
|
35
|
+
|
|
35
36
|
class << self
|
|
36
37
|
def configure
|
|
37
38
|
yield(config)
|
|
@@ -54,15 +55,36 @@ module Machina
|
|
|
54
55
|
config.cache_store || Rails.cache
|
|
55
56
|
end
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
# Builds the Console authorize URL.
|
|
59
|
+
#
|
|
60
|
+
# @param redirect_to [String, nil] explicit redirect URL (backwards compat)
|
|
61
|
+
# @param return_to [String, nil] user's intended destination, appended to
|
|
62
|
+
# the configured +identity_callback_uri+ as a query param
|
|
63
|
+
# @return [String] the full authorize URL
|
|
64
|
+
# @raise [ConfigurationError] when neither +redirect_to+ nor
|
|
65
|
+
# +identity_callback_uri+ is available
|
|
66
|
+
def authorize_url(redirect_to: nil, return_to: nil)
|
|
58
67
|
base = config.identity_service_url.to_s.sub(%r{/\z}, '')
|
|
59
|
-
return "#{base}/authorize" if redirect_to.blank?
|
|
60
68
|
|
|
61
|
-
"#{base}/authorize?redirect_to=#{CGI.escape(redirect_to)}"
|
|
69
|
+
return "#{base}/authorize?redirect_to=#{CGI.escape(redirect_to)}" if redirect_to.present?
|
|
70
|
+
|
|
71
|
+
callback = config.identity_callback_uri
|
|
72
|
+
if callback.blank?
|
|
73
|
+
raise ConfigurationError,
|
|
74
|
+
'identity_callback_uri must be configured to use authorize_url without an explicit redirect_to'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
target = return_to.present? ? "#{callback}?return_to=#{CGI.escape(return_to)}" : callback
|
|
78
|
+
|
|
79
|
+
"#{base}/authorize?redirect_to=#{CGI.escape(target)}"
|
|
62
80
|
end
|
|
63
81
|
|
|
82
|
+
# Convenience wrapper that delegates to {authorize_url} with +return_to+.
|
|
83
|
+
#
|
|
84
|
+
# @param return_to [String] the user's intended destination
|
|
85
|
+
# @return [String] the full authorize URL
|
|
64
86
|
def login_url(return_to:)
|
|
65
|
-
authorize_url(
|
|
87
|
+
authorize_url(return_to:)
|
|
66
88
|
end
|
|
67
89
|
end
|
|
68
90
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../rails_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Machina.authorize_url' do
|
|
6
|
+
let(:base_url) { 'https://machina.example.test' }
|
|
7
|
+
|
|
8
|
+
context 'when identity_callback_uri is configured' do
|
|
9
|
+
before do
|
|
10
|
+
Machina.config.identity_callback_uri = 'http://localhost:3000/auth/machina/callback'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'uses the callback URI as redirect_to' do
|
|
14
|
+
url = Machina.authorize_url
|
|
15
|
+
|
|
16
|
+
expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape('http://localhost:3000/auth/machina/callback')}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'appends return_to as a query param on the callback URI' do
|
|
20
|
+
url = Machina.authorize_url(return_to: '/dashboard')
|
|
21
|
+
|
|
22
|
+
callback_with_return = 'http://localhost:3000/auth/machina/callback?return_to=%2Fdashboard'
|
|
23
|
+
expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape(callback_with_return)}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'encodes a full return_to URL' do
|
|
27
|
+
url = Machina.authorize_url(return_to: 'http://localhost:3000/inquiries?page=2')
|
|
28
|
+
|
|
29
|
+
callback_with_return = 'http://localhost:3000/auth/machina/callback?return_to=http%3A%2F%2Flocalhost%3A3000%2Finquiries%3Fpage%3D2'
|
|
30
|
+
expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape(callback_with_return)}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context 'when identity_callback_uri is not configured' do
|
|
35
|
+
it 'raises a configuration error when called without redirect_to' do
|
|
36
|
+
expect { Machina.authorize_url }.to raise_error(
|
|
37
|
+
Machina::ConfigurationError,
|
|
38
|
+
/identity_callback_uri/,
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'still works with an explicit redirect_to for backwards compatibility' do
|
|
43
|
+
url = Machina.authorize_url(redirect_to: 'http://localhost:3000/login')
|
|
44
|
+
|
|
45
|
+
expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape('http://localhost:3000/login')}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '.login_url' do
|
|
50
|
+
before do
|
|
51
|
+
Machina.config.identity_callback_uri = 'http://localhost:3000/auth/machina/callback'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'delegates to authorize_url with return_to' do
|
|
55
|
+
url = Machina.login_url(return_to: '/settings')
|
|
56
|
+
|
|
57
|
+
callback_with_return = 'http://localhost:3000/auth/machina/callback?return_to=%2Fsettings'
|
|
58
|
+
expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape(callback_with_return)}")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -12,4 +12,10 @@ RSpec.describe Machina::Configuration do
|
|
|
12
12
|
config.product_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
|
13
13
|
expect(config.product_id).to eq('a1b2c3d4-e5f6-7890-abcd-ef1234567890')
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
it 'supports identity_callback_uri configuration' do
|
|
17
|
+
config = described_class.new
|
|
18
|
+
config.identity_callback_uri = 'http://localhost:3000/auth/machina/callback'
|
|
19
|
+
expect(config.identity_callback_uri).to eq('http://localhost:3000/auth/machina/callback')
|
|
20
|
+
end
|
|
15
21
|
end
|
|
@@ -29,12 +29,15 @@ RSpec.describe Machina::ControllerHelpers, type: :controller do
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
it 'redirects browser requests to authorize when unauthenticated' do
|
|
32
|
+
it 'redirects browser requests to authorize using identity_callback_uri when unauthenticated' do
|
|
33
|
+
Machina.config.identity_callback_uri = 'http://localhost:3000/auth/machina/callback'
|
|
34
|
+
|
|
33
35
|
get :index
|
|
34
36
|
|
|
35
37
|
expect(response).to have_http_status(:redirect)
|
|
36
38
|
expect(response.location).to include('https://machina.example.test/authorize')
|
|
37
39
|
expect(response.location).to include('redirect_to=')
|
|
40
|
+
expect(CGI.unescape(response.location)).to include('http://localhost:3000/auth/machina/callback?return_to=')
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
it 'returns json unauthorized for api-style requests' do
|
|
@@ -54,6 +54,52 @@ RSpec.describe Machina::Middleware::Authentication do
|
|
|
54
54
|
expect(JSON.parse(body.first)).to eq('error' => 'unauthorized')
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
it 'evicts cached session when Console returns non-200 on stale re-fetch' do
|
|
58
|
+
token = 'ps_stale_token'
|
|
59
|
+
cache_key = "machina:session:#{token}"
|
|
60
|
+
stale_data = MockResponses.session_resolution_minimal['data'].merge(stale: true)
|
|
61
|
+
|
|
62
|
+
Machina.cache.write(cache_key, stale_data, expires_in: 5.minutes)
|
|
63
|
+
|
|
64
|
+
allow(identity_client).to receive(:resolve_session).with(token).and_return(
|
|
65
|
+
Machina::IdentityClient::Response.new(status: 404, body: '{}'),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
env = Rack::MockRequest.env_for('/resource', 'HTTP_AUTHORIZATION' => "Bearer #{token}")
|
|
69
|
+
status, = middleware.call(env)
|
|
70
|
+
|
|
71
|
+
expect(status).to eq(401)
|
|
72
|
+
expect(Machina.cache.read(cache_key)).to be_nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'evicts cached session when Console returns non-200 after cache expires' do
|
|
76
|
+
token = 'ps_expired_cache'
|
|
77
|
+
cache_key = "machina:session:#{token}"
|
|
78
|
+
|
|
79
|
+
# First request: populate cache via successful resolution
|
|
80
|
+
allow(identity_client).to receive(:resolve_session).with(token).and_return(
|
|
81
|
+
Machina::IdentityClient::Response.new(status: 200, body: MockResponses.session_resolution_minimal),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
env = Rack::MockRequest.env_for('/resource', 'HTTP_AUTHORIZATION' => "Bearer #{token}")
|
|
85
|
+
status, = middleware.call(env)
|
|
86
|
+
expect(status).to eq(200)
|
|
87
|
+
|
|
88
|
+
# Simulate cache expiry
|
|
89
|
+
Machina.cache.delete(cache_key)
|
|
90
|
+
|
|
91
|
+
# Console now rejects the token
|
|
92
|
+
allow(identity_client).to receive(:resolve_session).with(token).and_return(
|
|
93
|
+
Machina::IdentityClient::Response.new(status: 404, body: '{}'),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
env = Rack::MockRequest.env_for('/resource', 'HTTP_AUTHORIZATION' => "Bearer #{token}")
|
|
97
|
+
status, = middleware.call(env)
|
|
98
|
+
|
|
99
|
+
expect(status).to eq(401)
|
|
100
|
+
expect(Machina.cache.read(cache_key)).to be_nil
|
|
101
|
+
end
|
|
102
|
+
|
|
57
103
|
it 'uses the cache on subsequent requests' do
|
|
58
104
|
response = Machina::IdentityClient::Response.new(
|
|
59
105
|
status: 200,
|
|
@@ -106,7 +106,7 @@ RSpec.describe Machina::PermissionSync do
|
|
|
106
106
|
context 'with an ERB manifest' do
|
|
107
107
|
let(:tmpfile) do
|
|
108
108
|
file = Tempfile.new(['machina', '.yml'])
|
|
109
|
-
file.write(<<~
|
|
109
|
+
file.write(<<~YAML)
|
|
110
110
|
product_id: <%= "erb-product-id" %>
|
|
111
111
|
|
|
112
112
|
permissions:
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: machina-auth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ZAR
|
|
@@ -174,6 +174,7 @@ files:
|
|
|
174
174
|
- spec/dummy/config/routes.rb
|
|
175
175
|
- spec/dummy/db/schema.rb
|
|
176
176
|
- spec/fixtures/machina.yml
|
|
177
|
+
- spec/machina/authorize_url_spec.rb
|
|
177
178
|
- spec/machina/authorized_spec.rb
|
|
178
179
|
- spec/machina/configuration_spec.rb
|
|
179
180
|
- spec/machina/controller_helpers_spec.rb
|