apill 4.1.0 → 4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 905997a468c58168a9f87fbae4df382ced3cc1fd
4
- data.tar.gz: d354947d9ca949dadaad2d25969c5f7865b0652c
3
+ metadata.gz: cab5c932811642e80f8c28a2cd9bf132f2427e72
4
+ data.tar.gz: 9cb2a0d0e0b1a844c37e88eeab958a5952ee5d08
5
5
  SHA512:
6
- metadata.gz: a8576261ae11512401546d1b2e22b6e435921ab67ec2efce00e0ddeabcfff846522d1a2b918dbeff6c7175166004aee450db10eb6fed6cd136487c6c568b867e
7
- data.tar.gz: fdd74cbe6f6245d3b6caa3b730487cbbf04bb0c80c08deecc5a5dab7b84f216fe403732142191d57892a0313d2a65515b30c18474e6a41d5b388b9b1a249192a
6
+ metadata.gz: e10c046dcd6bb020b106670cbe6ea39e3324dba4a4b870f03895d4e25a5124e1fdd9b0eb33fa904398c760c49c0417015775e913c2172cc3509ce736baefa05a
7
+ data.tar.gz: 0f2c1c2de41e550eaa5a66b1d7a779dd52ca5ae331c9736e34b27e97f71839724380fad56ac75e248af6aa34e59cc1515f00f8f1090166d255f2562764481dd6
@@ -1,5 +1,10 @@
1
1
  require 'apill/version'
2
2
 
3
+ require 'apill/authorizers/parameters'
4
+ require 'apill/authorizers/parameters/filtering'
5
+ require 'apill/authorizers/parameters/resource'
6
+ require 'apill/authorizers/query'
7
+ require 'apill/authorizers/scope'
3
8
  require 'apill/configuration'
4
9
  require 'apill/matchers/accept_header'
5
10
  require 'apill/matchers/subdomain'
@@ -0,0 +1,160 @@
1
+ require 'apill/resource/naming'
2
+ require 'apill/resource/model'
3
+
4
+ module Apill
5
+ module AuthorizableResource
6
+ RESOURCE_COLLECTION_ACTIONS = %w{index}.freeze
7
+
8
+ module ClassMethods
9
+ def authorizer_prefix
10
+ @authorizer_prefix ||= name[Resource::Naming::CONTROLLER_RESOURCE_NAME_PATTERN, 2]
11
+ end
12
+
13
+ def authorizer_class
14
+ @authorizer_class ||= "#{authorizer_prefix}" \
15
+ 'Authorizers::' \
16
+ "#{resource_class_name}".
17
+ constantize
18
+ rescue NameError
19
+ 'Apill::Authorizers::Query'.constantize
20
+ end
21
+
22
+ def authorizer_scope_class
23
+ @authorizer_scope_class ||= "#{authorizer_prefix}" \
24
+ 'Authorizers::' \
25
+ "#{resource_class_name}" \
26
+ '::Scope'.
27
+ constantize
28
+ rescue NameError
29
+ 'Apill::Authorizers::Scope'.constantize
30
+ end
31
+
32
+ def authorizer_resource_params_class
33
+ @authorizer_resource_params_class ||= "#{authorizer_prefix}" \
34
+ 'Authorizers::' \
35
+ "#{resource_class_name}" \
36
+ '::ResourceParameters'.
37
+ constantize
38
+ rescue NameError
39
+ 'Apill::Authorizers::Parameters::Resource'.constantize
40
+ end
41
+
42
+ def authorizer_filtering_params_class
43
+ @authorizer_filtering_params_class ||= "#{authorizer_prefix}" \
44
+ 'Authorizers::' \
45
+ "#{resource_class_name}::" \
46
+ 'FilteringParameters'.
47
+ constantize
48
+ rescue NameError
49
+ 'Apill::Authorizers::Parameters::Filtering'.constantize
50
+ end
51
+ end
52
+
53
+ def self.included(base)
54
+ base.include Resource::Naming
55
+ base.extend ClassMethods
56
+
57
+ base.before_action :authorize
58
+ end
59
+
60
+ private
61
+
62
+ def authorize
63
+ HumanError.raise(
64
+ 'ForbiddenError',
65
+ resource_name: self.class.singular_resource_name,
66
+ resource_id: [params[:id]],
67
+ action: action_name,
68
+ ) unless authorizer.public_send(authorization_query)
69
+ end
70
+
71
+ def authorizer
72
+ @authorizer ||= self.
73
+ class.
74
+ authorizer_class.
75
+ new(token: token,
76
+ user: authorized_user,
77
+ resource: authorized_resource)
78
+ end
79
+
80
+ def authorized_scope
81
+ @authorized_scope ||= self.
82
+ class.
83
+ authorizer_scope_class.
84
+ new(token: token,
85
+ user: authorized_user,
86
+ scoped_user_id: scoped_user_id,
87
+ params: authorized_params,
88
+ scope_root: authorized_scope_root).
89
+ call
90
+ end
91
+
92
+ def authorized_params
93
+ @authorized_params ||= authorizer_params_class.
94
+ new(token: token,
95
+ user: authorized_user,
96
+ params: params).
97
+ call
98
+ end
99
+
100
+ def authorized_resource
101
+ return nil if RESOURCE_COLLECTION_ACTIONS.include?(action_name)
102
+
103
+ @authorized_resource ||= public_send(self.class.singular_resource_name)
104
+ end
105
+
106
+ def authorized_collection
107
+ return nil unless RESOURCE_COLLECTION_ACTIONS.include?(action_name)
108
+
109
+ @authorized_collection ||= Resource::Model.
110
+ new(resource: public_send(self.class.plural_resource_name),
111
+ parameters: authorized_params)
112
+ end
113
+
114
+ def authorizer_params_class
115
+ @authorizer_params_class ||= \
116
+ if RESOURCE_COLLECTION_ACTIONS.include?(action_name)
117
+ self.class.authorizer_filtering_params_class
118
+ else
119
+ self.class.authorizer_resource_params_class
120
+ end
121
+ end
122
+
123
+ def authorized_scope_root
124
+ @authorized_scope_root ||= "#{self.class.authorizer_prefix}" \
125
+ "#{self.class.resource_class_name}".
126
+ constantize
127
+ end
128
+
129
+ def scoped_user_id
130
+ @scoped_user_id ||= if requested_user_id.blank?
131
+ nil
132
+ else
133
+ requested_user_id
134
+ end
135
+ end
136
+
137
+ def requested_user_id
138
+ @requested_user_id ||= params.
139
+ fetch(:filter, {}).
140
+ fetch(authorized_user_underscored_class_name,
141
+ authorized_user.id)
142
+ end
143
+
144
+ def authorized_user
145
+ current_user
146
+ end
147
+
148
+ def authorized_user_underscored_class_name
149
+ @authorized_user_underscored_class_name ||= authorized_user.
150
+ class.
151
+ name[/([^:]+)\z/, 1].
152
+ underscore.
153
+ downcase
154
+ end
155
+
156
+ def authorization_query
157
+ @authorization_query ||= "able_to_#{action_name}?"
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,23 @@
1
+ module Apill
2
+ module Authorizers
3
+ class Parameters
4
+ attr_accessor :token,
5
+ :user,
6
+ :params
7
+
8
+ def initialize(token:, user:, params:, **other)
9
+ self.token = token
10
+ self.user = user
11
+ self.params = params
12
+
13
+ other.each do |name, value|
14
+ public_send("#{name}=", value)
15
+ end
16
+ end
17
+
18
+ def call
19
+ params
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ require 'apill/authorizers/parameters'
2
+
3
+ module Apill
4
+ module Authorizers
5
+ class Parameters
6
+ class Filtering < Authorizers::Parameters
7
+ def call
8
+ params.permit(*authorized_params)
9
+ end
10
+
11
+ private
12
+
13
+ def authorized_params
14
+ @authorized_params ||= [
15
+ :sort,
16
+ page: %i{
17
+ number
18
+ size
19
+ offset
20
+ limit
21
+ cursor
22
+ },
23
+ filter: [
24
+ :query,
25
+ {},
26
+ ],
27
+ ]
28
+ end
29
+
30
+ def add_filterable_parameter(name)
31
+ param = params.fetch(:filter, {}).
32
+ fetch(name, nil)
33
+
34
+ if param.class == Array
35
+ authorized_params[1][:filter][1][name] = []
36
+ else
37
+ authorized_params[1][:filter] << name
38
+ end
39
+ end
40
+
41
+ def add_filterable_parameters(*names)
42
+ names.each do |name|
43
+ add_filterable_parameter(name)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,10 @@
1
+ require 'apill/authorizers/parameters'
2
+
3
+ module Apill
4
+ module Authorizers
5
+ class Parameters
6
+ class Resource < Authorizers::Parameters
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,39 @@
1
+ module Apill
2
+ module Authorizers
3
+ class Query
4
+ attr_accessor :token,
5
+ :user,
6
+ :resource
7
+
8
+ def initialize(token:, user:, resource:, **other)
9
+ self.token = token
10
+ self.user = user
11
+ self.resource = resource
12
+
13
+ other.each do |name, value|
14
+ public_send("#{name}=", value)
15
+ end
16
+ end
17
+
18
+ def able_to_index?
19
+ true
20
+ end
21
+
22
+ def able_to_show?
23
+ false
24
+ end
25
+
26
+ def able_to_create?
27
+ false
28
+ end
29
+
30
+ def able_to_update?
31
+ false
32
+ end
33
+
34
+ def able_to_destroy?
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ module Apill
2
+ module Authorizers
3
+ class Scope
4
+ attr_accessor :token,
5
+ :user,
6
+ :scoped_user_id,
7
+ :params,
8
+ :scope_root
9
+
10
+ # rubocop:disable Metrics/ParameterLists
11
+ def initialize(token:, user:, params:, scoped_user_id:, scope_root:, **other)
12
+ self.token = token
13
+ self.user = user
14
+ self.params = params
15
+ self.scoped_user_id = scoped_user_id
16
+ self.scope_root = scope_root
17
+
18
+ other.each do |name, value|
19
+ public_send("#{name}=", value)
20
+ end
21
+ end
22
+ # rubocop:enable Metrics/ParameterLists
23
+
24
+ def call
25
+ scope_root.none
26
+ end
27
+ end
28
+ end
29
+ end
@@ -92,9 +92,9 @@ class Base
92
92
  def authorization_token_from_header
93
93
  case raw_authorization_header
94
94
  when JSON_WEB_TOKEN_HEADER_PATTERN
95
- Tokens::JsonWebToken.convert(
96
- token_private_key: token_private_key,
97
- raw_token: raw_authorization_header[JSON_WEB_TOKEN_HEADER_PATTERN, 1])
95
+ Tokens::JsonWebToken.from_jwe(
96
+ raw_authorization_header[JSON_WEB_TOKEN_HEADER_PATTERN, 1],
97
+ private_key: token_private_key)
98
98
  when BASE64_TOKEN_HEADER_PATTERN
99
99
  Tokens::Base64.convert(
100
100
  raw_token: raw_authorization_header[BASE64_TOKEN_HEADER_PATTERN, 1])
@@ -14,10 +14,9 @@ class Rack < Base
14
14
  def authorization_token_from_params
15
15
  case request['QUERY_STRING']
16
16
  when JSON_WEB_TOKEN_PARAM_PATTERN
17
- Tokens::JsonWebToken.convert(
18
- token_private_key: token_private_key,
19
- raw_token: request['QUERY_STRING'][JSON_WEB_TOKEN_PARAM_PATTERN, 1] || '',
20
- )
17
+ Tokens::JsonWebToken.from_jwe(
18
+ request['QUERY_STRING'][JSON_WEB_TOKEN_PARAM_PATTERN, 1] || '',
19
+ private_key: token_private_key)
21
20
  when BASE64_TOKEN_PARAM_PATTERN
22
21
  base64_token = request['QUERY_STRING'][BASE64_TOKEN_PARAM_PATTERN, 1]
23
22
 
@@ -10,9 +10,9 @@ class Rails < Base
10
10
  def authorization_token_from_params
11
11
  case
12
12
  when request.params.key?(JSON_WEB_TOKEN_PARAM_NAME)
13
- Tokens::JsonWebToken.convert(
14
- token_private_key: token_private_key,
15
- raw_token: request.params[JSON_WEB_TOKEN_PARAM_NAME] || '')
13
+ Tokens::JsonWebToken.from_jwe(
14
+ request.params[JSON_WEB_TOKEN_PARAM_NAME] || '',
15
+ private_key: token_private_key)
16
16
  when request.params.key?(BASE64_TOKEN_PARAM_NAME)
17
17
  Tokens::Base64.convert(raw_token: request.params[BASE64_TOKEN_PARAM_NAME] || '')
18
18
  else
@@ -1,13 +1,13 @@
1
- require 'apill/processable_resource'
1
+ require 'apill/authorizable_resource'
2
2
  require 'human_error/rescuable_resource'
3
3
  require 'human_error/verifiable_resource'
4
4
 
5
5
  module Apill
6
6
  module Resource
7
7
  def self.included(base)
8
- base.include Apill::ProcessableResource
9
8
  base.include HumanError::RescuableResource
10
9
  base.include HumanError::VerifiableResource
10
+ base.include Apill::AuthorizableResource
11
11
  end
12
12
  end
13
13
  end
@@ -0,0 +1,32 @@
1
+ module Apill
2
+ module Resource
3
+ module Naming
4
+ CONTROLLER_RESOURCE_NAME_PATTERN = /\A((.*?::)?.*?)(\w+)Controller\z/
5
+
6
+ module ClassMethods
7
+ def plural_resource_name
8
+ @plural_resource_name ||= name[CONTROLLER_RESOURCE_NAME_PATTERN, 3].
9
+ underscore.
10
+ pluralize.
11
+ downcase
12
+ end
13
+
14
+ def singular_resource_name
15
+ @singular_resource_name ||= name[CONTROLLER_RESOURCE_NAME_PATTERN, 3].
16
+ underscore.
17
+ singularize.
18
+ downcase
19
+ end
20
+
21
+ def resource_class_name
22
+ @resource_class_name ||= singular_resource_name.
23
+ camelize
24
+ end
25
+ end
26
+
27
+ def self.included(base)
28
+ base.extend ClassMethods
29
+ end
30
+ end
31
+ end
32
+ end
@@ -6,10 +6,33 @@ require 'apill/tokens/json_web_tokens/null'
6
6
  module Apill
7
7
  module Tokens
8
8
  class JsonWebToken
9
- attr_accessor :token
9
+ TRANSFORMATION_EXCEPTIONS = [
10
+ JSON::JWT::Exception,
11
+ JSON::JWT::InvalidFormat,
12
+ JSON::JWT::VerificationFailed,
13
+ JSON::JWT::UnexpectedAlgorithm,
14
+ JWT::DecodeError,
15
+ JWT::VerificationError,
16
+ JWT::ExpiredSignature,
17
+ JWT::IncorrectAlgorithm,
18
+ JWT::ImmatureSignature,
19
+ JWT::InvalidIssuerError,
20
+ JWT::InvalidIatError,
21
+ JWT::InvalidAudError,
22
+ JWT::InvalidSubError,
23
+ JWT::InvalidJtiError,
24
+ OpenSSL::PKey::RSAError,
25
+ OpenSSL::Cipher::CipherError,
26
+ ].freeze
10
27
 
11
- def initialize(token:)
12
- self.token = token
28
+ attr_accessor :data,
29
+ :private_key
30
+
31
+ def initialize(data:,
32
+ private_key: Apill.configuration.token_private_key)
33
+
34
+ self.data = data
35
+ self.private_key = private_key
13
36
  end
14
37
 
15
38
  def valid?
@@ -21,40 +44,66 @@ class JsonWebToken
21
44
  end
22
45
 
23
46
  def to_h
24
- token
25
- end
26
-
27
- def self.convert(raw_token:, token_private_key: Apill.configuration.token_private_key)
28
- return JsonWebTokens::Null.instance if raw_token.to_s == ''
29
-
30
- decrypted_token = JSON::JWT.decode(raw_token, token_private_key).plain_text
31
- decoded_token = JWT.decode(decrypted_token,
32
- token_private_key,
33
- true,
34
- algorithm: 'RS256',
35
- verify_expiration: true,
36
- verify_not_before: true,
37
- verify_iat: true,
38
- leeway: 5,
39
- )
40
-
41
- new(token: decoded_token)
42
- rescue JSON::JWT::Exception,
43
- JSON::JWT::InvalidFormat,
44
- JSON::JWT::VerificationFailed,
45
- JSON::JWT::UnexpectedAlgorithm,
46
- JWT::DecodeError,
47
- JWT::VerificationError,
48
- JWT::ExpiredSignature,
49
- JWT::IncorrectAlgorithm,
50
- JWT::ImmatureSignature,
51
- JWT::InvalidIssuerError,
52
- JWT::InvalidIatError,
53
- JWT::InvalidAudError,
54
- JWT::InvalidSubError,
55
- JWT::InvalidJtiError,
56
- OpenSSL::PKey::RSAError
47
+ data
48
+ end
49
+
50
+ def to_jwt
51
+ @jwt ||= JSON::JWT.new(data)
52
+ end
53
+
54
+ def to_jwt_s
55
+ @jwt_s ||= to_jwt.to_s
56
+ end
57
+
58
+ def to_jws
59
+ @jws ||= to_jwt.sign(private_key, 'RS256')
60
+ end
61
+
62
+ def to_jws_s
63
+ @jws_s ||= to_jws.to_s
64
+ end
65
+
66
+ def to_jwe
67
+ @jwe ||= to_jws.encrypt(private_key, 'RSA-OAEP', 'A256GCM')
68
+ end
69
+
70
+ def to_jwe_s
71
+ @jwe_s ||= to_jwe.to_s
72
+ end
73
+
74
+ def self.from_jwe(encrypted_token,
75
+ private_key: Apill.configuration.token_private_key)
76
+
77
+ return JsonWebTokens::Null.instance if encrypted_token.to_s == ''
78
+
79
+ decrypted_token = JSON::JWT.
80
+ decode(encrypted_token, private_key).
81
+ plain_text
82
+
83
+ from_jws(decrypted_token, private_key: private_key)
84
+ rescue *TRANSFORMATION_EXCEPTIONS
85
+ JsonWebTokens::Invalid.instance
86
+ end
87
+
88
+ def self.from_jws(signed_token,
89
+ private_key: Apill.configuration.token_private_key)
90
+
91
+ return JsonWebTokens::Null.instance if signed_token.to_s == ''
92
+
93
+ data = JWT.decode(
94
+ signed_token,
95
+ private_key,
96
+ true,
97
+ algorithm: 'RS256',
98
+ verify_expiration: true,
99
+ verify_not_before: true,
100
+ verify_iat: true,
101
+ leeway: 5,
102
+ )
57
103
 
104
+ new(data: data,
105
+ private_key: private_key)
106
+ rescue *TRANSFORMATION_EXCEPTIONS
58
107
  JsonWebTokens::Invalid.instance
59
108
  end
60
109
  end