padlock_auth 0.1.0

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