masks 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +50 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/builds/application.css +4764 -0
  6. data/app/assets/builds/application.js +8236 -0
  7. data/app/assets/builds/application.js.map +7 -0
  8. data/app/assets/builds/masks/application.css +1 -0
  9. data/app/assets/builds/masks/application.js +7122 -0
  10. data/app/assets/builds/masks/application.js.map +7 -0
  11. data/app/assets/images/masks.png +0 -0
  12. data/app/assets/javascripts/application.js +2 -0
  13. data/app/assets/javascripts/controllers/application.js +9 -0
  14. data/app/assets/javascripts/controllers/emails_controller.js +28 -0
  15. data/app/assets/javascripts/controllers/index.js +12 -0
  16. data/app/assets/javascripts/controllers/keys_controller.js +20 -0
  17. data/app/assets/javascripts/controllers/recover_controller.js +21 -0
  18. data/app/assets/javascripts/controllers/recover_password_controller.js +21 -0
  19. data/app/assets/javascripts/controllers/session_controller.js +94 -0
  20. data/app/assets/manifest.js +2 -0
  21. data/app/assets/masks_manifest.js +2 -0
  22. data/app/assets/stylesheets/application.css +26 -0
  23. data/app/controllers/concerns/masks/controller.rb +114 -0
  24. data/app/controllers/masks/actors_controller.rb +15 -0
  25. data/app/controllers/masks/application_controller.rb +35 -0
  26. data/app/controllers/masks/backup_codes_controller.rb +34 -0
  27. data/app/controllers/masks/debug_controller.rb +9 -0
  28. data/app/controllers/masks/devices_controller.rb +20 -0
  29. data/app/controllers/masks/emails_controller.rb +60 -0
  30. data/app/controllers/masks/error_controller.rb +14 -0
  31. data/app/controllers/masks/keys_controller.rb +45 -0
  32. data/app/controllers/masks/manage/actor_controller.rb +35 -0
  33. data/app/controllers/masks/manage/actors_controller.rb +12 -0
  34. data/app/controllers/masks/manage/base_controller.rb +12 -0
  35. data/app/controllers/masks/one_time_code_controller.rb +49 -0
  36. data/app/controllers/masks/passwords_controller.rb +33 -0
  37. data/app/controllers/masks/recoveries_controller.rb +43 -0
  38. data/app/controllers/masks/sessions_controller.rb +53 -0
  39. data/app/helpers/masks/application_helper.rb +49 -0
  40. data/app/jobs/masks/application_job.rb +7 -0
  41. data/app/jobs/masks/expire_actors_job.rb +15 -0
  42. data/app/jobs/masks/expire_recoveries_job.rb +15 -0
  43. data/app/mailers/masks/actor_mailer.rb +22 -0
  44. data/app/mailers/masks/application_mailer.rb +15 -0
  45. data/app/models/concerns/masks/access.rb +162 -0
  46. data/app/models/concerns/masks/actor.rb +132 -0
  47. data/app/models/concerns/masks/adapter.rb +68 -0
  48. data/app/models/concerns/masks/role.rb +9 -0
  49. data/app/models/concerns/masks/scoped.rb +54 -0
  50. data/app/models/masks/access/actor_password.rb +20 -0
  51. data/app/models/masks/access/actor_scopes.rb +18 -0
  52. data/app/models/masks/access/actor_signup.rb +22 -0
  53. data/app/models/masks/actors/anonymous.rb +40 -0
  54. data/app/models/masks/actors/system.rb +24 -0
  55. data/app/models/masks/adapters/active_record.rb +85 -0
  56. data/app/models/masks/application_model.rb +15 -0
  57. data/app/models/masks/application_record.rb +8 -0
  58. data/app/models/masks/check.rb +192 -0
  59. data/app/models/masks/credential.rb +166 -0
  60. data/app/models/masks/credentials/backup_code.rb +30 -0
  61. data/app/models/masks/credentials/device.rb +59 -0
  62. data/app/models/masks/credentials/email.rb +48 -0
  63. data/app/models/masks/credentials/factor2.rb +71 -0
  64. data/app/models/masks/credentials/key.rb +38 -0
  65. data/app/models/masks/credentials/last_login.rb +12 -0
  66. data/app/models/masks/credentials/masquerade.rb +32 -0
  67. data/app/models/masks/credentials/nickname.rb +63 -0
  68. data/app/models/masks/credentials/one_time_code.rb +34 -0
  69. data/app/models/masks/credentials/password.rb +28 -0
  70. data/app/models/masks/credentials/recovery.rb +71 -0
  71. data/app/models/masks/credentials/session.rb +67 -0
  72. data/app/models/masks/device.rb +30 -0
  73. data/app/models/masks/error.rb +51 -0
  74. data/app/models/masks/event.rb +14 -0
  75. data/app/models/masks/mask.rb +255 -0
  76. data/app/models/masks/rails/actor.rb +190 -0
  77. data/app/models/masks/rails/actor_role.rb +12 -0
  78. data/app/models/masks/rails/device.rb +47 -0
  79. data/app/models/masks/rails/email.rb +96 -0
  80. data/app/models/masks/rails/key.rb +61 -0
  81. data/app/models/masks/rails/recovery.rb +116 -0
  82. data/app/models/masks/rails/role.rb +20 -0
  83. data/app/models/masks/rails/scope.rb +15 -0
  84. data/app/models/masks/session.rb +447 -0
  85. data/app/models/masks/sessions/access.rb +26 -0
  86. data/app/models/masks/sessions/inline.rb +16 -0
  87. data/app/models/masks/sessions/request.rb +42 -0
  88. data/app/resources/masks/actor_resource.rb +9 -0
  89. data/app/resources/masks/session_resource.rb +15 -0
  90. data/app/views/layouts/masks/application.html.erb +17 -0
  91. data/app/views/layouts/masks/mailer.html.erb +17 -0
  92. data/app/views/layouts/masks/mailer.text.erb +1 -0
  93. data/app/views/layouts/masks/manage.html.erb +25 -0
  94. data/app/views/masks/actor_mailer/recover_credentials.html.erb +33 -0
  95. data/app/views/masks/actor_mailer/recover_credentials.text.erb +1 -0
  96. data/app/views/masks/actor_mailer/verify_email.html.erb +34 -0
  97. data/app/views/masks/actor_mailer/verify_email.text.erb +8 -0
  98. data/app/views/masks/actors/current.html.erb +152 -0
  99. data/app/views/masks/application/_header.html.erb +31 -0
  100. data/app/views/masks/backup_codes/new.html.erb +103 -0
  101. data/app/views/masks/emails/new.html.erb +103 -0
  102. data/app/views/masks/emails/verify.html.erb +51 -0
  103. data/app/views/masks/keys/new.html.erb +127 -0
  104. data/app/views/masks/manage/actor/show.html.erb +126 -0
  105. data/app/views/masks/manage/actors/index.html.erb +40 -0
  106. data/app/views/masks/one_time_code/new.html.erb +150 -0
  107. data/app/views/masks/passwords/edit.html.erb +58 -0
  108. data/app/views/masks/recoveries/new.html.erb +71 -0
  109. data/app/views/masks/recoveries/password.html.erb +64 -0
  110. data/app/views/masks/sessions/new.html.erb +153 -0
  111. data/config/brakeman.ignore +28 -0
  112. data/config/locales/en.yml +286 -0
  113. data/config/routes.rb +46 -0
  114. data/db/migrate/20231205173845_create_actors.rb +94 -0
  115. data/lib/generators/masks/install/USAGE +8 -0
  116. data/lib/generators/masks/install/install_generator.rb +33 -0
  117. data/lib/generators/masks/install/templates/initializer.rb +5 -0
  118. data/lib/generators/masks/install/templates/masks.json +6 -0
  119. data/lib/masks/configuration.rb +236 -0
  120. data/lib/masks/engine.rb +25 -0
  121. data/lib/masks/middleware.rb +70 -0
  122. data/lib/masks/version.rb +5 -0
  123. data/lib/masks.rb +183 -0
  124. data/lib/tasks/masks_tasks.rake +71 -0
  125. data/masks.json +274 -0
  126. 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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ VERSION = Gem::Version.new("0.2.0")
5
+ 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