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