machina-auth 0.1.4 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8b85487ba807943451cf345d17e7aec7ee18a9b61a233adcdb213f86134cde0
4
- data.tar.gz: 49908d36c4fb879354aa6c3b9ae96e33eaed76d98df2cd3dbfb8a51ce72b03a4
3
+ metadata.gz: 779a01d0ee76aefd789279497b06a5eb82bafc444d783d6f279cf75e31e37b57
4
+ data.tar.gz: 3533cf0c697aa3698225c9527df313f7d09643cd04e641af597d0b0d874ab518
5
5
  SHA512:
6
- metadata.gz: 97716d4c519933b65a8410615655adcf7e70a4013d9595e7ef5ca4a96ae2a57c44574640b33de5dc1adcd675b626c2ae39d09ffd0c5cf4fbcbb2932924988be9
7
- data.tar.gz: 868b463264e64e8551c938704bcfad481714e0d6a9f4aa8dcc6152eb4d8df1bda0c6a01ff2ab55f89e534ceeb03b1d53abed3792dc2afaa1de660c6726dd2e2c
6
+ metadata.gz: 4d896e66034311c92ef6b5b99f1ffd6d0adfa8033f78922305d354c5009500e516ebbd49b766f615758cbaee4003246017a2fed1209cdbbb2539b3e614e78d5f
7
+ data.tar.gz: e7276fa8589250b587754784882592380d20bf30e05b0dcf71b352b7d5664375decc54907c18821d8369e4aa83ae5f6c00d208ff0efe8f1ecd0f8e86873a3c00
@@ -75,8 +75,8 @@ module Machina
75
75
  end
76
76
 
77
77
  def assign_session_attrs(data)
78
- @session_id = data.dig('session', 'id') || data['session_id']
79
78
  @type = data.dig('session', 'type')&.to_sym
79
+ @session_id = data.dig('session', 'id') || data['session_id']
80
80
  raw_expires = data.dig('session', 'expires_at') || data['expires_at']
81
81
  @expires_at = raw_expires ? Time.zone.parse(raw_expires) : nil
82
82
  end
@@ -8,20 +8,11 @@ module Machina
8
8
 
9
9
  included do
10
10
  helper_method :authorized, :logged_in? if respond_to?(:helper_method)
11
-
12
- rescue_from Machina::Unauthorized do |error|
13
- if request.format.json?
14
- render json: { error: 'forbidden', permission: error.message }, status: :forbidden
15
- else
16
- redirect_to main_app.respond_to?(:root_path) ? main_app.root_path : '/',
17
- alert: "You don't have permission to do that."
18
- end
19
- end
11
+ rescue_from Machina::Unauthorized, with: :respond_unauthorized
20
12
  end
21
13
 
22
14
  def authorized
23
- current = defined?(::Current) ? ::Current : Machina::Current
24
- current.authorized || Machina::Authorized::EMPTY
15
+ Machina::Current.authorized || Machina::Authorized::EMPTY
25
16
  end
26
17
 
27
18
  def logged_in?
@@ -58,6 +49,15 @@ module Machina
58
49
 
59
50
  private
60
51
 
52
+ def respond_unauthorized(error)
53
+ if request.format.json?
54
+ render json: { error: 'forbidden', permission: error.message }, status: :forbidden
55
+ else
56
+ path = main_app.respond_to?(:root_path) ? main_app.root_path : '/'
57
+ redirect_to path, alert: "You don't have permission to do that."
58
+ end
59
+ end
60
+
61
61
  def extract_bearer_token
62
62
  header = request.headers['Authorization'].to_s
63
63
  match = header.match(/\ABearer\s+(.+)\z/)
@@ -1,7 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Machina
4
- # Thread-safe per-request store for the currently authenticated Authorized object.
4
+ # Thread-safe per-request store for the currently authenticated {Authorized} object.
5
+ #
6
+ # The middleware writes exclusively to +Machina::Current.authorized+. Host
7
+ # apps that want +::Current.authorized+ should inherit from this class:
8
+ #
9
+ # class Current < Machina::Current
10
+ # # add app-specific attributes here
11
+ # end
12
+ #
13
+ # Note: each +CurrentAttributes+ subclass has independent thread-local
14
+ # storage, so +::Current.authorized+ and +Machina::Current.authorized+ hold
15
+ # separate values even when +::Current < Machina::Current+. The gem always
16
+ # reads and writes through +Machina::Current+.
5
17
  class Current < ActiveSupport::CurrentAttributes
6
18
  attribute :authorized
7
19
  end
@@ -42,9 +42,9 @@ module Machina
42
42
  ensure_configured!
43
43
 
44
44
  response = connection.post(path) do |request|
45
- request.headers['Authorization'] = "Bearer #{config.service_token}"
46
- request.headers['Content-Type'] = 'application/json'
47
45
  request.headers['Accept'] = 'application/json'
46
+ request.headers['Content-Type'] = 'application/json'
47
+ request.headers['Authorization'] = "Bearer #{config.service_token}"
48
48
  request.body = JSON.generate(payload)
49
49
  end
50
50
 
@@ -19,16 +19,20 @@ module Machina
19
19
  return @app.call(env) if token.blank?
20
20
 
21
21
  session_data = resolve_session(token)
22
- return unauthorized_response unless session_data
23
22
 
24
- authorized = Machina::Authorized.new(session_data)
25
- Machina::Current.authorized = authorized
26
- ::Current.authorized = authorized if defined?(::Current) && ::Current != Machina::Current
23
+ # Token present but invalid/expired: clear the stale cache entry and
24
+ # fall through with nil authorized. API callers get 401 from the
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
30
+
31
+ Machina::Current.authorized = Machina::Authorized.new(session_data)
27
32
 
28
33
  @app.call(env)
29
34
  ensure
30
35
  Machina::Current.reset
31
- ::Current.reset if defined?(::Current) && ::Current != Machina::Current
32
36
  end
33
37
 
34
38
  private
@@ -53,7 +57,10 @@ module Machina
53
57
  end
54
58
 
55
59
  def extract_token(request)
56
- request.cookies['machina_session'] || extract_bearer(request) || request.headers['X-Api-Key'] || request.params['token']
60
+ request.cookies['machina_session']
61
+ || extract_bearer(request)
62
+ || request.headers['X-Api-Key']
63
+ || request.params['token']
57
64
  end
58
65
 
59
66
  def extract_bearer(request)
@@ -90,16 +97,18 @@ module Machina
90
97
  end
91
98
 
92
99
  def cache_workspace_ref(data)
93
- workspace = data['workspace']
100
+ workspace = data['workspace']
94
101
  organization = data['organization']
95
102
 
96
103
  return unless workspace.is_a?(Hash) && organization.is_a?(Hash)
97
104
  return unless defined?(Machina::WorkspaceRef) && Machina::WorkspaceRef.table_exists?
98
105
 
99
106
  ref = Machina::WorkspaceRef.find_or_initialize_by(tenant_ref: workspace['id'])
100
- ref.organization_id = organization['id']
107
+
101
108
  ref.name = workspace['name']
102
109
  ref.cached_at = Time.current
110
+ ref.organization_id = organization['id']
111
+
103
112
  ref.save!
104
113
  rescue StandardError
105
114
  nil
@@ -108,11 +117,6 @@ module Machina
108
117
  def cache_key(token)
109
118
  "machina:session:#{token}"
110
119
  end
111
-
112
- def unauthorized_response
113
- body = JSON.generate(error: 'unauthorized')
114
- [401, { 'Content-Type' => 'application/json', 'Content-Length' => body.bytesize.to_s }, [body]]
115
- end
116
120
  end
117
121
  end
118
122
  end
@@ -14,16 +14,14 @@ module Machina
14
14
  def call!
15
15
  manifest = load_manifest
16
16
 
17
- product_id = manifest[:product_id] || Machina.config.product_id
18
- if product_id.blank?
19
- raise Machina::ConfigurationError,
20
- '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)'
21
19
  end
22
20
 
23
21
  Machina.identity_client.sync_permissions(
24
- product_id:,
25
- permissions: manifest.fetch(:permissions),
22
+ product_id: Machina.config.product_id,
26
23
  policies: manifest.fetch(:policies, []),
24
+ permissions: manifest.fetch(:permissions),
27
25
  )
28
26
  end
29
27
 
@@ -63,10 +63,10 @@ module Machina
63
63
  data = default_session_data
64
64
 
65
65
  data['user'].merge!(stringify(user))
66
- data['organization'].merge!(stringify(organization))
66
+ data['session'].merge!(stringify(session))
67
67
  data['workspace'].merge!(stringify(workspace))
68
+ data['organization'].merge!(stringify(organization))
68
69
  data['workspace']['id'] = tenant_ref.to_s if tenant_ref
69
- data['session'].merge!(stringify(session))
70
70
  data['permissions'] = permissions.map(&:to_s) if permissions
71
71
 
72
72
  authorized = Machina::Authorized.new(data)
@@ -87,9 +87,9 @@ module Machina
87
87
  def default_session_data
88
88
  {
89
89
  'user' => fake_user_data,
90
- 'organization' => fake_organization_data,
91
- 'workspace' => fake_workspace_data,
92
90
  'session' => fake_session_data,
91
+ 'workspace' => fake_workspace_data,
92
+ 'organization' => fake_organization_data,
93
93
  'permissions' => []
94
94
  }
95
95
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Machina
4
- VERSION = '0.1.4'
4
+ VERSION = '0.1.6'
5
5
  end
@@ -5,13 +5,15 @@ module Machina
5
5
  # invalidating or marking cached sessions as stale when permissions change.
6
6
  class WebhookReceiver
7
7
  def initialize(request, cache: Machina.cache)
8
- @request = request
9
8
  @cache = cache
9
+ @request = request
10
10
  @raw_body = request.body.read
11
+
11
12
  request.body.rewind if request.body.respond_to?(:rewind)
12
- @payload = @raw_body.present? ? JSON.parse(@raw_body) : {}
13
+
13
14
  @event = request.headers['X-Machina-Event']
14
15
  @signature = request.headers['X-Machina-Signature'].to_s
16
+ @payload = @raw_body.present? ? JSON.parse(@raw_body) : {}
15
17
  end
16
18
 
17
19
  def valid?
@@ -12,8 +12,7 @@ module Machina
12
12
  validates :tenant_ref, presence: true
13
13
 
14
14
  scope :in_current_workspace, lambda {
15
- current = defined?(::Current) ? ::Current : Machina::Current
16
- where(tenant_ref: current.authorized&.workspace_id)
15
+ where(tenant_ref: Machina::Current.authorized&.workspace_id)
17
16
  }
18
17
  end
19
18
  end
data/lib/machina.rb CHANGED
@@ -57,26 +57,28 @@ module Machina
57
57
 
58
58
  # Builds the Console authorize URL.
59
59
  #
60
- # @param redirect_to [String, nil] explicit redirect URL (backwards compat)
60
+ # The Console's +/authorize+ endpoint requires a +redirect_to+ query param
61
+ # so it knows where to send the user after workspace selection. This method
62
+ # always produces that param — the two keyword arguments control how:
63
+ #
64
+ # 1. *Callback-based (preferred)* — uses the configured
65
+ # +identity_callback_uri+ as the redirect target. Pass +return_to+ to
66
+ # append the user's intended destination as a query param on the callback.
67
+ # 2. *Explicit* — pass +redirect_to+ directly to bypass callback resolution.
68
+ # Intended for backwards compatibility only.
69
+ #
70
+ # @param redirect_to [String, nil] explicit product redirect URL; when
71
+ # present the callback URI config is ignored
61
72
  # @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
73
+ # +identity_callback_uri+ so the product app can restore it after auth
74
+ # @return [String] the full Console authorize URL
75
+ # @raise [ConfigurationError] when +redirect_to+ is omitted and
76
+ # +identity_callback_uri+ is not configured
66
77
  def authorize_url(redirect_to: nil, return_to: nil)
67
- base = config.identity_service_url.to_s.sub(%r{/\z}, '')
68
-
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
+ redirect_target = redirect_to.presence || callback_redirect_target(return_to)
78
79
 
79
- "#{base}/authorize?redirect_to=#{CGI.escape(target)}"
80
+ base = config.identity_service_url.to_s.sub(%r{/\z}, '')
81
+ "#{base}/authorize?redirect_to=#{CGI.escape(redirect_target)}"
80
82
  end
81
83
 
82
84
  # Convenience wrapper that delegates to {authorize_url} with +return_to+.
@@ -86,5 +88,24 @@ module Machina
86
88
  def login_url(return_to:)
87
89
  authorize_url(return_to:)
88
90
  end
91
+
92
+ private
93
+
94
+ # Resolves the redirect target from +identity_callback_uri+ config,
95
+ # optionally appending a +return_to+ query param.
96
+ #
97
+ # @param return_to [String, nil] path or URL the user wants after auth
98
+ # @return [String] the callback URI (with optional +return_to+)
99
+ # @raise [ConfigurationError] when +identity_callback_uri+ is blank
100
+ def callback_redirect_target(return_to)
101
+ callback = config.identity_callback_uri
102
+
103
+ if callback.blank?
104
+ raise ConfigurationError,
105
+ 'identity_callback_uri must be configured to use authorize_url without an explicit redirect_to'
106
+ end
107
+
108
+ return_to.present? ? "#{callback}?return_to=#{CGI.escape(return_to)}" : callback
109
+ end
89
110
  end
90
111
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../rails_helper'
4
+ require_relative '../support/mock_responses'
5
+
6
+ RSpec.describe Machina::Current do
7
+ let(:data) { MockResponses.session_resolution['data'] }
8
+ let(:authorized) { Machina::Authorized.new(data) }
9
+
10
+ describe '.authorized' do
11
+ it 'returns the value set on Machina::Current' do
12
+ described_class.authorized = authorized
13
+
14
+ expect(described_class.authorized).to eq(authorized)
15
+ end
16
+
17
+ it 'is independent from host ::Current even when it inherits' do
18
+ # The dummy app's ::Current inherits from Machina::Current,
19
+ # but each CurrentAttributes subclass has its own thread-local storage.
20
+ described_class.authorized = authorized
21
+
22
+ expect(Current.authorized).to be_nil
23
+ expect(described_class.authorized).to eq(authorized)
24
+ ensure
25
+ Current.reset
26
+ end
27
+
28
+ it 'returns nil when nothing is set' do
29
+ expect(described_class.authorized).to be_nil
30
+ end
31
+ end
32
+ end
@@ -42,7 +42,7 @@ RSpec.describe Machina::Middleware::Authentication do
42
42
  expect(workspace_ref).to have_attributes(name: 'Primary')
43
43
  end
44
44
 
45
- it 'returns unauthorized when resolution fails' do
45
+ it 'passes through with nil authorized when resolution fails' do
46
46
  allow(identity_client).to receive(:resolve_session).and_return(
47
47
  Machina::IdentityClient::Response.new(status: 404, body: '{}'),
48
48
  )
@@ -50,11 +50,11 @@ RSpec.describe Machina::Middleware::Authentication do
50
50
  env = Rack::MockRequest.env_for('/resource', 'HTTP_AUTHORIZATION' => 'Bearer ps_123')
51
51
  status, _headers, body = middleware.call(env)
52
52
 
53
- expect(status).to eq(401)
54
- expect(JSON.parse(body.first)).to eq('error' => 'unauthorized')
53
+ expect(status).to eq(200)
54
+ expect(JSON.parse(body.first)).to eq('user_id' => nil, 'permissions' => nil)
55
55
  end
56
56
 
57
- it 'evicts cached session when Console returns non-200 on stale re-fetch' do
57
+ it 'evicts cached session and passes through when Console returns non-200 on stale re-fetch' do
58
58
  token = 'ps_stale_token'
59
59
  cache_key = "machina:session:#{token}"
60
60
  stale_data = MockResponses.session_resolution_minimal['data'].merge(stale: true)
@@ -68,7 +68,7 @@ RSpec.describe Machina::Middleware::Authentication do
68
68
  env = Rack::MockRequest.env_for('/resource', 'HTTP_AUTHORIZATION' => "Bearer #{token}")
69
69
  status, = middleware.call(env)
70
70
 
71
- expect(status).to eq(401)
71
+ expect(status).to eq(200)
72
72
  expect(Machina.cache.read(cache_key)).to be_nil
73
73
  end
74
74
 
@@ -96,7 +96,7 @@ RSpec.describe Machina::Middleware::Authentication do
96
96
  env = Rack::MockRequest.env_for('/resource', 'HTTP_AUTHORIZATION' => "Bearer #{token}")
97
97
  status, = middleware.call(env)
98
98
 
99
- expect(status).to eq(401)
99
+ expect(status).to eq(200)
100
100
  expect(Machina.cache.read(cache_key)).to be_nil
101
101
  end
102
102
 
@@ -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 = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
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: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
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: 'flat-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 do
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: 'test-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 do
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: 'erb-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: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
146
+ product_id: config_product_id,
187
147
  permissions: [{ key: 'items.view', description: 'View items' }],
188
148
  policies: [],
189
149
  )
@@ -19,7 +19,7 @@ RSpec.describe Machina::WorkspaceScoped do
19
19
  end
20
20
 
21
21
  it 'filters records to the current workspace' do
22
- Current.authorized = Machina::Authorized.new(
22
+ Machina::Current.authorized = Machina::Authorized.new(
23
23
  'user' => { 'id' => 'u1' },
24
24
  'organization' => { 'id' => 'o1' },
25
25
  'workspace' => { 'id' => 'ws-1', 'name' => 'Test' },
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
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - ZAR
@@ -178,6 +178,7 @@ files:
178
178
  - spec/machina/authorized_spec.rb
179
179
  - spec/machina/configuration_spec.rb
180
180
  - spec/machina/controller_helpers_spec.rb
181
+ - spec/machina/current_spec.rb
181
182
  - spec/machina/identity_client_spec.rb
182
183
  - spec/machina/middleware/authentication_spec.rb
183
184
  - spec/machina/middleware/skip_paths_spec.rb