padlock_auth 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 295f953fb3b0a6a821cca2f2f18ac16a773d140612d337442bed3f5108400777
4
+ data.tar.gz: c50bbeb53da5386be9305875c842beae00583af59a5fe488dd271c057eaee943
5
+ SHA512:
6
+ metadata.gz: 7e1d4cf307d4d63bf7a44026b209fc26c4cfe839476560da5cab65224285da2f3966a68073a38b7fbab907d878f5725ea1c254a08477fcdc80f8272e0a77756b
7
+ data.tar.gz: 2689fe7dd1d69bff8933967ee33523235041aa34b5ec84714ec87f26ac6dc3ff22886ebf6c92eea34c5e3305831ca3192800415d7717173e9368b46e28b8a4f4
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Ben Morrall
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,275 @@
1
+ # PadlockAuth
2
+
3
+ PadlockAuth allows you to secure your Rails application using access tokens provided by an external provider.
4
+
5
+ ## Usage
6
+
7
+ PadlockAuth separates the __how__ of token verification from the __where__ authentication occurs. You configure an authentication strategy in an initializer and use callbacks in controllers to secure endpoints, allowing strategy changes without modifying your controllers.
8
+
9
+ Designed for lightweight use, PadlockAuth is ideal for microservices or high-volume APIs, with support ranging from simple token matching to more complex JWT-based authentication. Unlike [Devise](https://github.com/heartcombo/devise) or [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper), PadlockAuth doesn't require a database, making it more suitable for microservices and lightweight scenarios.
10
+
11
+ ### Configuring a Basic Token Strategy
12
+
13
+ The Basic Token Strategy is a simple authentication mechanism where the token received in the request is compared with a pre-configured secret key. This strategy is ideal for straightforward use cases where you only need to validate the presence of a valid token. It does not provide any scopes to be authenticated against.
14
+
15
+ #### Example Configuration
16
+
17
+ ```ruby
18
+ # config/initializers/padlock_auth.rb
19
+ PadlockAuth.configure do
20
+ secure_with :token do
21
+ secret_key "MySecretKey"
22
+ end
23
+ end
24
+ ```
25
+
26
+ In this example:
27
+
28
+ - The `:token` strategy validates the request's token by comparing it with the configured `secret_key`.
29
+
30
+ ### Configuring a Custom Strategy
31
+
32
+ A Custom Strategy in PadlockAuth allows you to implement your own authentication logic. This requires creating a custom Strategy class that generates an Access Token class.
33
+
34
+ #### 1. Define the Strategy Class
35
+
36
+ The Strategy class should inherit from `PadlockAuth::AbstractStrategy` and implement the following methods:
37
+
38
+ - `build_access_token`: Builds an access token from a provided String access token.
39
+ - `build_access_token_from_credentials`: Builds an access token from provided username and password.
40
+
41
+ Both methods should return an `AccessToken` object.
42
+
43
+ ```ruby
44
+ class MyCustomStrategy < PadlockAuth::AbstractStrategy
45
+ def build_access_token(token_string)
46
+ # Logic to build an access token from a string token
47
+ MyAccessToken.new(token_string)
48
+ end
49
+
50
+ def build_access_token_from_credentials(username, password)
51
+ # Logic to build an access token from provided credentials
52
+ MyAccessToken.new(generate_token_from_credentials(username, password))
53
+ end
54
+ end
55
+ ```
56
+
57
+ #### 2. Define the AccessToken Class
58
+
59
+ The `AccessToken` class should inherit from `PadlockAuth::AbstractAccessToken` and implement the following methods:
60
+
61
+ - `accessible?`: Returns `true` if the access token is valid. This means the token:
62
+ - Matches the expected token value, and
63
+ - Contains any required attributes, and
64
+ - Has not expired.
65
+ - `includes_scope?`: Returns `true` if the access token matches at least one of the provided scope values.
66
+
67
+ ```ruby
68
+ class MyAccessToken < PadlockAuth::AbstractAccessToken
69
+ def accessible?
70
+ # Check token validity logic
71
+ valid_token? && required_attributes_present? && not_expired?
72
+ end
73
+
74
+ def includes_scope?(scopes)
75
+ # Check if the token includes at least one of the provided scopes
76
+ (scopes & token_scopes).any?
77
+ end
78
+ end
79
+ ```
80
+
81
+ #### 3. Configure the Custom Strategy
82
+
83
+ Finally, configure PadlockAuth to use your custom strategy within the initializer:
84
+
85
+ ```ruby
86
+ # config/initializers/padlock_auth.rb
87
+ PadlockAuth.configure do
88
+ secure_with MyCustomStrategy.new
89
+ end
90
+ ```
91
+
92
+ This configuration allows you to implement a fully custom authentication strategy that integrates with PadlockAuth.
93
+
94
+ ### Securing a Rails Controller
95
+
96
+ The `padlock_authorize!` method secures your API endpoints and optionally enforces scope requirements. The verification of scopes is managed by the configured authentication strategy.
97
+
98
+ #### Example: Specifying Scopes
99
+
100
+ You can specify multiple scopes in a single call:
101
+
102
+ ```ruby
103
+ before_action { padlock_authorize! :read, :write }
104
+ ```
105
+
106
+ In this example:
107
+
108
+ - The action requires the access token to include either the :read **or** :write scope.
109
+
110
+ Alternatively, you can require multiple scopes by calling padlock_authorize! separately for each:
111
+
112
+ ```ruby
113
+ before_action :require_read_and_write_scopes
114
+
115
+ private
116
+
117
+ def require_read_and_write_scopes
118
+ padlock_authorize! :read
119
+ padlock_authorize! :write
120
+ end
121
+ ```
122
+
123
+ In this case:
124
+
125
+ The action requires the access token to include **both** the :read and :write scopes.
126
+
127
+ #### Example: Using Default Scopes
128
+
129
+ When no scopes are provided to `padlock_authorize!`, the default_scopes configuration will be applied. You can configure the default_scopes value during setup:
130
+
131
+ ```ruby
132
+ PadlockAuth.configure do
133
+ secure_with MyCustomStrategy
134
+ default_scopes [:read] # Optional, defines the default required scopes
135
+ end
136
+ ```
137
+
138
+ In this case:
139
+
140
+ If `padlock_authorize!` is called without explicit scopes, the `:read` scope will be enforced by default.
141
+
142
+ For example:
143
+
144
+ ```ruby
145
+ before_action :padlock_authorize!
146
+ ```
147
+
148
+ - If the token includes the `:read` scope, the action will proceed.
149
+ - If `default_scopes` is not set, no scopes are enforced by default when padlock_authorize! is called without scopes..
150
+
151
+ ### Providing Access Token Credentials
152
+
153
+ You can configure PadlockAuth to support different ways of extracting a single access token by specifying an array of access_token_methods:
154
+
155
+ ```ruby
156
+ PadlockAuth.configure do
157
+ access_token_methods [
158
+ :from_bearer_authorization, # Extracts token from Authorization header with Bearer token
159
+ :from_access_token_param, # Extracts token from access_token param
160
+ :from_bearer_param # Extracts token from bearer param
161
+ ]
162
+ end
163
+ ```
164
+
165
+ #### Token Extraction Methods
166
+
167
+ - `from_bearer_authorization`: Extracts the token from an `Authorization` header with a Bearer token (i.e. Bearer VALID_ACCESS_TOKEN).
168
+ - `from_access_token_param`: Extracts the token from an `access_token` parameter in the query string or form data.
169
+ - `from_bearer_param`: Extracts the token from a `bearer` parameter in the query string or form data.
170
+
171
+ These methods will call `build_access_token` with the provided strategy to create an AccessToken object.
172
+
173
+ #### Example: Calling an Endpoint with a Bearer Token
174
+
175
+ Here’s an example of how to call an endpoint with an access token using the `from_bearer_authorization` method. The token will be extracted from the Authorization header:
176
+
177
+ ```ruby
178
+ require 'net/http'
179
+ require 'uri'
180
+
181
+ uri = URI.parse("http://example.com/api/endpoint")
182
+ http = Net::HTTP.new(uri.host, uri.port)
183
+
184
+ request = Net::HTTP::Get.new(uri)
185
+ request["Authorization"] = "Bearer VALID_ACCESS_TOKEN"
186
+
187
+ response = http.request(request)
188
+ puts response.body
189
+ ```
190
+
191
+ In this example:
192
+
193
+ - The Bearer token `VALID_ACCESS_TOKEN` is passed in the Authorization header.
194
+ - PadlockAuth will extract the token using the `from_bearer_authorization` method and validate it
195
+
196
+ #### Example: Calling an Endpoint with a Token Parameter
197
+
198
+ You can also pass the token as a query parameter. Here’s an example of how to call the same endpoint with the token passed in the `access_token` parameter:
199
+
200
+ ```ruby
201
+ uri = URI.parse("http://example.com/api/endpoint?access_token=VALID_ACCESS_TOKEN")
202
+ http = Net::HTTP.new(uri.host, uri.port)
203
+
204
+ request = Net::HTTP::Get.new(uri)
205
+ response = http.request(request)
206
+ puts response.body
207
+ ```
208
+
209
+ PadlockAuth will extract the token from the `access_token` parameter and validate it.
210
+
211
+ ### Providing Credentials with Username and Password
212
+
213
+ You can also provide username and password credentials by adding the `from_basic_authorization` method:
214
+
215
+ ```ruby
216
+ PadlockAuth.configure do
217
+ access_token_methods [
218
+ :from_basic_authorization # Extracts token from a HTTP BASIC AUTHORIZATION header
219
+ ]
220
+ end
221
+ ```
222
+
223
+ - `from_basic_authorization`: Extracts the Username and Password from a Basic Authorization header.
224
+
225
+ #### Example: Calling an Endpoint with Basic Authentication
226
+
227
+ If you need to authenticate with a username and password, you can send the credentials in the `Authorization` header using Basic Authentication:
228
+
229
+ ```ruby
230
+ uri = URI.parse("http://example.com/api/endpoint")
231
+ http = Net::HTTP.new(uri.host, uri.port)
232
+
233
+ request = Net::HTTP::Get.new(uri)
234
+ request.basic_auth 'username', 'secret'
235
+
236
+ response = http.request(request)
237
+ puts response.body
238
+ ```
239
+
240
+ PadlockAuth will extract the username and password using the `from_basic_authorization` method and use them to generate an access token.
241
+
242
+ ## Installation
243
+
244
+ Add this line to your application's Gemfile:
245
+
246
+ ```ruby
247
+ gem "padlock_auth"
248
+ ```
249
+
250
+ And then execute:
251
+ ```bash
252
+ $ bundle
253
+ ```
254
+
255
+ Or install it yourself as:
256
+ ```bash
257
+ $ gem install padlock_auth
258
+ ```
259
+
260
+ ## Development
261
+
262
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake` to run the tests and code quality checks.
263
+
264
+ Generate documentaion using `rake yard`, which can be found in the `/doc` directory.
265
+
266
+ ## Contributing
267
+
268
+ Bug reports and pull requests are welcome on GitHub at https://github.com/bmorrall/padlock_auth. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/bmorrall/padlock_auth/blob/main/CODE_OF_CONDUCT.md).
269
+
270
+ ## License
271
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
272
+
273
+ ## Code of Conduct
274
+
275
+ Everyone interacting in the PadlockAuth project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/bmorrall/padlock_auth/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require "yard"
6
+ YARD::Rake::YardocTask.new do |t|
7
+ t.files = ["lib/**/*.rb"]
8
+ end
9
+
10
+ require "rspec/core/rake_task"
11
+ RSpec::Core::RakeTask.new(:spec)
12
+
13
+ require "standard/rake"
14
+
15
+ task default: %i[
16
+ spec
17
+ standard
18
+ ]
@@ -0,0 +1,14 @@
1
+ en:
2
+ padlock_auth:
3
+ errors:
4
+ messages:
5
+ server_error: "An error occurred while processing your request."
6
+
7
+ invalid_token:
8
+ revoked: "The access token was revoked."
9
+ expired: "The access token has expired."
10
+ unknown: "The access token is invalid."
11
+
12
+ forbidden_token:
13
+ missing_scope: 'Access to this resource requires scope "%{oauth_scopes}".'
14
+ unknown: "The access token is forbidden."
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PadlockAuth
4
+ # @abstract
5
+ #
6
+ # AbstractAccessToken is a base class for all access token classes.
7
+ #
8
+ # It provides all methods that are required for an access token to be
9
+ # compatible with PadlockAuth.
10
+ #
11
+ # All implemented methods will default to returning false or nil, so that
12
+ # any authentication/authorisation attempt will fail unless the methods are implemented.
13
+ class AbstractAccessToken
14
+ # Indicates if token is acceptable for specific scopes.
15
+ #
16
+ # @param scopes [Array<String>] scopes
17
+ #
18
+ # @return [Boolean] true if record is accessible and includes scopes, or false in other cases
19
+ #
20
+ def acceptable?(scopes)
21
+ accessible? && includes_scope?(scopes)
22
+ end
23
+
24
+ # Indicates the access token matches the specific criteria of the strategy to
25
+ # be considered a valid access token.
26
+ #
27
+ # Tokens failing to be accessible will be rejected as an invalid grant request,
28
+ # with a 401 Unauthorized response.
29
+ #
30
+ # @abstract Implement this method in your access token class
31
+ #
32
+ # @return [Boolean] true if the token is accessible, false otherwise
33
+ #
34
+ def accessible?
35
+ Kernel.warn "[PADLOCK_AUTH] #accessible? not implemented for #{self.class}"
36
+ false
37
+ end
38
+
39
+ # Provides a lookup key for the reason the token is invalid.
40
+ #
41
+ # Messages will use the i18n scope `padlock_auth.errors.messages.invalid_token`,
42
+ # with the default key of :unknown, providing a generic error message.
43
+ #
44
+ # @return [Symbol] the reason the token is invalid
45
+ #
46
+ def invalid_token_reason
47
+ :unknown
48
+ end
49
+
50
+ # Indicates if the token includes the required scopes/audience.
51
+ #
52
+ # Tokens failing to include the required scopes will be rejected as an invalid scope request,
53
+ # with a 403 Forbidden response.
54
+ #
55
+ # @abstract Implement this method in your access token class
56
+ #
57
+ # @param _required_scopes [Boolean] true if the token includes the required scopes, false otherwise
58
+ #
59
+ def includes_scope?(_required_scopes)
60
+ Kernel.warn "[PADLOCK_AUTH] #includes_scope? not implemented for #{self.class}"
61
+ false
62
+ end
63
+
64
+ # Provides a lookup key for the reason the token is forbidden.
65
+ #
66
+ # Messages will use the i18n scope `padlock_auth.errors.messages.forbidden_token`,
67
+ # with the default key of :missing_scope, providing a generic error message.
68
+ #
69
+ # The required scopes are passed as an argument to the i18n for some user feedback as required.
70
+ #
71
+ # @return [Symbol] the reason the token is forbidden
72
+ #
73
+ def forbidden_token_reason
74
+ :unknown
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,52 @@
1
+ module PadlockAuth
2
+ # @abstract
3
+ #
4
+ # Abstract strategy for building and authenticating access tokens.
5
+ #
6
+ # Strategies are responsible for building access tokens and authenticating them.
7
+ class AbstractStrategy
8
+ # Build an access token from a raw token.
9
+ #
10
+ # @param _raw_token [String] The raw token
11
+ #
12
+ # @return [PadlockAuth::AbstractAccessToken, nil] The access token
13
+ def build_access_token(_raw_token)
14
+ Kernel.warn "[PADLOCK_AUTH] #build_access_token not implemented for #{self.class}"
15
+ nil
16
+ end
17
+
18
+ # Build an access token from credentials.
19
+ #
20
+ # @param _username [String] The username
21
+ # @param _password [String] The password
22
+ #
23
+ # @return [PadlockAuth::AbstractAccessToken, nil] The access token
24
+ def build_access_token_from_credentials(_username, _password)
25
+ Kernel.warn "[PADLOCK_AUTH] #build_access_token_from_credentials not implemented for #{self.class}"
26
+ nil
27
+ end
28
+
29
+ # Build an invalid token response.
30
+ #
31
+ # Used to indicate that a token is invalid.
32
+ #
33
+ # @param access_token [PadlockAuth::AbstractAccessToken] The access token
34
+ #
35
+ # @return [PadlockAuth::Http::InvalidTokenResponse] The response
36
+ def build_invalid_token_response(access_token)
37
+ Http::InvalidTokenResponse.from_access_token(access_token)
38
+ end
39
+
40
+ # Build a forbidden token response.
41
+ #
42
+ # Used to indicate that a token does not have the required scopes.
43
+ #
44
+ # @param access_token [PadlockAuth::AbstractAccessToken] The access token
45
+ # @param scopes [PadlockAuth::Config::Scopes] The required scopes
46
+ #
47
+ # @return [PadlockAuth::Http::ForbiddenTokenResponse] The response
48
+ def build_forbidden_token_response(access_token, scopes)
49
+ Http::ForbiddenTokenResponse.from_access_token(access_token, scopes)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PadlockAuth
4
+ class Config
5
+ ##
6
+ # PadlockAuth configuration option DSL.
7
+ #
8
+ # Adds configuration methods to a builder class which will be used to configure the object.
9
+ #
10
+ # Adds accessor methods to the object being configured.
11
+ #
12
+ # @example
13
+ # class MyConfig
14
+ # class Builder < PadlockAuth::Config::AbstractBuilder; end
15
+ #
16
+ # mattr_reader(:builder_class) { Builder }
17
+ #
18
+ # extend PadlockAuth::Config::Option
19
+ #
20
+ # option :name
21
+ # end
22
+ #
23
+ # config = MyConfig.builder_class.build do
24
+ # name 'My Name'
25
+ # end
26
+ # config.name # => 'My Name'
27
+ #
28
+ module Option
29
+ # Defines configuration option
30
+ #
31
+ # When you call option, it defines two methods. One method will take place
32
+ # in the +Config+ class and the other method will take place in the
33
+ # +Builder+ class.
34
+ #
35
+ # The +name+ parameter will set both builder method and config attribute.
36
+ # If the +:as+ option is defined, the builder method will be the specified
37
+ # option while the config attribute will be the +name+ parameter.
38
+ #
39
+ # If you want to introduce another level of config DSL you can
40
+ # define +builder_class+ parameter.
41
+ # Builder should take a block as the initializer parameter and respond to function +build+
42
+ # that returns the value of the config attribute.
43
+ #
44
+ # @param name [Symbol] The name of the configuration option
45
+ # @param options [Hash] The options hash which can contain:
46
+ # - as [String] Set the builder method that goes inside +configure+ block
47
+ # - default [Object] The default value in case no option was set
48
+ # - builder_class [Class] Configuration option builder class
49
+ #
50
+ #
51
+ # @example
52
+ # option :name
53
+ #
54
+ # @example
55
+ # option :name, as: :set_name
56
+ #
57
+ # @example
58
+ # option :name, default: 'My Name'
59
+ #
60
+ # @example
61
+ # option :scopes, builder_class: ScopesBuilder
62
+ #
63
+ #
64
+ def option(name, options = {})
65
+ attribute = options[:as] || name
66
+
67
+ builder_class.instance_eval do
68
+ if method_defined?(name)
69
+ Kernel.warn "[PADLOCK_AUTH] Option #{self.name}##{name} already defined and will be overridden"
70
+ remove_method name
71
+ end
72
+
73
+ define_method name do |*args, &block|
74
+ if (deprecation_opts = options[:deprecated])
75
+ warning = "[PADLOCK_AUTH] #{self.class.name}##{name} has been deprecated and will soon be removed"
76
+ warning = "#{warning}\n#{deprecation_opts.fetch(:message)}" if deprecation_opts.is_a?(Hash)
77
+
78
+ Kernel.warn(warning)
79
+ end
80
+
81
+ value = block || args.first
82
+
83
+ config.instance_variable_set(:"@#{attribute}", value)
84
+ end
85
+ end
86
+
87
+ define_method attribute do |*_args|
88
+ if instance_variable_defined?(:"@#{attribute}")
89
+ instance_variable_get(:"@#{attribute}")
90
+ else
91
+ options[:default]
92
+ end
93
+ end
94
+
95
+ public attribute
96
+ end
97
+
98
+ # Uses extended hook to ensure builder_class is defined.
99
+ #
100
+ # Implementing classes should define a +builder_class+ method that returns a builder class.
101
+ #
102
+ def self.extended(base)
103
+ return if base.respond_to?(:builder_class)
104
+
105
+ raise NotImplementedError,
106
+ "Define `self.builder_class` method for #{base} that returns your custom Builder class to use options DSL!"
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PadlockAuth
4
+ class Config
5
+ # Represents a collection of scopes.
6
+ #
7
+ class Scopes
8
+ include Enumerable
9
+ include Comparable
10
+
11
+ # Create a new Scopes instance from a string.
12
+ #
13
+ # @param string [String] A space-separated string of scopes
14
+ #
15
+ # @return [PadlockAuth::Config::Scopes] A new Scopes instance
16
+ def self.from_string(string)
17
+ string ||= ""
18
+ new.tap do |scope|
19
+ scope.add(*string.split(/\s+/))
20
+ end
21
+ end
22
+
23
+ # Create a new Scopes instance from an array.
24
+ #
25
+ # @param array [Array<String>] An array of scopes
26
+ #
27
+ # @return [PadlockAuth::Config::Scopes] A new Scopes instance
28
+ #
29
+ def self.from_array(*array)
30
+ new.tap do |scope|
31
+ scope.add(*array)
32
+ end
33
+ end
34
+
35
+ delegate :each, :empty?, to: :@scopes
36
+
37
+ # Initialize a new Scopes instance.
38
+ def initialize
39
+ @scopes = []
40
+ end
41
+
42
+ # Check if a scope exists in the collection.
43
+ #
44
+ # @param scope [String] The scope to check
45
+ #
46
+ # @return [Boolean] True if the scope exists
47
+ #
48
+ def exists?(scope)
49
+ @scopes.include? scope.to_s
50
+ end
51
+
52
+ # Add a scope to the collection.
53
+ #
54
+ # @param scopes [Array<String>] The scopes to add
55
+ #
56
+ def add(*scopes)
57
+ @scopes.push(*scopes.flatten.compact.map(&:to_s))
58
+ @scopes.uniq!
59
+ end
60
+
61
+ # Returns all scopes in the collection.
62
+ #
63
+ # @return [Array<String>] All scopes
64
+ def all
65
+ @scopes
66
+ end
67
+
68
+ # Returns all scopes as a string.
69
+ #
70
+ # @return [String] All scopes as a space-joined string
71
+ def to_s
72
+ @scopes.join(" ")
73
+ end
74
+
75
+ # Returns true if all scopes exist in the collection.
76
+ #
77
+ # @param scopes [Array<String>] The scopes to check
78
+ #
79
+ # @return [Boolean] True if all scopes exist
80
+ def scopes?(scopes)
81
+ scopes.all? { |scope| exists?(scope) }
82
+ end
83
+
84
+ alias_method :has_scopes?, :scopes?
85
+
86
+ # Adds two collections of scopes together.
87
+ #
88
+ def +(other)
89
+ self.class.from_array(all + to_array(other))
90
+ end
91
+
92
+ # Compares two collections of scopes.
93
+ #
94
+ # @param other [PadlockAuth::Config::Scopes, Array<String>] The other collection
95
+ #
96
+ def <=>(other)
97
+ if other.respond_to?(:map)
98
+ map(&:to_s).sort <=> other.map(&:to_s).sort
99
+ else
100
+ super
101
+ end
102
+ end
103
+
104
+ # Returns a scopes array with elements contained in both collections.
105
+ #
106
+ # @param other [PadlockAuth::Config::Scopes, Array<String>] The other collection
107
+ #
108
+ def &(other)
109
+ self.class.from_array(all & to_array(other))
110
+ end
111
+
112
+ private
113
+
114
+ def to_array(other)
115
+ case other
116
+ when Scopes
117
+ other.all
118
+ else
119
+ other.to_a
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end