fun_with_json_api 0.0.5 → 0.0.6.pre.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/config/locales/fun_with_json_api.en.yml +3 -0
- data/lib/fun_with_json_api/attributes/relationship.rb +11 -3
- data/lib/fun_with_json_api/attributes/relationship_collection.rb +21 -6
- data/lib/fun_with_json_api/deserializer.rb +12 -1
- data/lib/fun_with_json_api/deserializer_class_methods.rb +27 -17
- data/lib/fun_with_json_api/exceptions/unauthorized_resource.rb +16 -0
- data/lib/fun_with_json_api/find_collection_from_document.rb +9 -33
- data/lib/fun_with_json_api/find_resource_from_document.rb +10 -3
- data/lib/fun_with_json_api/schema_validators/check_collection_has_all_members.rb +67 -0
- data/lib/fun_with_json_api/schema_validators/check_collection_is_authorized.rb +63 -0
- data/lib/fun_with_json_api/schema_validators/check_relationships.rb +2 -2
- data/lib/fun_with_json_api/schema_validators/check_resource_is_authorized.rb +50 -0
- data/lib/fun_with_json_api/version.rb +1 -1
- data/spec/dummy/log/test.log +34269 -0
- data/spec/fun_with_json_api/deserializer_spec.rb +52 -10
- data/spec/fun_with_json_api/find_collection_from_document_spec.rb +82 -4
- data/spec/fun_with_json_api/find_resource_from_document_spec.rb +36 -2
- data/spec/fun_with_json_api/schema_validators/check_relationships_spec.rb +2 -2
- data/spec/fun_with_json_api_spec.rb +68 -0
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa43a36433dec9d40e3a4afaad17becacfad028f
|
4
|
+
data.tar.gz: 468d6bda13144854fa1771a0e113e3d906a223f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 028179ff069e5a7130e78ca19208113d29b3c93fff1c437e56cf1355e76f53efd465800716502a1c5f1f580a51c4556f45855ed3d0925649d1436270043138c5
|
7
|
+
data.tar.gz: 5f17c1140f3ce92040c395f5f0f303b9ab5eeaac98c1b4bca61f1295aeffb6986f4cf5292b593cadaa8adbaa17d43985e95b353e9b82e4b376ff7151b790f5bf
|
@@ -7,6 +7,7 @@ en:
|
|
7
7
|
illegal_client_generated_identifier: 'Request json_api attempted to set an unsupported client-generated id'
|
8
8
|
invalid_document_type: 'Request json_api data type does not match endpoint'
|
9
9
|
missing_resource: 'Unable to find the requested resource'
|
10
|
+
unauthorized_resource: 'Unable to access the requested resource'
|
10
11
|
invalid_attribute: 'Request json_api attribute data is invalid'
|
11
12
|
unknown_attribute: 'Request json_api attribute is unsupported by the current endpoint'
|
12
13
|
invalid_relationship: 'Request json_api relationship data is invalid'
|
@@ -34,7 +35,9 @@ en:
|
|
34
35
|
invalid_document: "Expected data to be a Hash or null"
|
35
36
|
invalid_document_type: "Expected data type to be a '%{resource}' resource"
|
36
37
|
missing_resource: "Unable to find '%{resource}' with matching id: '%{resource_id}'"
|
38
|
+
unauthorized_resource: "Unable to assign the requested '%{resource}' (%{resource_id}) to the current resource"
|
37
39
|
find_collection_from_document:
|
38
40
|
invalid_document: "Expected data to be a Array of '%{resource}' resources"
|
39
41
|
invalid_document_type: "Expected '%{type}' to be a '%{resource}' resource"
|
40
42
|
missing_resource: "Unable to find '%{resource}' with matching id: '%{resource_id}'"
|
43
|
+
unauthorized_resource: "Unable to assign the requested '%{resource}' (%{resource_id}) to the current resource"
|
@@ -29,9 +29,11 @@ module FunWithJsonApi
|
|
29
29
|
end
|
30
30
|
|
31
31
|
resource = deserializer.load_resource_from_id_value(id_value)
|
32
|
-
|
32
|
+
raise build_missing_relationship_error(id_value) if resource.nil?
|
33
33
|
|
34
|
-
|
34
|
+
check_resource_is_authorized!(resource, id_value)
|
35
|
+
|
36
|
+
resource.id
|
35
37
|
end
|
36
38
|
|
37
39
|
# rubocop:disable Style/PredicateName
|
@@ -52,6 +54,12 @@ module FunWithJsonApi
|
|
52
54
|
|
53
55
|
private
|
54
56
|
|
57
|
+
def check_resource_is_authorized!(resource, id_value)
|
58
|
+
SchemaValidators::CheckResourceIsAuthorised.call(
|
59
|
+
resource, id_value, deserializer, prefix: "/data/relationships/#{name}/data"
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
55
63
|
def build_deserializer_from_options
|
56
64
|
if @deserializer_class.respond_to?(:call)
|
57
65
|
@deserializer_class.call
|
@@ -72,7 +80,7 @@ module FunWithJsonApi
|
|
72
80
|
def build_missing_relationship_error(id_value, message = nil)
|
73
81
|
message ||= missing_resource_debug_message(id_value)
|
74
82
|
payload = ExceptionPayload.new
|
75
|
-
payload.pointer = "/data/relationships/#{name}/id"
|
83
|
+
payload.pointer = "/data/relationships/#{name}/data/id"
|
76
84
|
payload.detail = "Unable to find '#{deserializer.type}' with matching id"\
|
77
85
|
": #{id_value.inspect}"
|
78
86
|
Exceptions::MissingRelationship.new(message, payload)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'fun_with_json_api/schema_validators/check_collection_is_authorized'
|
2
|
+
|
1
3
|
module FunWithJsonApi
|
2
4
|
module Attributes
|
3
5
|
class RelationshipCollection < FunWithJsonApi::Attribute
|
@@ -29,11 +31,10 @@ module FunWithJsonApi
|
|
29
31
|
collection = deserializer.load_collection_from_id_values(values)
|
30
32
|
|
31
33
|
# Ensure the collection size matches
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
34
|
+
check_collection_matches_values!(collection, values)
|
35
|
+
|
36
|
+
# Ensure the user is authorized to access the collection
|
37
|
+
check_collection_is_authorized!(collection, values)
|
37
38
|
|
38
39
|
# Call ActiceRecord#pluck if it is available
|
39
40
|
convert_collection_to_ids(collection)
|
@@ -72,6 +73,20 @@ module FunWithJsonApi
|
|
72
73
|
end
|
73
74
|
end
|
74
75
|
|
76
|
+
def check_collection_matches_values!(collection, values)
|
77
|
+
expected_size = values.size
|
78
|
+
result_size = collection.size
|
79
|
+
if result_size != expected_size
|
80
|
+
raise build_missing_relationship_error_from_collection(collection, values)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def check_collection_is_authorized!(collection, values)
|
85
|
+
SchemaValidators::CheckCollectionIsAuthorised.call(
|
86
|
+
collection, values, deserializer, prefix: "/data/relationships/#{name}/data"
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
75
90
|
def convert_collection_to_ids(collection)
|
76
91
|
if collection.respond_to? :pluck
|
77
92
|
# Well... pluck+arel doesn't work with SQLite, but select at least is safe
|
@@ -105,7 +120,7 @@ module FunWithJsonApi
|
|
105
120
|
next if collection_ids.include?(resource_id)
|
106
121
|
|
107
122
|
ExceptionPayload.new.tap do |payload|
|
108
|
-
payload.pointer = "/data/relationships/#{name}/#{index}/id"
|
123
|
+
payload.pointer = "/data/relationships/#{name}/data/#{index}/id"
|
109
124
|
payload.detail = "Unable to find '#{deserializer.type}' with matching id"\
|
110
125
|
": \"#{resource_id}\""
|
111
126
|
end
|
@@ -6,6 +6,13 @@ module FunWithJsonApi
|
|
6
6
|
class Deserializer
|
7
7
|
extend FunWithJsonApi::DeserializerClassMethods
|
8
8
|
|
9
|
+
# Fake resource_authorizer that always returns 'authorised'
|
10
|
+
class ResourceAuthorizerDummy
|
11
|
+
def call(_)
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
9
16
|
# Creates a new instance of a
|
10
17
|
def self.create(options = {})
|
11
18
|
new(options)
|
@@ -16,7 +23,6 @@ module FunWithJsonApi
|
|
16
23
|
|
17
24
|
attr_reader :id_param
|
18
25
|
attr_reader :type
|
19
|
-
attr_reader :resource_class
|
20
26
|
|
21
27
|
attr_reader :attributes
|
22
28
|
|
@@ -25,6 +31,7 @@ module FunWithJsonApi
|
|
25
31
|
@type = options.fetch(:type) { self.class.type }
|
26
32
|
@resource_class = options[:resource_class]
|
27
33
|
@resource_collection = options[:resource_collection] if @type
|
34
|
+
@resource_authorizer = options[:resource_authorizer]
|
28
35
|
load_attributes_from_options(options)
|
29
36
|
load_relationships_from_options(options)
|
30
37
|
end
|
@@ -59,6 +66,10 @@ module FunWithJsonApi
|
|
59
66
|
@resource_collection ||= resource_class
|
60
67
|
end
|
61
68
|
|
69
|
+
def resource_authorizer
|
70
|
+
@resource_authorizer ||= ResourceAuthorizerDummy.new
|
71
|
+
end
|
72
|
+
|
62
73
|
def relationships
|
63
74
|
relationship_lookup.values
|
64
75
|
end
|
@@ -25,9 +25,11 @@ module FunWithJsonApi
|
|
25
25
|
# Attributes
|
26
26
|
|
27
27
|
def attribute(name, options = {})
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
lock.synchronize do
|
29
|
+
Attribute.create(name, options).tap do |attribute|
|
30
|
+
add_parse_attribute_method(attribute)
|
31
|
+
attributes << attribute
|
32
|
+
end
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
@@ -38,26 +40,30 @@ module FunWithJsonApi
|
|
38
40
|
# Relationships
|
39
41
|
|
40
42
|
def belongs_to(name, deserializer_class_or_callable, options = {})
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
43
|
+
lock.synchronize do
|
44
|
+
Attributes::Relationship.create(
|
45
|
+
name,
|
46
|
+
deserializer_class_or_callable,
|
47
|
+
options
|
48
|
+
).tap do |relationship|
|
49
|
+
add_parse_resource_method(relationship)
|
50
|
+
relationships << relationship
|
51
|
+
end
|
48
52
|
end
|
49
53
|
end
|
50
54
|
|
51
55
|
# rubocop:disable Style/PredicateName
|
52
56
|
|
53
57
|
def has_many(name, deserializer_class_or_callable, options = {})
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
58
|
+
lock.synchronize do
|
59
|
+
Attributes::RelationshipCollection.create(
|
60
|
+
name,
|
61
|
+
deserializer_class_or_callable,
|
62
|
+
options
|
63
|
+
).tap do |relationship|
|
64
|
+
add_parse_resource_method(relationship)
|
65
|
+
relationships << relationship
|
66
|
+
end
|
61
67
|
end
|
62
68
|
end
|
63
69
|
|
@@ -80,6 +86,10 @@ module FunWithJsonApi
|
|
80
86
|
|
81
87
|
private
|
82
88
|
|
89
|
+
def lock
|
90
|
+
@lock ||= Mutex.new
|
91
|
+
end
|
92
|
+
|
83
93
|
def relationships
|
84
94
|
@relationships ||= []
|
85
95
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module FunWithJsonApi
|
2
|
+
module Exceptions
|
3
|
+
# Indicates a Resource or Collection item not authorized
|
4
|
+
class UnauthorisedResource < FunWithJsonApi::Exception
|
5
|
+
def initialize(message, payload = ExceptionPayload.new)
|
6
|
+
payload = Array.wrap(payload).each do |unauthorized|
|
7
|
+
unauthorized.code ||= 'unauthorized_resource'
|
8
|
+
unauthorized.title ||=
|
9
|
+
I18n.t('unauthorized_resource', scope: 'fun_with_json_api.exceptions')
|
10
|
+
unauthorized.status ||= '403'
|
11
|
+
end
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'fun_with_json_api/schema_validators/check_collection_is_authorized'
|
2
|
+
require 'fun_with_json_api/schema_validators/check_collection_has_all_members'
|
3
|
+
|
1
4
|
module FunWithJsonApi
|
2
5
|
class FindCollectionFromDocument
|
3
6
|
def self.find(*args)
|
@@ -27,6 +30,7 @@ module FunWithJsonApi
|
|
27
30
|
# Load resource from id value
|
28
31
|
deserializer.load_collection_from_id_values(document_ids).tap do |collection|
|
29
32
|
check_collection_contains_all_requested_resources!(collection)
|
33
|
+
check_collection_is_authorised!(collection)
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|
@@ -49,10 +53,11 @@ module FunWithJsonApi
|
|
49
53
|
private
|
50
54
|
|
51
55
|
def check_collection_contains_all_requested_resources!(collection)
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
+
SchemaValidators::CheckCollectionHasAllMembers.call(collection, document_ids, deserializer)
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_collection_is_authorised!(collection)
|
60
|
+
SchemaValidators::CheckCollectionIsAuthorised.call(collection, document_ids, deserializer)
|
56
61
|
end
|
57
62
|
|
58
63
|
def check_document_types_match_deserializer!
|
@@ -83,26 +88,6 @@ module FunWithJsonApi
|
|
83
88
|
Exceptions::InvalidDocumentType.new(message, payload)
|
84
89
|
end
|
85
90
|
|
86
|
-
def build_missing_resources_error(collection_ids)
|
87
|
-
payload = document_ids.each_with_index.map do |resource_id, index|
|
88
|
-
build_missing_resource_payload(collection_ids, resource_id, index)
|
89
|
-
end.reject(&:nil?)
|
90
|
-
|
91
|
-
missing_values = document_ids.reject { |value| collection_ids.include?(value.to_s) }
|
92
|
-
message = "Couldn't find #{resource_class} items with "\
|
93
|
-
"#{id_param} in #{missing_values.inspect}"
|
94
|
-
Exceptions::MissingResource.new(message, payload)
|
95
|
-
end
|
96
|
-
|
97
|
-
def build_missing_resource_payload(collection_ids, resource_id, index)
|
98
|
-
unless collection_ids.include?(resource_id)
|
99
|
-
ExceptionPayload.new(
|
100
|
-
pointer: "/data/#{index}/id",
|
101
|
-
detail: missing_resource_message(resource_id)
|
102
|
-
)
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
91
|
def document_type_does_not_match_endpoint_message(type)
|
107
92
|
I18n.t(
|
108
93
|
:invalid_document_type,
|
@@ -111,14 +96,5 @@ module FunWithJsonApi
|
|
111
96
|
scope: 'fun_with_json_api.find_collection_from_document'
|
112
97
|
)
|
113
98
|
end
|
114
|
-
|
115
|
-
def missing_resource_message(resource_id)
|
116
|
-
I18n.t(
|
117
|
-
:missing_resource,
|
118
|
-
resource: resource_type,
|
119
|
-
resource_id: resource_id,
|
120
|
-
scope: 'fun_with_json_api.find_collection_from_document'
|
121
|
-
)
|
122
|
-
end
|
123
99
|
end
|
124
100
|
end
|
@@ -24,9 +24,7 @@ module FunWithJsonApi
|
|
24
24
|
raise build_invalid_document_type_error unless document_matches_resource_type?
|
25
25
|
|
26
26
|
# Load resource from id value
|
27
|
-
|
28
|
-
raise build_missing_resource_error if resource.nil?
|
29
|
-
end
|
27
|
+
load_resource_and_check!
|
30
28
|
end
|
31
29
|
|
32
30
|
def document_id
|
@@ -57,6 +55,15 @@ module FunWithJsonApi
|
|
57
55
|
|
58
56
|
private
|
59
57
|
|
58
|
+
def load_resource_and_check!
|
59
|
+
deserializer.load_resource_from_id_value(document_id).tap do |resource|
|
60
|
+
raise build_missing_resource_error if resource.nil?
|
61
|
+
FunWithJsonApi::SchemaValidators::CheckResourceIsAuthorised.call(
|
62
|
+
resource, document_id, deserializer
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
60
67
|
def build_invalid_document_error
|
61
68
|
payload = ExceptionPayload.new
|
62
69
|
payload.pointer = '/data'
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'fun_with_json_api/exception'
|
2
|
+
|
3
|
+
module FunWithJsonApi
|
4
|
+
module SchemaValidators
|
5
|
+
class CheckCollectionHasAllMembers
|
6
|
+
def self.call(*args)
|
7
|
+
new(*args).call
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :collection
|
11
|
+
attr_reader :document_ids
|
12
|
+
attr_reader :deserializer
|
13
|
+
attr_reader :prefix
|
14
|
+
|
15
|
+
delegate :id_param, :resource_class, to: :deserializer
|
16
|
+
|
17
|
+
def initialize(collection, document_ids, deserializer, prefix: '/data')
|
18
|
+
@collection = collection
|
19
|
+
@document_ids = document_ids
|
20
|
+
@deserializer = deserializer
|
21
|
+
@prefix = prefix
|
22
|
+
end
|
23
|
+
|
24
|
+
def call
|
25
|
+
if collection.size != document_ids.size
|
26
|
+
collection_ids = deserializer.format_collection_ids(collection)
|
27
|
+
raise build_missing_resources_error(collection_ids)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def resource_type
|
32
|
+
deserializer.type
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def build_missing_resources_error(collection_ids)
|
38
|
+
payload = document_ids.each_with_index.map do |resource_id, index|
|
39
|
+
build_missing_resource_payload(collection_ids, resource_id, index)
|
40
|
+
end.reject(&:nil?)
|
41
|
+
|
42
|
+
missing_values = document_ids.reject { |value| collection_ids.include?(value.to_s) }
|
43
|
+
message = "Couldn't find #{resource_class} items with "\
|
44
|
+
"#{id_param} in #{missing_values.inspect}"
|
45
|
+
Exceptions::MissingResource.new(message, payload)
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_missing_resource_payload(collection_ids, resource_id, index)
|
49
|
+
unless collection_ids.include?(resource_id)
|
50
|
+
ExceptionPayload.new(
|
51
|
+
pointer: "#{prefix}/#{index}/id",
|
52
|
+
detail: missing_resource_message(resource_id)
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def missing_resource_message(resource_id)
|
58
|
+
I18n.t(
|
59
|
+
:missing_resource,
|
60
|
+
resource: resource_type,
|
61
|
+
resource_id: resource_id,
|
62
|
+
scope: 'fun_with_json_api.find_collection_from_document'
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'fun_with_json_api/exception'
|
2
|
+
|
3
|
+
module FunWithJsonApi
|
4
|
+
module SchemaValidators
|
5
|
+
class CheckCollectionIsAuthorised
|
6
|
+
def self.call(*args)
|
7
|
+
new(*args).call
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :collection
|
11
|
+
attr_reader :collection_ids
|
12
|
+
attr_reader :deserializer
|
13
|
+
attr_reader :prefix
|
14
|
+
|
15
|
+
delegate :resource_class,
|
16
|
+
to: :deserializer
|
17
|
+
|
18
|
+
def initialize(collection, collection_ids, deserializer, prefix: '/data')
|
19
|
+
@collection = collection
|
20
|
+
@collection_ids = collection_ids
|
21
|
+
@deserializer = deserializer
|
22
|
+
@prefix = prefix
|
23
|
+
end
|
24
|
+
|
25
|
+
def call
|
26
|
+
payload = collection.each_with_index.map do |resource, index|
|
27
|
+
build_unauthorized_resource_payload(resource, index)
|
28
|
+
end.reject(&:nil?)
|
29
|
+
|
30
|
+
return if payload.empty?
|
31
|
+
|
32
|
+
raise Exceptions::UnauthorisedResource.new(
|
33
|
+
"resource_authorizer method for one or more '#{deserializer.type}' items returned false",
|
34
|
+
payload
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def resource_type
|
39
|
+
deserializer.type
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def build_unauthorized_resource_payload(resource, index)
|
45
|
+
unless deserializer.resource_authorizer.call(resource)
|
46
|
+
ExceptionPayload.new(
|
47
|
+
pointer: "#{prefix}/#{index}/id",
|
48
|
+
detail: unauthorized_resource_message(collection_ids[index])
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def unauthorized_resource_message(resource_id)
|
54
|
+
I18n.t(
|
55
|
+
:unauthorized_resource,
|
56
|
+
resource: resource_type,
|
57
|
+
resource_id: resource_id,
|
58
|
+
scope: 'fun_with_json_api.find_collection_from_document'
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|