masks 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|