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 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