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.
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