cased-rails 0.3.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d0e3655a9bc7ff8cce27657993146a0eda10999481d2c89194c88bd5f601262
4
- data.tar.gz: 78a0974005fe163ff98c154a8848bbdeb280af5d757c2f6330b6de7a482b8b5e
3
+ metadata.gz: 6ac8f2d72b0bd9acb03474b7903c2d7d061c6a95d99beb5ea46d8b18dfa2fe37
4
+ data.tar.gz: 17c1ea4d0eaa1c44a365d51b1a88b04a3c5a3c479f6318ab33bdd03c5163ea02
5
5
  SHA512:
6
- metadata.gz: 4deb9a511c4079537c25af408aebac3342ce39ed8706874e99e513cb1c66e05ef141d5927dabda1cd2f100930a991dbb4bef37dda340de3604456ccaf74b6aad
7
- data.tar.gz: 61e3279ca41dab08c8a92cbc47876bf4f147976283e83f7c68798f637efacce07d79ecc25aacbd9fa3d5f3f2ad9517f90b472a234e2fd5ef0442454fc64792ab
6
+ metadata.gz: 00c9c0d383329eb6e53432458a578db0c795107822bafcdcb8249bcc5432541ac3b5c7609b288110c8e6b9d531c6ac211399a4b43aeb1988115a28ad03b1bab6
7
+ data.tar.gz: b9d292a6ea549455cd9f595fe4d764910abfaa9e0f5331595359fb2a3520deec48479c5bf6591606df1e74e40958825b34d3c2b98af8cd7c2a4ec37a3544258b
data/README.md CHANGED
@@ -7,15 +7,19 @@ A Cased client for Ruby on Rails applications in your organization to control an
7
7
  - [Installation](#installation)
8
8
  - [Configuration](#configuration)
9
9
  - [Usage](#usage)
10
- - [Publishing events to Cased](#publishing-events-to-cased)
11
- - [Publishing audit events for all record creation, updates, and deletions automatically](#publishing-audit-events-for-all-record-creation-updates-and-deletions-automatically)
12
- - [Retrieving events from a Cased audit trail](#retrieving-events-from-a-cased-audit-trail)
13
- - [Retrieving events from multiple Cased audit trails](#retrieving-events-from-multiple-cased-audit-trails)
14
- - [Exporting events](#exporting-events)
15
- - [Masking & filtering sensitive information](#masking-and-filtering-sensitive-information)
16
- - [Disable publishing events](#disable-publishing-events)
17
- - [Context](#context)
18
- - [Testing](#testing)
10
+ - [Cased CLI](#cased-cli)
11
+ - [Recording console sessions](#recording-console-sessions)
12
+ - [Approval workflows for sensitive operations](#approval-workflows-for-sensitive-operations)
13
+ - [Audit trails](#audit-trails)
14
+ - [Publishing events to Cased](#publishing-events-to-cased)
15
+ - [Publishing audit events for all record creation, updates, and deletions automatically](#publishing-audit-events-for-all-record-creation-updates-and-deletions-automatically)
16
+ - [Retrieving events from a Cased audit trail](#retrieving-events-from-a-cased-audit-trail)
17
+ - [Retrieving events from multiple Cased audit trails](#retrieving-events-from-multiple-cased-audit-trails)
18
+ - [Exporting events](#exporting-events)
19
+ - [Masking & filtering sensitive information](#masking-and-filtering-sensitive-information)
20
+ - [Disable publishing events](#disable-publishing-events)
21
+ - [Context](#context)
22
+ - [Testing](#testing)
19
23
  - [Customizing cased-rails](#customizing-cased-rails)
20
24
  - [Contributing](#contributing)
21
25
 
@@ -41,6 +45,26 @@ All configuration options available in cased-rails are available to be configure
41
45
 
42
46
  ```ruby
43
47
  Cased.configure do |config|
48
+ # GUARD_APPLICATION_KEY=guard_application_1ntKX0P4vUbKoc0lMWGiSbrBHcH
49
+ config.guard_application_key = 'guard_application_1ntKX0P4vUbKoc0lMWGiSbrBHcH'
50
+
51
+ # GUARD_USER_TOKEN=user_1oFqlROLNRGVLOXJSsHkJiVmylr
52
+ config.guard_user_token = 'user_1oFqlROLNRGVLOXJSsHkJiVmylr'
53
+
54
+ # DENY_IF_UNREACHABLE=1
55
+ config.guard_deny_if_unreachable = true
56
+
57
+ # Attach metadata to all CLI requests. This metadata will appear in Cased and
58
+ # any notification source such as email or Slack.
59
+ #
60
+ # You are limited to 20 properties and cannot be a nested dictionary. Metadata
61
+ # specified in the CLI request overrides any configured globally.
62
+ config.cli.metadata = {
63
+ rails_env: ENV['RAILS_ENV'],
64
+ heroku_application: ENV['HEROKU_APP_NAME'],
65
+ git_commit: ENV['GIT_COMMIT'],
66
+ }
67
+
44
68
  # CASED_POLICY_KEY=policy_live_1dQpY5JliYgHSkEntAbMVzuOROh
45
69
  config.policy_key = 'policy_live_1dQpY5JliYgHSkEntAbMVzuOROh'
46
70
 
@@ -76,7 +100,119 @@ end
76
100
 
77
101
  ## Usage
78
102
 
79
- ### Publishing events to Cased
103
+ ### Cased CLI
104
+
105
+ #### Playback console sessions
106
+
107
+ Having visibility into production terminal sessions is essential to providing
108
+ access to sensitive data and critical systems. `cased-rails` can provide complete
109
+ command line session recordings with minimal configuration.
110
+
111
+ First, enable the "Record output" option in your application's settings page on Cased.
112
+
113
+ Next grab the application's key from the same settings page and configure
114
+ `cased-rails` with it either by using an environment variable or manually.
115
+
116
+ **Environment variable**
117
+
118
+ ```
119
+ GUARD_APPLICATION_KEY=guard_application_1rBCh8o3YMaI1eAKxbrNvnLki3x rails console
120
+ ```
121
+
122
+ **Manually**
123
+
124
+ ```ruby
125
+ Cased.configure do |config|
126
+ config.guard_application_key = 'guard_application_1rBCh8o3YMaI1eAKxbrNvnLki3x'
127
+ end
128
+ ```
129
+
130
+ By default playback will be saved only when a Rails console is started outside
131
+ of development and test. When the playback is being saved, by default all
132
+ parameters other than `id`, `action`, and `controller` will be filtered out.
133
+ For example:
134
+
135
+ ```
136
+ #<User id: "user_1qwkKB8IGxQFlu3C4lI53tCIyZI", organization: "Enterprise">
137
+ ```
138
+
139
+ Would become:
140
+
141
+ ```
142
+ #<User id: "user_1qwkKB8IGxQFlu3C4lI53tCIyZI", organization: [FILTERED]>
143
+ ```
144
+
145
+ If you'd like to configure if filtering is enabled or specify which attributes
146
+ are not filtered you can do so with:
147
+
148
+ ```ruby
149
+ Cased.configure do |config|
150
+ config.unfiltered_parameters = ['id', 'action', 'controller']
151
+ config.filter_parameters = Rails.env.production?
152
+ end
153
+ ```
154
+
155
+ #### Approval workflows for sensitive operations
156
+
157
+ Adding approval workflows to your controllers is a two step process in your
158
+ Rails applications.
159
+
160
+ First, mount the Rails engine in your routes. The included Rails engine in
161
+ cased-rails is necessary for the approval workflow to know whether or not it has
162
+ been requested, approved, denied, canceled or timed out.
163
+
164
+ ```ruby
165
+ Rails.application.routes.draw do
166
+ mount Cased::Rails::Engine => '/cased'
167
+
168
+ root to: 'home#show'
169
+ end
170
+ ```
171
+
172
+ To control the requirements for an approval workflow, that must be configured
173
+ within your CLI application settings on Cased. Some controls include restricting
174
+ which users or groups can approve the request, if a reason is required, how long
175
+ until the request times out, and more.
176
+
177
+ To start an your approval workflow all that is needed is to call the `guard`
178
+ method before a request using `before_action`.
179
+
180
+ ```ruby
181
+ class AccountsController < ApplicationController
182
+ before_action :guard, only: %i[update destroy]
183
+
184
+ def update
185
+ if current_account.update(account_params)
186
+ redirect_to current_account
187
+ else
188
+ render :edit
189
+ end
190
+ end
191
+
192
+ def destroy
193
+ if current_account.destroy
194
+ redirect_to accounts_path
195
+ else
196
+ redirect_to current_account
197
+ end
198
+ end
199
+
200
+ private
201
+
202
+ def account_params
203
+ params.require(:account).permit(:name, :description, :email)
204
+ end
205
+ end
206
+ ```
207
+
208
+ Approval workflows are best started just before data is about to be created,
209
+ updated, or destroyed. Approval workflows are not intended to control permission
210
+ to view resources. The actions we recommend guarding are `create`, `update`, and
211
+ `destroy` based on your needs.
212
+
213
+ ### Audit trails
214
+
215
+ #### Publishing events to Cased
80
216
 
81
217
  Once Cased is setup there are two ways to publish your first audit trail event.
82
218
  The first is using the `cased` helper method included in all ActiveRecord models.
@@ -151,7 +287,7 @@ end
151
287
 
152
288
  By publishing the `team.create` audit event within the controller directly as shown you risk not having a complete and comprehensive audit trail for each team created in your application as it may happen in your API, model callbacks, and more.
153
289
 
154
- ### Publishing audit events for all record creation, updates, and deletions automatically
290
+ #### Publishing audit events for all record creation, updates, and deletions automatically
155
291
 
156
292
  Cased provides a mixin you can include in your models or in `ApplicationRecord` to automatically publish when new models are created, updated, or destroyed.
157
293
 
@@ -173,7 +309,7 @@ end
173
309
 
174
310
  This mixin is intended to get you up and running quickly. You'll likely need to configure your own callbacks to control what exactly gets published to Cased.
175
311
 
176
- ### Retrieving events from a Cased audit trail
312
+ #### Retrieving events from a Cased audit trail
177
313
 
178
314
  If you plan on retrieving events from your audit trails to power a user facing audit trail or API you must use a Cased API key.
179
315
 
@@ -200,7 +336,7 @@ class AuditTrailController < ApplicationController
200
336
  end
201
337
  ```
202
338
 
203
- ### Retrieving events from multiple Cased audit trails
339
+ #### Retrieving events from multiple Cased audit trails
204
340
 
205
341
  To retrieve events from one or more Cased audit trails you can configure multiple Cased API keys and retrieve events for each one by fetching their respective clients.
206
342
 
@@ -227,7 +363,7 @@ results.each do |event|
227
363
  end
228
364
  ```
229
365
 
230
- ### Exporting events
366
+ #### Exporting events
231
367
 
232
368
  Exporting events from Cased allows you to provide users with exports of their own data or to respond to data requests.
233
369
 
@@ -243,7 +379,7 @@ export = Cased.policy.exports.create(
243
379
  export.download_url # => https://api.cased.com/exports/export_1dSHQSNtAH90KA8zGTooMnmMdiD/download?token=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidXNlcl8xZFFwWThiQmdFd2RwbWRwVnJydER6TVg0ZkgiLCJ
244
380
  ```
245
381
 
246
- ### Masking & filtering sensitive information
382
+ #### Masking & filtering sensitive information
247
383
 
248
384
  If you are handling sensitive information on behalf of your users you should consider masking or filtering any sensitive information.
249
385
 
@@ -258,7 +394,7 @@ Cased.publish(
258
394
  )
259
395
  ```
260
396
 
261
- ### Console Usage
397
+ #### Console Usage
262
398
 
263
399
  Most Cased events will be created by users from actions on the website from
264
400
  custom defined events or lifecycle callbacks. The exception is any console
@@ -275,7 +411,7 @@ Rails.application.console do
275
411
  end
276
412
  ```
277
413
 
278
- ### Disable publishing events
414
+ #### Disable publishing events
279
415
 
280
416
  Although rare, there may be times where you wish to disable publishing events to Cased. To do so wrap your transaction inside of a `Cased.disable` block:
281
417
 
@@ -291,7 +427,7 @@ Or you can configure the entire process to disable publishing events.
291
427
  CASED_DISABLE_PUBLISHING=1 bundle exec ruby crawl.rb
292
428
  ```
293
429
 
294
- ### Context
430
+ #### Context
295
431
 
296
432
  When you include `cased-rails` in your application your Ruby on Rails application is configures a [Rack middleware](https://github.com/cased/cased-ruby/blob/master/lib/cased/rack_middleware.rb) that populates `Cased.context` with the following information for each request:
297
433
 
@@ -364,7 +500,7 @@ To clear/reset the context:
364
500
  Cased.context.clear
365
501
  ```
366
502
 
367
- ### Testing
503
+ #### Testing
368
504
 
369
505
  `cased-rails` provides a Cased::TestHelper test helper class that you can use to test events are being published to Cased.
370
506
 
@@ -0,0 +1,2 @@
1
+ //= link_tree ../../images/cased
2
+ //= link_directory ../../javascripts/cased .js
Binary file
Binary file
@@ -0,0 +1,75 @@
1
+ //= require rails-ujs
2
+
3
+ let windowReference = null;
4
+ let previousUrl = null;
5
+ let casedCreateSession = null;
6
+ let casedLoggedInContainer = null;
7
+ let casedLoggedOutContainer = null;
8
+ let casedUser = null;
9
+
10
+ // receiveMessage is the callback that is triggered when an authentication
11
+ // response is received from the new window we opened.
12
+ //
13
+ // We use this callback to update the user information in the UI and show the
14
+ // logged in container.
15
+ const receiveMessage = (event) => {
16
+ if (!event.isTrusted) {
17
+ return;
18
+ }
19
+
20
+ const { user } = event.data;
21
+ casedUser.innerText = user;
22
+ if (casedCreateSession) {
23
+ casedCreateSession.submit();
24
+ } else {
25
+ casedLoggedInContainer.classList.remove("hidden");
26
+ casedLoggedOutContainer.classList.add("hidden");
27
+ }
28
+ };
29
+
30
+ // openSignInWindow is used to present the Cased sign in window.
31
+ const openSignInWindow = (url) => {
32
+ window.removeEventListener("message", receiveMessage);
33
+ const windowFeatures =
34
+ "toolbar=no, menubar=no, width=600, height=700, top=50, left=200";
35
+
36
+ if (windowReference === null || windowReference.closed) {
37
+ windowReference = window.open(url, "Cased", windowFeatures);
38
+ } else if (previousUrl !== url) {
39
+ // If the window is already open and the previous URL was different, we need
40
+ // to load a new URL and refocus.
41
+ windowReference = window.open(url, "Cased", windowFeatures);
42
+ windowReference.focus();
43
+ } else {
44
+ windowReference.focus();
45
+ }
46
+
47
+ window.addEventListener("message", (event) => receiveMessage(event), false);
48
+ previousUrl = url;
49
+ };
50
+
51
+ window.addEventListener("DOMContentLoaded", (event) => {
52
+ // Global elements
53
+ casedCreateSession = document.getElementById("cased-create-session");
54
+ casedLoggedInContainer = document.getElementById("cased-logged-in");
55
+ casedLoggedOutContainer = document.getElementById("cased-logged-out");
56
+ casedUser = document.getElementById("cased-user");
57
+
58
+ // Local elements
59
+ const casedAuthenticate = document.getElementById("cased-authenticate");
60
+ if (casedAuthenticate) {
61
+ casedAuthenticate.addEventListener("click", (event) => {
62
+ event.preventDefault();
63
+
64
+ openSignInWindow(event.currentTarget.href);
65
+ });
66
+ }
67
+
68
+ const casedLogout = document.getElementById("cased-logout");
69
+ if (casedLogout) {
70
+ casedLogout.addEventListener("ajax:success", (_event) => {
71
+ casedLoggedInContainer.classList.add("hidden");
72
+ casedLoggedOutContainer.classList.remove("hidden");
73
+ });
74
+ }
75
+ });
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cased
4
+ class AuthorizationsController < ApplicationController
5
+ def create
6
+ self.cased_authorization = params[:token]
7
+ end
8
+
9
+ def destroy
10
+ self.cased_authorization = nil
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cased
4
+ module CLI
5
+ class SessionsController < ApplicationController
6
+ def show
7
+ guard_session = Cased::CLI::Session.find(params[:guard_session_id])
8
+
9
+ respond_to do |format|
10
+ format.html do
11
+ render partial: 'cased/cli/sessions/form', locals: { guard_session: guard_session }
12
+ end
13
+
14
+ format.json do
15
+ render partial: 'cased/cli/sessions/guard_session', locals: { guard_session: guard_session }
16
+ end
17
+ end
18
+ end
19
+
20
+ def cancel
21
+ guard_session = Cased::CLI::Session.find(params[:guard_session_id])
22
+ guard_session.cancel
23
+
24
+ respond_to do |format|
25
+ format.html do
26
+ safe_redirect_back
27
+ end
28
+
29
+ format.json do
30
+ render partial: 'cased/cli/sessions/guard_session', locals: { guard_session: guard_session }
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def safe_redirect_back(allow_other_host: false, **args)
38
+ referer = params[:referer]
39
+ redirect_to_referer = referer && (allow_other_host || url_host_allowed?(referer))
40
+ redirect_to redirect_to_referer ? referer : guard_fallback_location, **args
41
+ end
42
+
43
+ def url_host_allowed?(url)
44
+ uri = URI(url.to_s)
45
+
46
+ # We're redirecting to a path on app.cased.com, that is okay.
47
+ return true if uri.host.blank?
48
+
49
+ uri.host == request.host
50
+ rescue ArgumentError, URI::Error
51
+ false
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CasedHelper
4
+ # Guarded parameters are the original parameters when the form was first
5
+ # submitted. These parameters need to be preserved.
6
+ def guarded_parameters(form)
7
+ form_params = params.except(:authenticity_token, :controller, :action)
8
+
9
+ safe_join render_guarded_parameters(form, form_params.to_unsafe_h)
10
+ end
11
+
12
+ def render_guarded_parameters(form, form_params, prefix = nil)
13
+ form_params.collect do |key, value|
14
+ case value
15
+ when Hash
16
+ render_guarded_parameters(form, value, key)
17
+ else
18
+ name = prefix ? "#{prefix}[#{key}]" : key
19
+ hidden_field_tag(name, value)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+
5
+ module Cased
6
+ class Authorization
7
+ class MissingApplicationKey < StandardError
8
+ MESSAGE = <<~MSG
9
+ Missing GUARD_APPLICATION_KEY or Cased.config.guard_application_key.
10
+ MSG
11
+
12
+ def initialize
13
+ super(MESSAGE)
14
+ end
15
+ end
16
+
17
+ ALGORITHM = 'HS256'
18
+
19
+ def self.load!(token)
20
+ raise MissingApplicationKey if Cased.config.guard_application_key.blank?
21
+
22
+ # JWT.decode will raise here if the token has expired or the application
23
+ # key does not match meaning it has been tampered with.
24
+ data, = JWT.decode(token, Cased.config.guard_application_key, true, algorithm: ALGORITHM)
25
+
26
+ new(
27
+ user: data.fetch('user'),
28
+ user_id: data.fetch('user_id'),
29
+ expires_at: data.fetch('exp'),
30
+ issuer: data.fetch('iss'),
31
+ issued_at: data.fetch('iat'),
32
+ )
33
+ end
34
+
35
+ def self.validate!(token)
36
+ load!(token)
37
+ end
38
+
39
+ attr_reader :user
40
+ attr_reader :user_id
41
+ attr_reader :issued_at
42
+ attr_reader :expires_at
43
+ attr_reader :issuer
44
+
45
+ def initialize(user:, user_id:, issued_at:, expires_at:, issuer:)
46
+ @user = user
47
+ @user_id = user_id
48
+ @issued_at = Time.at(issued_at)
49
+ @expires_at = Time.at(expires_at)
50
+ @issuer = issuer
51
+ end
52
+
53
+ def token
54
+ user_id
55
+ end
56
+
57
+ def to_s
58
+ user
59
+ end
60
+
61
+ def to_param
62
+ user_id
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,10 @@
1
+ <script>
2
+ // We need to let the originating window know about the new logged in user.
3
+ // Once the originating window is notified we can close this window.
4
+ if (window.opener) {
5
+ window.opener.postMessage({
6
+ user: "<%= cased_authorization.user %>"
7
+ })
8
+ window.close()
9
+ }
10
+ </script>
@@ -0,0 +1,15 @@
1
+ <div id="guard-session-container">
2
+ <% if guard_session.requested? %>
3
+ <%= render 'cased/cli/sessions/steps/requested', guard_session: guard_session %>
4
+ <% elsif guard_session.denied? %>
5
+ <%= render 'cased/cli/sessions/steps/denied', guard_session: guard_session %>
6
+ <% elsif guard_session.canceled? %>
7
+ <%= render 'cased/cli/sessions/steps/canceled', guard_session: guard_session %>
8
+ <% elsif guard_session.timed_out? %>
9
+ <%= render 'cased/cli/sessions/steps/timed_out', guard_session: guard_session %>
10
+ <% elsif guard_session.reason_required? %>
11
+ <%= render 'cased/cli/sessions/steps/reason_required', guard_session: guard_session %>
12
+ <% else %>
13
+ <%= render 'cased/cli/sessions/steps/create', guard_session: guard_session %>
14
+ <% end %>
15
+ </div>
@@ -0,0 +1,27 @@
1
+ json.id guard_session.id
2
+ json.url guard_session.url
3
+ json.api_url guard_session.api_url
4
+ json.state guard_session.state
5
+ json.command guard_session.command
6
+ json.metadata guard_session.metadata
7
+ json.reason guard_session.reason
8
+ json.ip_address guard_session.ip_address
9
+ json.requester do |requester|
10
+ requester.id guard_session.requester['id']
11
+ requester.email guard_session.requester['email']
12
+ end
13
+
14
+ json.responded_at guard_session.responded_at
15
+ json.responder do |responder|
16
+ responder.id guard_session.responder['id']
17
+ responder.email guard_session.responder['email']
18
+ end
19
+
20
+ json.guard_application do |guard_application|
21
+ guard_application.id guard_session.guard_application['id']
22
+ guard_application.name guard_session.guard_application['name']
23
+ guard_application.settings do |settings|
24
+ settings.message_of_the_day guard_session.guard_application.dig('settings', 'message_of_the_day')
25
+ settings.reason_required guard_session.guard_application.dig('settings', 'reason_required')
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ <div class="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
2
+ <div class="sm:mx-auto sm:w-full sm:max-w-md">
3
+ <%= image_tag 'cased/logo.png', class: 'mx-auto h-12 w-auto' %>
4
+ </div>
5
+
6
+ <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
7
+ <div id="cased-logged-in" class="<%= 'hidden' unless cased_authorization? %>">
8
+ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
9
+ <%= render 'cased/cli/sessions/form', guard_session: current_guard_session %>
10
+ </div>
11
+ <div class="text-center py-4 flex justify-center space-x-2">
12
+ <div>
13
+ Logged in as <span id="cased-user"><%= cased_authorization %></span>.
14
+ </div>
15
+ <%= button_to 'Logout', cased.logout_path, form: { id: 'cased-logout' }, method: :delete, remote: true, class: 'p-0 border-none bg-transparent text-blue-500 shadow-none cursor-pointer' %>
16
+ </div>
17
+ </div>
18
+ <div id="cased-logged-out" class="<%= 'hidden' if cased_authorization? %>">
19
+ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
20
+ <%= link_to 'Sign in to Cased', "#{Cased.config.url}/login/connect/#{Cased.config.guard_application_key}?return_to=#{cased.authorizations_url}", id: 'cased-authenticate', class: 'w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500' %>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </div>
@@ -0,0 +1,17 @@
1
+ <div class="sm:flex sm:items-start">
2
+ <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
3
+ <svg class="h-6 w-6 text-red-600" x-description="Heroicon name: outline/exclamation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
4
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
5
+ </svg>
6
+ </div>
7
+ <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
8
+ <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
9
+ Request Canceled
10
+ </h3>
11
+ <div class="mt-2">
12
+ <p class="text-sm text-gray-500">
13
+ You canceled the request.
14
+ </p>
15
+ </div>
16
+ </div>
17
+ </div>
@@ -0,0 +1,15 @@
1
+ <%= form_with method: request.request_method_symbol, local: true, id: 'cased-create-session' do |form| %>
2
+ <%= guarded_parameters form %>
3
+
4
+ <div class="flex justify-center">
5
+ <div>
6
+ <svg class="animate-spin -ml-1 mr-3 h-6 w-6 text-gray" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
7
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
8
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
9
+ </svg>
10
+ </div>
11
+ <div>
12
+ Loading…
13
+ </div>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <div class="sm:flex sm:items-start">
2
+ <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
3
+ <svg class="h-6 w-6 text-red-600" x-description="Heroicon name: outline/exclamation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
4
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
5
+ </svg>
6
+ </div>
7
+ <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
8
+ <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
9
+ Request Denied
10
+ </h3>
11
+ <div class="mt-2">
12
+ <p class="text-sm text-gray-500">
13
+ <strong><%= guard_session.responder['email'] %></strong> denied your request.
14
+ </p>
15
+ </div>
16
+ </div>
17
+ </div>
@@ -0,0 +1,16 @@
1
+ <%= form_with method: request.request_method_symbol, class: 'space-y-6', local: true, id: 'guard-reason-required-form' do |form| %>
2
+ <%= guarded_parameters form %>
3
+
4
+ <div>
5
+ <%= form.label 'guard_session[reason]', 'Reason', class: 'block text-sm font-medium text-gray-700' %>
6
+ <div class="mt-1">
7
+ <%= form.text_field 'guard_session[reason]', required: true, autocomplete: 'off', placeholder: 'Provide a reason', class: 'appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm' %>
8
+ </div>
9
+ </div>
10
+
11
+ <div>
12
+ <button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
13
+ Submit
14
+ </button>
15
+ </div>
16
+ <% end %>
@@ -0,0 +1,49 @@
1
+ <%= form_with method: request.request_method_symbol, local: true, id: 'guard-session-form' do |form| %>
2
+ <%= guarded_parameters form %>
3
+
4
+ <div class="flex justify-center">
5
+ <div>
6
+ <svg class="animate-spin -ml-1 mr-3 h-6 w-6 text-gray" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
7
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
8
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
9
+ </svg>
10
+ </div>
11
+ <div>
12
+ Waiting for approval…
13
+ </div>
14
+ </div>
15
+ <%= form.hidden_field 'guard_session[id]', value: guard_session.id %>
16
+ <%= form.hidden_field 'guard_session[referer]', value: request.referer %>
17
+ <% end %>
18
+
19
+ <script>
20
+ const checkState = () => {
21
+ fetch("<%= cased.guard_session_url(guard_session, format: :json) %>")
22
+ .then((response) => response.json())
23
+ .then((json) => {
24
+ switch (json.state) {
25
+ case 'approved':
26
+ document.getElementById('guard-session-form').submit()
27
+ break;
28
+
29
+ case 'requested':
30
+ setTimeout(checkState, 1000)
31
+ break;
32
+
33
+ default:
34
+ refresh()
35
+ break;
36
+ }
37
+ })
38
+ }
39
+
40
+ const refresh = () => {
41
+ fetch("<%= cased.guard_session_url(guard_session) %>")
42
+ .then((response) => response.text())
43
+ .then((html) => {
44
+ document.getElementById('guard-session-container').innerHTML = html
45
+ })
46
+ }
47
+
48
+ checkState()
49
+ </script>
@@ -0,0 +1,17 @@
1
+ <div class="sm:flex sm:items-start">
2
+ <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-yellow-100 sm:mx-0 sm:h-10 sm:w-10">
3
+ <svg class="h-6 w-6 text-yellow-600" x-description="Heroicon name: outline/exclamation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
4
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
5
+ </svg>
6
+ </div>
7
+ <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
8
+ <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
9
+ Request Timed Out
10
+ </h3>
11
+ <div class="mt-2">
12
+ <p class="text-sm text-gray-500">
13
+ Your request timed out.
14
+ </p>
15
+ </div>
16
+ </div>
17
+ </div>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Cased</title>
5
+ <%= csp_meta_tag %>
6
+ <%= favicon_link_tag 'cased/favicon.ico' %>
7
+ <%= javascript_include_tag 'cased/index' %>
8
+ <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
9
+ </head>
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1,3 @@
1
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
2
+ inflect.acronym 'CLI'
3
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ Cased::Rails::Engine.routes.draw do
2
+ get '/authorizations/callback' => 'cased/authorizations#create', as: :authorizations
3
+ delete '/logout' => 'cased/authorizations#destroy', as: :logout
4
+ get '/cli/sessions/:guard_session_id' => 'cased/cli/sessions#show', as: :guard_session
5
+ post '/cli/sessions/:guard_session_id/cancel' => 'cased/cli/sessions#cancel', as: :cancel_guard_session
6
+ end
@@ -6,10 +6,107 @@ module Cased
6
6
 
7
7
  included do
8
8
  before_action :cased_setup_request_context
9
+ if respond_to?(:helper_method)
10
+ helper_method :current_guard_session
11
+ helper_method :cased_authorization
12
+ helper_method :cased_authorization?
13
+ end
9
14
  end
10
15
 
11
16
  private
12
17
 
18
+ def guard_required?
19
+ true
20
+ end
21
+
22
+ def cased_authorization
23
+ @cased_authorization ||= begin
24
+ if cookies[:cased_authorization]
25
+ Cased::Authorization.load!(cookies[:cased_authorization])
26
+ end
27
+ rescue JWT::ExpiredSignature
28
+ cookies.delete(:cased_authorization)
29
+ nil
30
+ end
31
+ end
32
+
33
+ def cased_authorization?
34
+ cased_authorization.present?
35
+ end
36
+
37
+ def cased_authorization=(token)
38
+ if token.nil?
39
+ cookies.delete(:cased_authorization)
40
+ else
41
+ Cased::Authorization.validate!(token)
42
+
43
+ cookies[:cased_authorization] = token
44
+ end
45
+ end
46
+
47
+ def current_guard_session
48
+ @current_guard_session ||= Cased::CLI::Session.new(
49
+ reason: params.dig(:guard_session, :reason),
50
+ metadata: guard_session_metadata,
51
+ authentication: cased_authorization,
52
+ )
53
+ end
54
+
55
+ def guard_session_approved?
56
+ guard_session_id = params.dig(:guard_session, :id)
57
+ return false unless guard_session_id.present?
58
+
59
+ session = Cased::CLI::Session.find(guard_session_id)
60
+ session.approved?
61
+ end
62
+
63
+ def guard
64
+ # TODO: Cancel previous session if not used
65
+ return true unless guard_required?
66
+
67
+ if guard_session_approved?
68
+ Cased.context.merge(guard_session: current_guard_session)
69
+ return true
70
+ end
71
+
72
+ if cased_authorization? && current_guard_session.create && current_guard_session.approved?
73
+ Cased.context.merge(guard_session: current_guard_session)
74
+ return true
75
+ end
76
+
77
+ render_guard
78
+ end
79
+
80
+ def guard_fallback_location
81
+ if respond_to?(:root_path)
82
+ root_path
83
+ else
84
+ '/'
85
+ end
86
+ end
87
+
88
+ def render_guard
89
+ respond_to do |format|
90
+ format.html do
91
+ render template: 'cased/cli/sessions/new', layout: 'cased/cli'
92
+ end
93
+
94
+ format.json do
95
+ render json: { error: true }
96
+ end
97
+ end
98
+ end
99
+
100
+ def guard_session_metadata
101
+ {
102
+ location: request.remote_ip,
103
+ request_http_method: request.method,
104
+ request_user_agent: request.headers['User-Agent'],
105
+ request_url: request.original_url,
106
+ request_id: request.request_id,
107
+ }
108
+ end
109
+
13
110
  def cased_setup_request_context
14
111
  Cased.context.merge(cased_initial_request_context)
15
112
  end
data/lib/cased/rails.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'cased-ruby'
4
+ require 'cased/rails/config'
4
5
  require 'cased/rails/railtie'
5
6
  require 'cased/rails/engine'
6
7
  require 'cased/model/automatic'
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cased
4
+ module Rails
5
+ module Config
6
+ def unfiltered_parameters=(new_unfiltered_parameters)
7
+ @unfiltered_parameters = Array.wrap(new_unfiltered_parameters)
8
+ end
9
+
10
+ def unfiltered_parameters
11
+ @unfiltered_parameters ||= [
12
+ # Database record ID's
13
+ 'id',
14
+ # Controller actions
15
+ 'action',
16
+ # Controller names
17
+ 'controller',
18
+ ].freeze
19
+ end
20
+
21
+ def filter_parameters=(new_filter_parameters)
22
+ @filter_parameters = new_filter_parameters
23
+ end
24
+
25
+ def filter_parameters?
26
+ return @filter_parameters if defined?(@filter_parameters)
27
+
28
+ @filter_parameters = if ENV['CASED_FILTER_PARAMETERS']
29
+ parse_bool(ENV['CASED_FILTER_PARAMETERS'])
30
+ else
31
+ ::Rails.env.staging? || ::Rails.env.production?
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ Cased::Config.prepend(Cased::Rails::Config)
@@ -5,6 +5,19 @@ require 'rails/railtie'
5
5
  module Cased
6
6
  module Rails
7
7
  class Railtie < ::Rails::Railtie
8
+ initializer 'cased.parameter_filter' do |app|
9
+ app.config.filter_parameters << proc do |key, value, _original_params|
10
+ next unless Cased.config.filter_parameters?
11
+ next if Cased.config.unfiltered_parameters.include?(key) || !value.respond_to?(:replace)
12
+
13
+ value.replace(ActiveSupport::ParameterFilter::FILTERED)
14
+ end
15
+ end
16
+
17
+ initializer 'cased.assets.precompile' do |app|
18
+ app.config.assets.precompile << 'cased/manifest.js'
19
+ end
20
+
8
21
  initializer 'cased.include_controller_helpers' do
9
22
  ActiveSupport.on_load(:action_controller) do
10
23
  require 'cased/controller_helpers'
@@ -43,6 +56,23 @@ module Cased
43
56
  # :nocov:
44
57
  console do
45
58
  Cased.console
59
+
60
+ # We only want to start an interactive session if Cased CLI is
61
+ # configured.
62
+ next if Cased.config.guard_application_key.blank?
63
+
64
+ session = Cased::CLI::InteractiveSession.start(command: "#{Dir.pwd}/bin/rails console")
65
+ Cased.context.merge(guard_session: session)
66
+ # If the session does not need its output recorded, we can bypass any
67
+ # forced exits.
68
+ next unless session.record_output?
69
+
70
+ # If we reach this line inside of the recorded session we don't want to
71
+ # exit but instead proceed to the `rails console` as usual.
72
+ #
73
+ # We don't want to enter the parent `rails console` so we exit right
74
+ # away as we know the child `rails console` completed successfully.
75
+ exit unless Cased::CLI::Session.current&.approved?
46
76
  end
47
77
  end
48
78
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cased
4
4
  module Rails
5
- VERSION = '0.3.1'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Enforce your Cased CLI controls before the Rake task is executed'
4
+ task :guard do
5
+ next if Cased.config.guard_application_key.blank?
6
+
7
+ session = Cased::CLI::InteractiveSession.start
8
+ next unless session.record_output?
9
+
10
+ exit unless Cased::CLI::Session.current&.approved?
11
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cased-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Garrett Bjerkhoel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-21 00:00:00.000000000 Z
11
+ date: 2021-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cased-ruby
@@ -16,14 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.3.3
19
+ version: 0.5.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.3.3
26
+ version: 0.5.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: jbuilder
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rails
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -103,14 +117,37 @@ extra_rdoc_files: []
103
117
  files:
104
118
  - README.md
105
119
  - Rakefile
120
+ - app/assets/config/cased/manifest.js
121
+ - app/assets/images/cased/favicon.ico
122
+ - app/assets/images/cased/logo.png
123
+ - app/assets/javascripts/cased/index.js
124
+ - app/controllers/cased/authorizations_controller.rb
125
+ - app/controllers/cased/cli/sessions_controller.rb
126
+ - app/helpers/cased_helper.rb
127
+ - app/models/cased/authorization.rb
128
+ - app/views/cased/authorizations/create.html.erb
129
+ - app/views/cased/cli/sessions/_form.html.erb
130
+ - app/views/cased/cli/sessions/_guard_session.json.jbuilder
131
+ - app/views/cased/cli/sessions/new.html.erb
132
+ - app/views/cased/cli/sessions/steps/_canceled.html.erb
133
+ - app/views/cased/cli/sessions/steps/_create.html.erb
134
+ - app/views/cased/cli/sessions/steps/_denied.html.erb
135
+ - app/views/cased/cli/sessions/steps/_reason_required.html.erb
136
+ - app/views/cased/cli/sessions/steps/_requested.html.erb
137
+ - app/views/cased/cli/sessions/steps/_timed_out.html.erb
138
+ - app/views/layouts/cased/cli.html.erb
139
+ - config/initializers/inflections.rb
140
+ - config/routes.rb
106
141
  - lib/cased/controller_helpers.rb
107
142
  - lib/cased/model/automatic.rb
108
143
  - lib/cased/rails.rb
109
144
  - lib/cased/rails/active_job.rb
145
+ - lib/cased/rails/config.rb
110
146
  - lib/cased/rails/engine.rb
111
147
  - lib/cased/rails/model.rb
112
148
  - lib/cased/rails/railtie.rb
113
149
  - lib/cased/rails/version.rb
150
+ - lib/tasks/cased.rake
114
151
  homepage: https://github.com/cased/cased-rails
115
152
  licenses:
116
153
  - MIT