masks 0.2.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +50 -0
- data/Rakefile +11 -0
- data/app/assets/builds/application.css +4764 -0
- data/app/assets/builds/application.js +8236 -0
- data/app/assets/builds/application.js.map +7 -0
- data/app/assets/builds/masks/application.css +1 -0
- data/app/assets/builds/masks/application.js +7122 -0
- data/app/assets/builds/masks/application.js.map +7 -0
- data/app/assets/images/masks.png +0 -0
- data/app/assets/javascripts/application.js +2 -0
- data/app/assets/javascripts/controllers/application.js +9 -0
- data/app/assets/javascripts/controllers/emails_controller.js +28 -0
- data/app/assets/javascripts/controllers/index.js +12 -0
- data/app/assets/javascripts/controllers/keys_controller.js +20 -0
- data/app/assets/javascripts/controllers/recover_controller.js +21 -0
- data/app/assets/javascripts/controllers/recover_password_controller.js +21 -0
- data/app/assets/javascripts/controllers/session_controller.js +94 -0
- data/app/assets/manifest.js +2 -0
- data/app/assets/masks_manifest.js +2 -0
- data/app/assets/stylesheets/application.css +26 -0
- data/app/controllers/concerns/masks/controller.rb +114 -0
- data/app/controllers/masks/actors_controller.rb +15 -0
- data/app/controllers/masks/application_controller.rb +35 -0
- data/app/controllers/masks/backup_codes_controller.rb +34 -0
- data/app/controllers/masks/debug_controller.rb +9 -0
- data/app/controllers/masks/devices_controller.rb +20 -0
- data/app/controllers/masks/emails_controller.rb +60 -0
- data/app/controllers/masks/error_controller.rb +14 -0
- data/app/controllers/masks/keys_controller.rb +45 -0
- data/app/controllers/masks/manage/actor_controller.rb +35 -0
- data/app/controllers/masks/manage/actors_controller.rb +12 -0
- data/app/controllers/masks/manage/base_controller.rb +12 -0
- data/app/controllers/masks/one_time_code_controller.rb +49 -0
- data/app/controllers/masks/passwords_controller.rb +33 -0
- data/app/controllers/masks/recoveries_controller.rb +43 -0
- data/app/controllers/masks/sessions_controller.rb +53 -0
- data/app/helpers/masks/application_helper.rb +49 -0
- data/app/jobs/masks/application_job.rb +7 -0
- data/app/jobs/masks/expire_actors_job.rb +15 -0
- data/app/jobs/masks/expire_recoveries_job.rb +15 -0
- data/app/mailers/masks/actor_mailer.rb +22 -0
- data/app/mailers/masks/application_mailer.rb +15 -0
- data/app/models/concerns/masks/access.rb +162 -0
- data/app/models/concerns/masks/actor.rb +132 -0
- data/app/models/concerns/masks/adapter.rb +68 -0
- data/app/models/concerns/masks/role.rb +9 -0
- data/app/models/concerns/masks/scoped.rb +54 -0
- data/app/models/masks/access/actor_password.rb +20 -0
- data/app/models/masks/access/actor_scopes.rb +18 -0
- data/app/models/masks/access/actor_signup.rb +22 -0
- data/app/models/masks/actors/anonymous.rb +40 -0
- data/app/models/masks/actors/system.rb +24 -0
- data/app/models/masks/adapters/active_record.rb +85 -0
- data/app/models/masks/application_model.rb +15 -0
- data/app/models/masks/application_record.rb +8 -0
- data/app/models/masks/check.rb +192 -0
- data/app/models/masks/credential.rb +166 -0
- data/app/models/masks/credentials/backup_code.rb +30 -0
- data/app/models/masks/credentials/device.rb +59 -0
- data/app/models/masks/credentials/email.rb +48 -0
- data/app/models/masks/credentials/factor2.rb +71 -0
- data/app/models/masks/credentials/key.rb +38 -0
- data/app/models/masks/credentials/last_login.rb +12 -0
- data/app/models/masks/credentials/masquerade.rb +32 -0
- data/app/models/masks/credentials/nickname.rb +63 -0
- data/app/models/masks/credentials/one_time_code.rb +34 -0
- data/app/models/masks/credentials/password.rb +28 -0
- data/app/models/masks/credentials/recovery.rb +71 -0
- data/app/models/masks/credentials/session.rb +67 -0
- data/app/models/masks/device.rb +30 -0
- data/app/models/masks/error.rb +51 -0
- data/app/models/masks/event.rb +14 -0
- data/app/models/masks/mask.rb +255 -0
- data/app/models/masks/rails/actor.rb +190 -0
- data/app/models/masks/rails/actor_role.rb +12 -0
- data/app/models/masks/rails/device.rb +47 -0
- data/app/models/masks/rails/email.rb +96 -0
- data/app/models/masks/rails/key.rb +61 -0
- data/app/models/masks/rails/recovery.rb +116 -0
- data/app/models/masks/rails/role.rb +20 -0
- data/app/models/masks/rails/scope.rb +15 -0
- data/app/models/masks/session.rb +447 -0
- data/app/models/masks/sessions/access.rb +26 -0
- data/app/models/masks/sessions/inline.rb +16 -0
- data/app/models/masks/sessions/request.rb +42 -0
- data/app/resources/masks/actor_resource.rb +9 -0
- data/app/resources/masks/session_resource.rb +15 -0
- data/app/views/layouts/masks/application.html.erb +17 -0
- data/app/views/layouts/masks/mailer.html.erb +17 -0
- data/app/views/layouts/masks/mailer.text.erb +1 -0
- data/app/views/layouts/masks/manage.html.erb +25 -0
- data/app/views/masks/actor_mailer/recover_credentials.html.erb +33 -0
- data/app/views/masks/actor_mailer/recover_credentials.text.erb +1 -0
- data/app/views/masks/actor_mailer/verify_email.html.erb +34 -0
- data/app/views/masks/actor_mailer/verify_email.text.erb +8 -0
- data/app/views/masks/actors/current.html.erb +152 -0
- data/app/views/masks/application/_header.html.erb +31 -0
- data/app/views/masks/backup_codes/new.html.erb +103 -0
- data/app/views/masks/emails/new.html.erb +103 -0
- data/app/views/masks/emails/verify.html.erb +51 -0
- data/app/views/masks/keys/new.html.erb +127 -0
- data/app/views/masks/manage/actor/show.html.erb +126 -0
- data/app/views/masks/manage/actors/index.html.erb +40 -0
- data/app/views/masks/one_time_code/new.html.erb +150 -0
- data/app/views/masks/passwords/edit.html.erb +58 -0
- data/app/views/masks/recoveries/new.html.erb +71 -0
- data/app/views/masks/recoveries/password.html.erb +64 -0
- data/app/views/masks/sessions/new.html.erb +153 -0
- data/config/brakeman.ignore +28 -0
- data/config/locales/en.yml +286 -0
- data/config/routes.rb +46 -0
- data/db/migrate/20231205173845_create_actors.rb +94 -0
- data/lib/generators/masks/install/USAGE +8 -0
- data/lib/generators/masks/install/install_generator.rb +33 -0
- data/lib/generators/masks/install/templates/initializer.rb +5 -0
- data/lib/generators/masks/install/templates/masks.json +6 -0
- data/lib/masks/configuration.rb +236 -0
- data/lib/masks/engine.rb +25 -0
- data/lib/masks/middleware.rb +70 -0
- data/lib/masks/version.rb +5 -0
- data/lib/masks.rb +183 -0
- data/lib/tasks/masks_tasks.rake +71 -0
- data/masks.json +274 -0
- metadata +416 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "recursive-open-struct"
|
|
5
|
+
|
|
6
|
+
module Masks
|
|
7
|
+
# Container for masks configuration data.
|
|
8
|
+
class Configuration
|
|
9
|
+
include ActiveModel::Model
|
|
10
|
+
include ActiveModel::Validations
|
|
11
|
+
include ActiveModel::Attributes
|
|
12
|
+
include ActiveModel::Serializers::JSON
|
|
13
|
+
|
|
14
|
+
attribute :data
|
|
15
|
+
attribute :name
|
|
16
|
+
attribute :adapter, default: "Masks::Adapters::ActiveRecord"
|
|
17
|
+
attribute :theme
|
|
18
|
+
attribute :site_name
|
|
19
|
+
attribute :site_url
|
|
20
|
+
attribute :site_links
|
|
21
|
+
attribute :site_logo
|
|
22
|
+
attribute :lifetimes
|
|
23
|
+
attribute :masks
|
|
24
|
+
attribute :models
|
|
25
|
+
attribute :version
|
|
26
|
+
|
|
27
|
+
# Returns all configuration data as +RecursiveOpenStruct+.
|
|
28
|
+
#
|
|
29
|
+
# @return RecursiveOpenStruct
|
|
30
|
+
def dat
|
|
31
|
+
RecursiveOpenStruct.new(data)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns a string to use as the "issuer" for various secrets—TOTP, JWT, etc.
|
|
35
|
+
# @return [String]
|
|
36
|
+
def issuer
|
|
37
|
+
data.fetch(:name, "masks").parameterize
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns a site theme to use on the frontend.
|
|
41
|
+
# @return [String]
|
|
42
|
+
def theme
|
|
43
|
+
super || data.fetch(:theme, "dark")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the site logo, used for visual identification of the site.
|
|
47
|
+
# @return [String]
|
|
48
|
+
def site_logo
|
|
49
|
+
super || data.fetch(:logo, nil)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns the site name, used for referring to the site (e.g. in the logo).
|
|
53
|
+
# @return [String]
|
|
54
|
+
def site_name
|
|
55
|
+
super || data.fetch(:name, "masks")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the site title, used primarily in meta tags.
|
|
59
|
+
# @return [String]
|
|
60
|
+
def site_title
|
|
61
|
+
super || data.fetch(:title, "")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns the canonical url for the site.
|
|
65
|
+
# @return [String]
|
|
66
|
+
def site_url
|
|
67
|
+
super || data.fetch(:url, nil)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A hash of links—urls to various places on the frontend.
|
|
71
|
+
#
|
|
72
|
+
# These default to generated rails routes, but can be overridden
|
|
73
|
+
# where necessary.
|
|
74
|
+
#
|
|
75
|
+
# @return [Hash]
|
|
76
|
+
def site_links
|
|
77
|
+
{
|
|
78
|
+
root: rails_url.try(:root_path) || "/",
|
|
79
|
+
recover: rails_url.recover_path,
|
|
80
|
+
login: rails_url.session_path,
|
|
81
|
+
after_login: rails_url.session_path,
|
|
82
|
+
after_logout: rails_url.session_path
|
|
83
|
+
}.merge(super || data.fetch(:links, {}))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns the current global session key.
|
|
87
|
+
# @return [String]
|
|
88
|
+
def version
|
|
89
|
+
super || "v1"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A hash of expiration times for various resources.
|
|
93
|
+
#
|
|
94
|
+
# These values are used to override expiration times for things like
|
|
95
|
+
# password reset emails and other time-based authentication methods.
|
|
96
|
+
#
|
|
97
|
+
# @return [Hash]
|
|
98
|
+
def lifetimes
|
|
99
|
+
(super || dat.lifetimes&.to_h || {}).transform_values do |seconds|
|
|
100
|
+
seconds.to_i.seconds
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A hash of default models the app relies on.
|
|
105
|
+
#
|
|
106
|
+
# The following keys are available:
|
|
107
|
+
#
|
|
108
|
+
# actor: +Masks::Rails::Actor+
|
|
109
|
+
# role: +Masks::Rails::Role+
|
|
110
|
+
# scope: +Masks::Rails::Scope+
|
|
111
|
+
# email: +Masks::Rails::Email+
|
|
112
|
+
# recovery: +Masks::Rails::Recovery+
|
|
113
|
+
#
|
|
114
|
+
# This makes it easy to provide a substitute for key models
|
|
115
|
+
# while still relying on the base active record implementation.
|
|
116
|
+
#
|
|
117
|
+
# @return [Hash{Symbol => String}]
|
|
118
|
+
def models
|
|
119
|
+
{
|
|
120
|
+
actor: "Masks::Rails::Actor",
|
|
121
|
+
scope: "Masks::Rails::Scope",
|
|
122
|
+
role: "Masks::Rails::Role",
|
|
123
|
+
email: "Masks::Rails::Email",
|
|
124
|
+
recovery: "Masks::Rails::Recovery",
|
|
125
|
+
device: "Masks::Rails::Device",
|
|
126
|
+
key: "Masks::Rails::Key",
|
|
127
|
+
session_json: "Masks::SessionResource",
|
|
128
|
+
request: "Masks::Sessions::Request",
|
|
129
|
+
inline: "Masks::Sessions::Inline",
|
|
130
|
+
access: "Masks::Sessions::Access"
|
|
131
|
+
}.merge(super || data.fetch(:models, {}))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns a model +Class+.
|
|
135
|
+
#
|
|
136
|
+
# @return [Class]
|
|
137
|
+
def model(name)
|
|
138
|
+
models[name]&.constantize
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns configuration data for a given mask type.
|
|
142
|
+
#
|
|
143
|
+
# @param [String] type
|
|
144
|
+
# @return [Hash]
|
|
145
|
+
def mask(type)
|
|
146
|
+
config = data.dig(:types, type.to_sym)
|
|
147
|
+
raise Masks::Error::InvalidConfiguration, type unless config
|
|
148
|
+
|
|
149
|
+
config
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Returns all configured masks.
|
|
153
|
+
#
|
|
154
|
+
# @return [Array<Masks::Mask>]
|
|
155
|
+
def masks
|
|
156
|
+
@masks ||=
|
|
157
|
+
begin
|
|
158
|
+
masks = super || data.fetch(:masks, [])
|
|
159
|
+
masks.map do |opts|
|
|
160
|
+
case opts
|
|
161
|
+
when Masks::Mask
|
|
162
|
+
opts
|
|
163
|
+
when Hash
|
|
164
|
+
Masks::Mask.new(opts.merge(config: self))
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Returns configuration data for the given access name.
|
|
171
|
+
# @param [String] name
|
|
172
|
+
# @return [Hash]
|
|
173
|
+
def access(name)
|
|
174
|
+
defaults = Masks::Access.defaults(name)
|
|
175
|
+
raise Masks::Error::UnknownAccess, name unless defaults
|
|
176
|
+
|
|
177
|
+
defaults.deep_merge(access_types&.fetch(name, {}))
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Returns configuration data for all access types.
|
|
181
|
+
#
|
|
182
|
+
# This does not include defaults created with +Masks::Access.access+.
|
|
183
|
+
#
|
|
184
|
+
# @return [Hash]
|
|
185
|
+
def access_types
|
|
186
|
+
data
|
|
187
|
+
.fetch(:access, {})
|
|
188
|
+
.to_h do |name, opts|
|
|
189
|
+
[name, opts.merge(name:, cls: opts[:cls]&.constantize)]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Setter for whether or not sigups are allowed.
|
|
194
|
+
#
|
|
195
|
+
# @param [Boolean] value
|
|
196
|
+
def signups=(value)
|
|
197
|
+
data[:signups] = value
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Whether or not sigups are allowed.
|
|
201
|
+
#
|
|
202
|
+
# @return [Boolean]
|
|
203
|
+
def signups?
|
|
204
|
+
data[:signups]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
delegate :find_key,
|
|
208
|
+
:find_actor,
|
|
209
|
+
:find_actors,
|
|
210
|
+
:build_actor,
|
|
211
|
+
:find_device,
|
|
212
|
+
:find_recovery,
|
|
213
|
+
:build_recovery,
|
|
214
|
+
:expire_actors,
|
|
215
|
+
:expire_recoveries,
|
|
216
|
+
to: :adapt
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def rails_url
|
|
221
|
+
Masks::Engine.routes.url_helpers
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def adapt
|
|
225
|
+
@adapt =
|
|
226
|
+
case adapter
|
|
227
|
+
when String
|
|
228
|
+
adapter.constantize.new(self)
|
|
229
|
+
when Class
|
|
230
|
+
adapter.new(self)
|
|
231
|
+
else
|
|
232
|
+
raise Masks::Error::InvalidConfiguration, adapter
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
data/lib/masks/engine.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "middleware"
|
|
4
|
+
module Masks
|
|
5
|
+
# The Rails engine/railtie for Masks.
|
|
6
|
+
#
|
|
7
|
+
# Adds two initializers:
|
|
8
|
+
#
|
|
9
|
+
# - masks.assets.precompile - ensures masks assets are precompiled and added to the Rails asset pipeline
|
|
10
|
+
# - masks.middleware - appends +Masks::Middleware+ to the middleware chain
|
|
11
|
+
#
|
|
12
|
+
class Engine < ::Rails::Engine
|
|
13
|
+
isolate_namespace Masks
|
|
14
|
+
|
|
15
|
+
initializer "masks.assets.precompile" do |app|
|
|
16
|
+
app.config.assets.precompile += %w[masks_manifest.js] if app.config.try(
|
|
17
|
+
:assets
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
initializer "masks.middleware" do |app|
|
|
22
|
+
app.config.app_middleware.use Masks::Middleware
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Masks
|
|
4
|
+
# Integrate masks with Rails, via middleware.
|
|
5
|
+
#
|
|
6
|
+
# This middleware creates a new session for every request using
|
|
7
|
+
# +Masks.request+. If the session is +passed?+ the middleware
|
|
8
|
+
# chain is allowed to continue.
|
|
9
|
+
#
|
|
10
|
+
# The middleware uses the +fail+ value on the session's mask to
|
|
11
|
+
# determine what to do otherwise. If +false+ the chain is allowed
|
|
12
|
+
# to continue regardless of pass/fail status. It will also accept
|
|
13
|
+
# a URI or path for redirects, an HTTP status code, or a controller
|
|
14
|
+
# action in the form of +ControllerClass#action_name+.
|
|
15
|
+
#
|
|
16
|
+
# The session is stored in the rack environment under the name
|
|
17
|
+
# +Mask::Middleware::SESSION_KEY+, which is how it is made available to
|
|
18
|
+
# controllers in +Masks::Controller#masked_session+.
|
|
19
|
+
#
|
|
20
|
+
# @see Masks.request Masks.request
|
|
21
|
+
# @see Masks::Session Masks::Session
|
|
22
|
+
# @see Masks::Controller Masks::Controller
|
|
23
|
+
class Middleware
|
|
24
|
+
SESSION_KEY = "masks.session"
|
|
25
|
+
|
|
26
|
+
def initialize(app)
|
|
27
|
+
@app = app
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(env)
|
|
31
|
+
request = ActionDispatch::Request.new(env)
|
|
32
|
+
session = Masks.request(request)
|
|
33
|
+
|
|
34
|
+
::Rails.logger.info(
|
|
35
|
+
[
|
|
36
|
+
"Mask #{session.passed? ? "allowed" : "blocked"}",
|
|
37
|
+
"by type=#{session.mask.type}",
|
|
38
|
+
session.mask.name ? "name=#{session.mask.name}" : ""
|
|
39
|
+
].join(" ")
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
env[SESSION_KEY] = session
|
|
43
|
+
app = @app
|
|
44
|
+
app = (session.passed? ? app : error_app(session, app))
|
|
45
|
+
|
|
46
|
+
app.call(env)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def error_app(session, app)
|
|
52
|
+
value = session.mask.fail
|
|
53
|
+
case value
|
|
54
|
+
when false
|
|
55
|
+
app
|
|
56
|
+
when /.+\#.+/
|
|
57
|
+
controller, action = value.split("#")
|
|
58
|
+
controller.constantize.action(action)
|
|
59
|
+
when %r{/.*}, %r{https?://.+}
|
|
60
|
+
Masks::ErrorController.action(:redirect)
|
|
61
|
+
when /\d+/
|
|
62
|
+
Masks::ErrorController.action("render_#{value}")
|
|
63
|
+
when /\w+/
|
|
64
|
+
Masks::ErrorController.action(value)
|
|
65
|
+
else
|
|
66
|
+
Masks::ErrorController.action(:render_unauthorized)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/masks.rb
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "masks/version"
|
|
4
|
+
require "masks/engine"
|
|
5
|
+
require "masks/configuration"
|
|
6
|
+
require "fuzzyurl"
|
|
7
|
+
require "valid_email"
|
|
8
|
+
require "phonelib"
|
|
9
|
+
require "rqrcode"
|
|
10
|
+
require "rotp"
|
|
11
|
+
require "alba"
|
|
12
|
+
require "pagy"
|
|
13
|
+
require "device_detector"
|
|
14
|
+
require "active_model"
|
|
15
|
+
|
|
16
|
+
# Top-level module for masks.
|
|
17
|
+
#
|
|
18
|
+
# Includes helper methods for managing configuration and building
|
|
19
|
+
# sessions, access, etc.
|
|
20
|
+
module Masks
|
|
21
|
+
# @visibility private
|
|
22
|
+
ANON = :anon
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# Returns an access instance based on the type & session passed.
|
|
26
|
+
#
|
|
27
|
+
# @param [Symbol|String] name
|
|
28
|
+
# @param [Masks::Session] session
|
|
29
|
+
# @return [Masks::Access]
|
|
30
|
+
# @raise [Masks::Error::Unauthorized] if access cannot be granted to the session passed
|
|
31
|
+
# @raise [Masks::UnknownAccess] if the access type cannot be found
|
|
32
|
+
def access(name, session, **_opts)
|
|
33
|
+
configuration.access(name).fetch(:cls).build(session)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns a masked session based on the request passed.
|
|
37
|
+
#
|
|
38
|
+
# @param [ActionDispatch::Request] request
|
|
39
|
+
# @return [Masks::Sessions::Request] session
|
|
40
|
+
def request(request)
|
|
41
|
+
model = configuration.model(:request)
|
|
42
|
+
session = model.new(config: configuration, request:)
|
|
43
|
+
session.mask!
|
|
44
|
+
session
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns a masked session based on the params and data passed.
|
|
48
|
+
#
|
|
49
|
+
# This method is useful for creating masks directly, outside of a
|
|
50
|
+
# specific context like a web request.
|
|
51
|
+
#
|
|
52
|
+
# @param params [Hash]
|
|
53
|
+
# @param data [Hash]
|
|
54
|
+
# @yield [Masks::Sessions::Inline]
|
|
55
|
+
# @return [Masks::Sessions::Inline] session
|
|
56
|
+
def mask(name, params: {}, **data)
|
|
57
|
+
model = configuration.model(:inline)
|
|
58
|
+
session = model.new(name:, config: configuration, params:, data:)
|
|
59
|
+
session.mask!
|
|
60
|
+
yield session if session.passed? && block_given?
|
|
61
|
+
session
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Yields the global masks configuration object.
|
|
65
|
+
# @return [Masks::Configuration]
|
|
66
|
+
# @yield [Masks::Configuration]
|
|
67
|
+
def configure
|
|
68
|
+
yield configuration
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns the global configuration object for masks.
|
|
72
|
+
# @return [Masks::Configuration]
|
|
73
|
+
def configuration
|
|
74
|
+
@configuration ||= Masks::Configuration.new(data: config.dup)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @visibility private
|
|
78
|
+
def event(name, **opts, &block)
|
|
79
|
+
ActiveSupport::Notifications.instrument("masks.#{name}", **opts) do
|
|
80
|
+
block.call
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @visibility private
|
|
85
|
+
def config
|
|
86
|
+
@config ||=
|
|
87
|
+
begin
|
|
88
|
+
config = load_config(config_path)
|
|
89
|
+
|
|
90
|
+
defaults =
|
|
91
|
+
if config[:extend]
|
|
92
|
+
# extend a "masks.json" from a gem
|
|
93
|
+
load_config(gem_path(config[:extend]))
|
|
94
|
+
else
|
|
95
|
+
{}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
config = defaults.except(:masks).deep_merge(config)
|
|
99
|
+
config[:masks] = [*config[:masks], *defaults[:masks]] if defaults[
|
|
100
|
+
:masks
|
|
101
|
+
]
|
|
102
|
+
config
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @visibility private
|
|
107
|
+
def with_config(path_or_config)
|
|
108
|
+
previous_config = @config
|
|
109
|
+
previous_path = @config_path
|
|
110
|
+
|
|
111
|
+
case path_or_config
|
|
112
|
+
when String
|
|
113
|
+
@config_path = Pathname.new(path_or_config)
|
|
114
|
+
when Pathname
|
|
115
|
+
@config_path = path_or_config
|
|
116
|
+
when Hash
|
|
117
|
+
@config_path = :custom
|
|
118
|
+
@config = path_or_config
|
|
119
|
+
else
|
|
120
|
+
raise Error::InvalidConfiguration, path_or_config
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
result = yield
|
|
124
|
+
ensure
|
|
125
|
+
@config = previous_config
|
|
126
|
+
@config_path = previous_path
|
|
127
|
+
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @visibility private
|
|
132
|
+
def config_path
|
|
133
|
+
path =
|
|
134
|
+
[
|
|
135
|
+
@config_path,
|
|
136
|
+
ENV["MASKS_DIR"] ? Pathname.new(ENV["MASKS_DIR"]) : nil,
|
|
137
|
+
Pathname.new(Dir.pwd),
|
|
138
|
+
defined?(ENGINE_ROOT) ? Pathname.new(ENGINE_ROOT) : nil
|
|
139
|
+
].find do |p|
|
|
140
|
+
p.is_a?(Symbol) || (p && File.exist?(p.join("masks.json")))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
raise Error::ConfigurationNotFound unless path
|
|
144
|
+
|
|
145
|
+
path
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @visibility private
|
|
149
|
+
def config_path=(value)
|
|
150
|
+
@config_path =
|
|
151
|
+
case value
|
|
152
|
+
when String
|
|
153
|
+
Pathname.new(value)
|
|
154
|
+
when Pathname
|
|
155
|
+
value
|
|
156
|
+
else
|
|
157
|
+
raise Error::InvalidConfiguration, value
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @visibility private
|
|
162
|
+
def reset!
|
|
163
|
+
@configuration = @config = @config_path = nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def load_config(dir)
|
|
167
|
+
JSON.parse(File.read(dir.join("masks.json")), symbolize_names: true)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def gem_path(gem_name)
|
|
171
|
+
begin
|
|
172
|
+
# Try to require the gem
|
|
173
|
+
require gem_name
|
|
174
|
+
rescue LoadError
|
|
175
|
+
return nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Get the gem specification
|
|
179
|
+
spec = Gem::Specification.find_by_name(gem_name)
|
|
180
|
+
Pathname.new(spec.gem_dir) if spec
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
def cli_access(access_name, **opts)
|
|
4
|
+
access = nil
|
|
5
|
+
actor = opts.delete(:as) || ENV["ACTOR"] || Masks::ANON
|
|
6
|
+
|
|
7
|
+
Masks.mask("cli", as: actor, **opts) do |session|
|
|
8
|
+
access = session.access(access_name)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
access
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
namespace :masks do
|
|
15
|
+
task boot: :environment do
|
|
16
|
+
# required to ensure access classes (and others) are registered properly
|
|
17
|
+
Rails.application.eager_load!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
task :signup, %i[nickname password] => :boot do |_task, args|
|
|
21
|
+
access = cli_access("actor.signup", args:)
|
|
22
|
+
signup = access.signup(**args) if access
|
|
23
|
+
|
|
24
|
+
if !access
|
|
25
|
+
puts "could not find actor..."
|
|
26
|
+
elsif signup.invalid?
|
|
27
|
+
puts "failed to signup '#{signup.nickname}'"
|
|
28
|
+
puts "error: #{signup.errors.full_messages.join(", ")}"
|
|
29
|
+
else
|
|
30
|
+
puts "created '#{signup.nickname}'!"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
task :assign_scopes, %i[actor scopes] => :boot do |_task, args|
|
|
35
|
+
if !args[:actor] || !args[:scopes]
|
|
36
|
+
puts "specify an actor and scopes, e.g. masks:assign_scopes[@foo,scope1;scope2;scope3]"
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
access = cli_access("actor.scopes", as: args[:actor])
|
|
41
|
+
access&.assign_scopes(args[:scopes].split(";"))
|
|
42
|
+
|
|
43
|
+
if !access
|
|
44
|
+
puts "could not find actor..."
|
|
45
|
+
elsif access.actor.invalid?
|
|
46
|
+
puts "failed to assign scopes to '#{access.actor.nickname}'"
|
|
47
|
+
puts "error: #{access.actor.errors.full_messages.join(", ")}"
|
|
48
|
+
else
|
|
49
|
+
puts "assigned scopes to '#{access.actor.nickname}'"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
task :change_password, %i[actor password] => :boot do |_task, args|
|
|
54
|
+
if !args[:actor] || !args[:password]
|
|
55
|
+
puts "specify an actor and password, e.g. masks:change_password[@foo,password]"
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
access = cli_access("actor.password", as: args[:actor])
|
|
60
|
+
access&.change_password(args[:password])
|
|
61
|
+
|
|
62
|
+
if !access
|
|
63
|
+
puts "could not find actor..."
|
|
64
|
+
elsif access.actor.invalid?
|
|
65
|
+
puts "failed to change password for '#{access.actor.nickname}'"
|
|
66
|
+
puts "error: #{access.actor.errors.full_messages.join(", ")}"
|
|
67
|
+
else
|
|
68
|
+
puts "changed password for '#{access.actor.nickname}'"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|