fun_with_json_api 0.0.5 → 0.0.6.pre.alpha.1

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: 1fac056055f69ea77d7b0b856c4902c84eb3ecec
4
- data.tar.gz: 3642e68cfa8a87c5aef220af9a11ad9064a58802
3
+ metadata.gz: aa43a36433dec9d40e3a4afaad17becacfad028f
4
+ data.tar.gz: 468d6bda13144854fa1771a0e113e3d906a223f2
5
5
  SHA512:
6
- metadata.gz: c8524bd26b6d0caa63b5ea4d5345e5e7ae9dd3a0f19a4cf51435e6332bf56886719c19dc2d45d9e84466567924990298caa40b33de2c0dfcbf186304a0003193
7
- data.tar.gz: 6b021fc4bdaddabdc0b2d0df83032118fd50c092b39a2be541616654673732692351d59666bfbdaebeae7b6d25333df21aec6e4ca2d3149eeda771f5dfdc8b92
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
- return resource.id if resource
32
+ raise build_missing_relationship_error(id_value) if resource.nil?
33
33
 
34
- raise build_missing_relationship_error(id_value)
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
- expected_size = values.size
33
- result_size = collection.size
34
- if result_size != expected_size
35
- raise build_missing_relationship_error_from_collection(collection, values)
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
- Attribute.create(name, options).tap do |attribute|
29
- add_parse_attribute_method(attribute)
30
- attributes << attribute
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
- Attributes::Relationship.create(
42
- name,
43
- deserializer_class_or_callable,
44
- options
45
- ).tap do |relationship|
46
- add_parse_resource_method(relationship)
47
- relationships << relationship
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
- Attributes::RelationshipCollection.create(
55
- name,
56
- deserializer_class_or_callable,
57
- options
58
- ).tap do |relationship|
59
- add_parse_resource_method(relationship)
60
- relationships << relationship
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
- if collection.size != document_ids.size
53
- collection_ids = deserializer.format_collection_ids(collection)
54
- raise build_missing_resources_error(collection_ids)
55
- end
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
- deserializer.load_resource_from_id_value(document_id).tap do |resource|
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