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 +4 -4
- data/lib/apill.rb +5 -0
- data/lib/apill/authorizable_resource.rb +160 -0
- data/lib/apill/authorizers/parameters.rb +23 -0
- data/lib/apill/authorizers/parameters/filtering.rb +49 -0
- data/lib/apill/authorizers/parameters/resource.rb +10 -0
- data/lib/apill/authorizers/query.rb +39 -0
- data/lib/apill/authorizers/scope.rb +29 -0
- data/lib/apill/requests/base.rb +3 -3
- data/lib/apill/requests/rack.rb +3 -4
- data/lib/apill/requests/rails.rb +3 -3
- data/lib/apill/resource.rb +2 -2
- data/lib/apill/resource/naming.rb +32 -0
- data/lib/apill/tokens/json_web_token.rb +85 -36
- data/lib/apill/tokens/json_web_token.rb.orig +62 -0
- data/lib/apill/tokens/json_web_tokens/invalid.rb.orig +23 -0
- data/lib/apill/tokens/json_web_tokens/null.rb.orig +23 -0
- data/lib/apill/version.rb +1 -1
- data/spec/apill/authorizers/parameters/filtering_spec.rb +70 -0
- data/spec/apill/authorizers/parameters/resource_spec.rb +11 -0
- data/spec/apill/authorizers/parameters_spec.rb +16 -0
- data/spec/apill/authorizers/query_spec.rb +20 -0
- data/spec/apill/authorizers/scope_spec.rb +19 -0
- data/spec/apill/middleware/api_request_spec.rb +2 -2
- data/spec/apill/requests/rack_spec.rb +7 -7
- data/spec/apill/requests/rails_spec.rb +7 -7
- data/spec/apill/tokens/json_web_token_spec.rb +103 -18
- data/spec/support/private_keys.rb +23 -10
- metadata +27 -7
- data/lib/apill/processable_resource.rb +0 -65
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cab5c932811642e80f8c28a2cd9bf132f2427e72
|
4
|
+
data.tar.gz: 9cb2a0d0e0b1a844c37e88eeab958a5952ee5d08
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e10c046dcd6bb020b106670cbe6ea39e3324dba4a4b870f03895d4e25a5124e1fdd9b0eb33fa904398c760c49c0417015775e913c2172cc3509ce736baefa05a
|
7
|
+
data.tar.gz: 0f2c1c2de41e550eaa5a66b1d7a779dd52ca5ae331c9736e34b27e97f71839724380fad56ac75e248af6aa34e59cc1515f00f8f1090166d255f2562764481dd6
|
data/lib/apill.rb
CHANGED
@@ -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,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
|
data/lib/apill/requests/base.rb
CHANGED
@@ -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.
|
96
|
-
|
97
|
-
|
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])
|
data/lib/apill/requests/rack.rb
CHANGED
@@ -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.
|
18
|
-
|
19
|
-
|
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
|
|
data/lib/apill/requests/rails.rb
CHANGED
@@ -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.
|
14
|
-
|
15
|
-
|
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
|
data/lib/apill/resource.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
require 'apill/
|
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
|
-
|
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
|
-
|
12
|
-
|
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
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|