hoodoo 1.0.2

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.
Files changed (216) hide show
  1. checksums.yaml +7 -0
  2. data/bin/hoodoo +5 -0
  3. data/lib/hoodoo.rb +27 -0
  4. data/lib/hoodoo/active.rb +32 -0
  5. data/lib/hoodoo/active/active_model/uuid_validator.rb +45 -0
  6. data/lib/hoodoo/active/active_record/base.rb +81 -0
  7. data/lib/hoodoo/active/active_record/creator.rb +134 -0
  8. data/lib/hoodoo/active/active_record/dated.rb +343 -0
  9. data/lib/hoodoo/active/active_record/error_mapping.rb +351 -0
  10. data/lib/hoodoo/active/active_record/finder.rb +606 -0
  11. data/lib/hoodoo/active/active_record/search_helper.rb +189 -0
  12. data/lib/hoodoo/active/active_record/secure.rb +431 -0
  13. data/lib/hoodoo/active/active_record/support.rb +106 -0
  14. data/lib/hoodoo/active/active_record/translated.rb +87 -0
  15. data/lib/hoodoo/active/active_record/uuid.rb +80 -0
  16. data/lib/hoodoo/active/active_record/writer.rb +321 -0
  17. data/lib/hoodoo/client.rb +23 -0
  18. data/lib/hoodoo/client/augmented_array.rb +29 -0
  19. data/lib/hoodoo/client/augmented_base.rb +168 -0
  20. data/lib/hoodoo/client/augmented_hash.rb +23 -0
  21. data/lib/hoodoo/client/client.rb +354 -0
  22. data/lib/hoodoo/client/endpoint/endpoint.rb +427 -0
  23. data/lib/hoodoo/client/endpoint/endpoints/amqp.rb +180 -0
  24. data/lib/hoodoo/client/endpoint/endpoints/auto_session.rb +194 -0
  25. data/lib/hoodoo/client/endpoint/endpoints/http.rb +203 -0
  26. data/lib/hoodoo/client/endpoint/endpoints/http_based.rb +367 -0
  27. data/lib/hoodoo/client/endpoint/endpoints/not_found.rb +59 -0
  28. data/lib/hoodoo/client/headers.rb +269 -0
  29. data/lib/hoodoo/communicators.rb +23 -0
  30. data/lib/hoodoo/communicators/fast.rb +44 -0
  31. data/lib/hoodoo/communicators/pool.rb +601 -0
  32. data/lib/hoodoo/communicators/slow.rb +84 -0
  33. data/lib/hoodoo/data.rb +51 -0
  34. data/lib/hoodoo/data/resources/caller.rb +39 -0
  35. data/lib/hoodoo/data/resources/errors.rb +28 -0
  36. data/lib/hoodoo/data/resources/log.rb +31 -0
  37. data/lib/hoodoo/data/resources/session.rb +26 -0
  38. data/lib/hoodoo/data/types/error_primitive.rb +27 -0
  39. data/lib/hoodoo/data/types/permissions.rb +40 -0
  40. data/lib/hoodoo/data/types/permissions_defaults.rb +32 -0
  41. data/lib/hoodoo/data/types/permissions_full.rb +28 -0
  42. data/lib/hoodoo/data/types/permissions_resources.rb +31 -0
  43. data/lib/hoodoo/discovery.rb +20 -0
  44. data/lib/hoodoo/errors.rb +19 -0
  45. data/lib/hoodoo/errors/error_descriptions.rb +229 -0
  46. data/lib/hoodoo/errors/errors.rb +322 -0
  47. data/lib/hoodoo/generator.rb +139 -0
  48. data/lib/hoodoo/logger.rb +23 -0
  49. data/lib/hoodoo/logger/fast_writer.rb +27 -0
  50. data/lib/hoodoo/logger/flattener_mixin.rb +36 -0
  51. data/lib/hoodoo/logger/logger.rb +387 -0
  52. data/lib/hoodoo/logger/slow_writer.rb +49 -0
  53. data/lib/hoodoo/logger/writer_mixin.rb +52 -0
  54. data/lib/hoodoo/logger/writers/file_writer.rb +45 -0
  55. data/lib/hoodoo/logger/writers/log_entries_dot_com_writer.rb +64 -0
  56. data/lib/hoodoo/logger/writers/stream_writer.rb +43 -0
  57. data/lib/hoodoo/middleware.rb +33 -0
  58. data/lib/hoodoo/presenters.rb +45 -0
  59. data/lib/hoodoo/presenters/base.rb +281 -0
  60. data/lib/hoodoo/presenters/base_dsl.rb +519 -0
  61. data/lib/hoodoo/presenters/common_resource_fields.rb +31 -0
  62. data/lib/hoodoo/presenters/embedding.rb +232 -0
  63. data/lib/hoodoo/presenters/types/array.rb +118 -0
  64. data/lib/hoodoo/presenters/types/boolean.rb +26 -0
  65. data/lib/hoodoo/presenters/types/date.rb +26 -0
  66. data/lib/hoodoo/presenters/types/date_time.rb +26 -0
  67. data/lib/hoodoo/presenters/types/decimal.rb +47 -0
  68. data/lib/hoodoo/presenters/types/enum.rb +55 -0
  69. data/lib/hoodoo/presenters/types/field.rb +158 -0
  70. data/lib/hoodoo/presenters/types/float.rb +26 -0
  71. data/lib/hoodoo/presenters/types/hash.rb +361 -0
  72. data/lib/hoodoo/presenters/types/integer.rb +26 -0
  73. data/lib/hoodoo/presenters/types/object.rb +117 -0
  74. data/lib/hoodoo/presenters/types/string.rb +53 -0
  75. data/lib/hoodoo/presenters/types/tags.rb +24 -0
  76. data/lib/hoodoo/presenters/types/text.rb +26 -0
  77. data/lib/hoodoo/presenters/types/uuid.rb +54 -0
  78. data/lib/hoodoo/services.rb +34 -0
  79. data/lib/hoodoo/services/discovery/discoverers/by_consul.rb +66 -0
  80. data/lib/hoodoo/services/discovery/discoverers/by_convention.rb +173 -0
  81. data/lib/hoodoo/services/discovery/discoverers/by_drb/by_drb.rb +195 -0
  82. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server.rb +166 -0
  83. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server_start.rb +37 -0
  84. data/lib/hoodoo/services/discovery/discovery.rb +186 -0
  85. data/lib/hoodoo/services/discovery/results/for_amqp.rb +58 -0
  86. data/lib/hoodoo/services/discovery/results/for_http.rb +85 -0
  87. data/lib/hoodoo/services/discovery/results/for_local.rb +85 -0
  88. data/lib/hoodoo/services/discovery/results/for_remote.rb +57 -0
  89. data/lib/hoodoo/services/middleware/amqp_log_message.rb +186 -0
  90. data/lib/hoodoo/services/middleware/amqp_log_writer.rb +119 -0
  91. data/lib/hoodoo/services/middleware/endpoints/inter_resource_local.rb +130 -0
  92. data/lib/hoodoo/services/middleware/endpoints/inter_resource_remote.rb +202 -0
  93. data/lib/hoodoo/services/middleware/exception_reporting/base_reporter.rb +105 -0
  94. data/lib/hoodoo/services/middleware/exception_reporting/exception_reporting.rb +115 -0
  95. data/lib/hoodoo/services/middleware/exception_reporting/reporters/airbrake_reporter.rb +64 -0
  96. data/lib/hoodoo/services/middleware/exception_reporting/reporters/raygun_reporter.rb +63 -0
  97. data/lib/hoodoo/services/middleware/interaction.rb +127 -0
  98. data/lib/hoodoo/services/middleware/middleware.rb +2705 -0
  99. data/lib/hoodoo/services/middleware/rack_monkey_patch.rb +73 -0
  100. data/lib/hoodoo/services/services/context.rb +153 -0
  101. data/lib/hoodoo/services/services/implementation.rb +132 -0
  102. data/lib/hoodoo/services/services/interface.rb +934 -0
  103. data/lib/hoodoo/services/services/permissions.rb +250 -0
  104. data/lib/hoodoo/services/services/request.rb +189 -0
  105. data/lib/hoodoo/services/services/response.rb +316 -0
  106. data/lib/hoodoo/services/services/service.rb +141 -0
  107. data/lib/hoodoo/services/services/session.rb +729 -0
  108. data/lib/hoodoo/utilities.rb +12 -0
  109. data/lib/hoodoo/utilities/string_inquirer.rb +54 -0
  110. data/lib/hoodoo/utilities/utilities.rb +380 -0
  111. data/lib/hoodoo/utilities/uuid.rb +44 -0
  112. data/lib/hoodoo/version.rb +17 -0
  113. data/spec/active/active_record/base_spec.rb +57 -0
  114. data/spec/active/active_record/creator_spec.rb +88 -0
  115. data/spec/active/active_record/dated_spec.rb +248 -0
  116. data/spec/active/active_record/error_mapping_spec.rb +360 -0
  117. data/spec/active/active_record/finder_spec.rb +744 -0
  118. data/spec/active/active_record/search_helper_spec.rb +384 -0
  119. data/spec/active/active_record/secure_spec.rb +435 -0
  120. data/spec/active/active_record/support_spec.rb +225 -0
  121. data/spec/active/active_record/translated_spec.rb +19 -0
  122. data/spec/active/active_record/uuid_spec.rb +72 -0
  123. data/spec/active/active_record/writer_spec.rb +272 -0
  124. data/spec/alchemy/alchemy-amq.rb +33 -0
  125. data/spec/client/augmented_array_spec.rb +15 -0
  126. data/spec/client/augmented_base_spec.rb +50 -0
  127. data/spec/client/augmented_hash_spec.rb +15 -0
  128. data/spec/client/client_spec.rb +955 -0
  129. data/spec/client/endpoint/endpoint_spec.rb +70 -0
  130. data/spec/client/endpoint/endpoints/amqp_spec.rb +16 -0
  131. data/spec/client/endpoint/endpoints/auto_session_spec.rb +9 -0
  132. data/spec/client/endpoint/endpoints/http_based_spec.rb +9 -0
  133. data/spec/client/endpoint/endpoints/http_spec.rb +103 -0
  134. data/spec/client/endpoint/endpoints/not_found_spec.rb +35 -0
  135. data/spec/client/headers_spec.rb +172 -0
  136. data/spec/communicators/fast_spec.rb +9 -0
  137. data/spec/communicators/pool_spec.rb +339 -0
  138. data/spec/communicators/slow_spec.rb +15 -0
  139. data/spec/data/resources/caller_spec.rb +156 -0
  140. data/spec/data/resources/errors_spec.rb +22 -0
  141. data/spec/data/resources/log_spec.rb +20 -0
  142. data/spec/data/resources/session_spec.rb +15 -0
  143. data/spec/data/types/error_primitive_spec.rb +15 -0
  144. data/spec/data/types/permissions_defaults_spec.rb +25 -0
  145. data/spec/data/types/permissions_full_spec.rb +44 -0
  146. data/spec/data/types/permissions_resources_spec.rb +34 -0
  147. data/spec/data/types/permissions_spec.rb +37 -0
  148. data/spec/errors/error_descriptions_spec.rb +98 -0
  149. data/spec/errors/errors_spec.rb +346 -0
  150. data/spec/integration/service_actions_spec.rb +112 -0
  151. data/spec/logger/fast_writer_spec.rb +18 -0
  152. data/spec/logger/logger_spec.rb +259 -0
  153. data/spec/logger/slow_writer_spec.rb +144 -0
  154. data/spec/logger/writers/file_writer_spec.rb +37 -0
  155. data/spec/logger/writers/log_entries_dot_com_writer_spec.rb +29 -0
  156. data/spec/logger/writers/stream_writer_spec.rb +38 -0
  157. data/spec/presenters/base_dsl_spec.rb +111 -0
  158. data/spec/presenters/base_spec.rb +871 -0
  159. data/spec/presenters/common_resource_fields_spec.rb +30 -0
  160. data/spec/presenters/embedding_spec.rb +87 -0
  161. data/spec/presenters/types/array_spec.rb +249 -0
  162. data/spec/presenters/types/boolean_spec.rb +51 -0
  163. data/spec/presenters/types/date_spec.rb +57 -0
  164. data/spec/presenters/types/date_time_spec.rb +59 -0
  165. data/spec/presenters/types/decimal_spec.rb +58 -0
  166. data/spec/presenters/types/enum_spec.rb +71 -0
  167. data/spec/presenters/types/field_spec.rb +77 -0
  168. data/spec/presenters/types/float_spec.rb +50 -0
  169. data/spec/presenters/types/hash_spec.rb +1069 -0
  170. data/spec/presenters/types/integer_spec.rb +50 -0
  171. data/spec/presenters/types/object_spec.rb +177 -0
  172. data/spec/presenters/types/string_spec.rb +65 -0
  173. data/spec/presenters/types/tags_spec.rb +56 -0
  174. data/spec/presenters/types/text_spec.rb +50 -0
  175. data/spec/presenters/types/uuid_spec.rb +46 -0
  176. data/spec/presenters/walk_spec.rb +198 -0
  177. data/spec/services/discovery/discoverers/by_consul_spec.rb +29 -0
  178. data/spec/services/discovery/discoverers/by_convention_spec.rb +67 -0
  179. data/spec/services/discovery/discoverers/by_drb/by_drb_spec.rb +80 -0
  180. data/spec/services/discovery/discoverers/by_drb/drb_server_spec.rb +205 -0
  181. data/spec/services/discovery/discovery_spec.rb +73 -0
  182. data/spec/services/discovery/results/for_amqp_spec.rb +17 -0
  183. data/spec/services/discovery/results/for_http_spec.rb +37 -0
  184. data/spec/services/discovery/results/for_local_spec.rb +21 -0
  185. data/spec/services/discovery/results/for_remote_spec.rb +15 -0
  186. data/spec/services/middleware/amqp_log_message_spec.rb +60 -0
  187. data/spec/services/middleware/amqp_log_writer_spec.rb +95 -0
  188. data/spec/services/middleware/endpoints/inter_resource_local_spec.rb +9 -0
  189. data/spec/services/middleware/endpoints/inter_resource_remote_spec.rb +9 -0
  190. data/spec/services/middleware/exception_reporting/base_reporter_spec.rb +16 -0
  191. data/spec/services/middleware/exception_reporting/exception_reporting_spec.rb +92 -0
  192. data/spec/services/middleware/exception_reporting/reporters/airbrake_reporter_spec.rb +24 -0
  193. data/spec/services/middleware/exception_reporting/reporters/raygun_reporter_spec.rb +23 -0
  194. data/spec/services/middleware/middleware_cors_spec.rb +93 -0
  195. data/spec/services/middleware/middleware_create_update_spec.rb +489 -0
  196. data/spec/services/middleware/middleware_dated_at_spec.rb +186 -0
  197. data/spec/services/middleware/middleware_exotic_communication_spec.rb +560 -0
  198. data/spec/services/middleware/middleware_logging_spec.rb +356 -0
  199. data/spec/services/middleware/middleware_multi_local_spec.rb +1094 -0
  200. data/spec/services/middleware/middleware_multi_remote_spec.rb +1440 -0
  201. data/spec/services/middleware/middleware_permissions_spec.rb +1014 -0
  202. data/spec/services/middleware/middleware_public_spec.rb +238 -0
  203. data/spec/services/middleware/middleware_spec.rb +1569 -0
  204. data/spec/services/middleware/string_inquirer_spec.rb +30 -0
  205. data/spec/services/services/application_spec.rb +74 -0
  206. data/spec/services/services/context_spec.rb +48 -0
  207. data/spec/services/services/implementation_spec.rb +45 -0
  208. data/spec/services/services/interface_spec.rb +262 -0
  209. data/spec/services/services/permissions_spec.rb +249 -0
  210. data/spec/services/services/request_spec.rb +95 -0
  211. data/spec/services/services/response_spec.rb +250 -0
  212. data/spec/services/services/session_spec.rb +432 -0
  213. data/spec/spec_helper.rb +298 -0
  214. data/spec/utilities/utilities_spec.rb +537 -0
  215. data/spec/utilities/uuid_spec.rb +20 -0
  216. metadata +615 -0
@@ -0,0 +1,189 @@
1
+ ########################################################################
2
+ # File:: search_helper.rb
3
+ # (C):: Loyalty New Zealand 2015
4
+ #
5
+ # Purpose:: Supplementary helper class included by "finder.rb". See
6
+ # Hoodoo::ActiveRecord::Finder, especially
7
+ # Hoodoo::ActiveRecord::Finder#search_with, for details.
8
+ # ----------------------------------------------------------------------
9
+ # 09-Jul-2015 (ADH): Created.
10
+ ########################################################################
11
+
12
+ module Hoodoo
13
+ module ActiveRecord
14
+ module Finder
15
+
16
+ # Help build up Hash maps to pass into Hoodoo::ActiveRecord::Finder
17
+ # methods Hoodoo::ActiveRecord::Finder#search_with and
18
+ # Hoodoo::ActiveRecord::Finder#filter_with.
19
+ #
20
+ # The usage pattern is as follows, using "sh" as a local variable
21
+ # just for brevity - it isn't required:
22
+ #
23
+ # sh = Hoodoo::ActiveRecord::Finder::SearchHelper
24
+ #
25
+ # class SomeModel < ActiveRecord::Base
26
+ # search_with(
27
+ # :colour => sh.cs_match_generic,
28
+ # :name => sh.ci_match_generic,
29
+ # :resource_ids => sh.cs_match_csv( :associated_id )
30
+ # end
31
+ # end
32
+ #
33
+ # The helper methods just provide values to pass into the Hash used
34
+ # with the search/fitler Hoodoo::ActiveRecord::Finder methods, so
35
+ # they're optional and compatible with calls that write it out "by
36
+ # hand".
37
+ #
38
+ # In all cases, in normal use the generated SQL _will not_ match +null+
39
+ # values; if negated for a filter ("<tt>where.not</tt>"), the generated
40
+ # SQL _will_ match +null+ values. As a result, passing in explicit
41
+ # searches for +nil+ won't work - but that's never expected as a use
42
+ # case here since search values are coming in via e.g. query
43
+ # string information from a URI.
44
+ #
45
+ class SearchHelper
46
+
47
+ # Case-sensitive match (default-style matching). *WARNING:* This
48
+ # will be case sensitive only if your database is configured for
49
+ # case sensitive matching by default.
50
+ #
51
+ # Results in a <tt>foo = bar</tt> query.
52
+ #
53
+ # +model_field_name+:: If the model attribute name differs from the
54
+ # search key you want to use in the URI, give
55
+ # the model attribute name here, else omit.
56
+ #
57
+ # Returns a value that can be asssigned to a URI query string key in
58
+ # the Hash given to Hoodoo::ActiveRecord::Finder#search_with or
59
+ # Hoodoo::ActiveRecord::Finder#filter_with.
60
+ #
61
+ def self.cs_match( model_field_name = nil )
62
+ Proc.new { | attr, value |
63
+ column = model_field_name || attr
64
+
65
+ [ "#{ column } = ? AND #{ column } IS NOT NULL", value ]
66
+ }
67
+ end
68
+
69
+ # Case-sensitive match of a series of values separated by commas,
70
+ # which are split into an array then processed by AREL back to
71
+ # something SQL-safe.
72
+ #
73
+ # Results in a <tt>foo IN bar,baz,boo</tt> query.
74
+ #
75
+ # +model_field_name+:: If the model attribute name differs from the
76
+ # search key you want to use in the URI, give
77
+ # the model attribute name here, else omit.
78
+ #
79
+ # Returns a value that can be asssigned to a URI query string key in
80
+ # the Hash given to Hoodoo::ActiveRecord::Finder#search_with or
81
+ # Hoodoo::ActiveRecord::Finder#filter_with.
82
+ #
83
+ def self.cs_match_csv( model_field_name = nil )
84
+ Proc.new { | attr, value |
85
+ column = model_field_name || attr
86
+ value = value.split( ',' )
87
+
88
+ [ "#{ column } IN (?) AND #{ column } IS NOT NULL", value ]
89
+ }
90
+ end
91
+
92
+ # Case-sensitive match of a series of values given as an Array.
93
+ # Normally, query string information comes in as a String so the
94
+ # use cases for this are quite unusual; you probably want to use
95
+ # #cs_match_csv most of the time.
96
+ #
97
+ # Results in a <tt>foo IN bar,baz,boo</tt> query.
98
+ #
99
+ # +model_field_name+:: If the model attribute name differs from the
100
+ # search key you want to use in the URI, give
101
+ # the model attribute name here, else omit.
102
+ #
103
+ # Returns a value that can be asssigned to a URI query string key in
104
+ # the Hash given to Hoodoo::ActiveRecord::Finder#search_with or
105
+ # Hoodoo::ActiveRecord::Finder#filter_with.
106
+ #
107
+ def self.cs_match_array( model_field_name = nil )
108
+ Proc.new { | attr, value |
109
+ column = model_field_name || attr
110
+ value = [ value ].flatten
111
+
112
+ [ "#{ column } IN (?) AND #{ column } IS NOT NULL", value ]
113
+ }
114
+ end
115
+
116
+ # Case-insensitive match which should be fairly database independent
117
+ # but will run relatively slowly as a result. If you are using
118
+ # PostgreSQL, consider using the faster #ci_match_postgres method
119
+ # instead.
120
+ #
121
+ # Results in a <tt>lower(foo) = bar</tt> query with +bar+ coerced to
122
+ # a String and converted to lower case by Ruby first.
123
+ #
124
+ # +model_field_name+:: If the model attribute name differs from the
125
+ # search key you want to use in the URI, give
126
+ # the model attribute name here, else omit.
127
+ #
128
+ # Returns a value that can be asssigned to a URI query string key in
129
+ # the Hash given to Hoodoo::ActiveRecord::Finder#search_with or
130
+ # Hoodoo::ActiveRecord::Finder#filter_with.
131
+ #
132
+ def self.ci_match_generic( model_field_name = nil )
133
+ Proc.new { | attr, value |
134
+ column = model_field_name || attr
135
+ value = ( value || '' ).to_s.downcase
136
+
137
+ [ "lower(#{ column }) = ? AND #{ column } IS NOT NULL", value ]
138
+ }
139
+ end
140
+
141
+ # As #ci_match_generic, but adds wildcards at the front and end of
142
+ # the string for a case-insensitive-all-wildcard match.
143
+ #
144
+ def self.ciaw_match_generic( model_field_name = nil )
145
+ Proc.new { | attr, value |
146
+ column = model_field_name || attr
147
+ value = ( value || '' ).to_s.downcase
148
+
149
+ [ "lower(#{ column }) LIKE ? AND #{ column } IS NOT NULL", "%#{ value }%" ]
150
+ }
151
+ end
152
+
153
+ # Case-insensitive match which requires PostgreSQL but should run
154
+ # quickly. If you need a database agnostic solution, consider using
155
+ # the slower #ci_match_generic method instead.
156
+ #
157
+ # Results in a <tt>foo ILIKE bar</tt> query.
158
+ #
159
+ # +model_field_name+:: If the model attribute name differs from the
160
+ # search key you want to use in the URI, give
161
+ # the model attribute name here, else omit.
162
+ #
163
+ # Returns a value that can be asssigned to a URI query string key in
164
+ # the Hash given to Hoodoo::ActiveRecord::Finder#search_with or
165
+ # Hoodoo::ActiveRecord::Finder#filter_with.
166
+ #
167
+ def self.ci_match_postgres( model_field_name = nil )
168
+ Proc.new { | attr, value |
169
+ column = model_field_name || attr
170
+
171
+ [ "#{ column } ILIKE ? AND #{ column } IS NOT NULL", value ]
172
+ }
173
+ end
174
+
175
+ # As #ci_match_postgres, but adds wildcards at the front and end of
176
+ # the string for a case-insensitive-all-wildcard match.
177
+ #
178
+ def self.ciaw_match_postgres( model_field_name = nil )
179
+ Proc.new { | attr, value |
180
+ column = model_field_name || attr
181
+
182
+ [ "#{ column } ILIKE ? AND #{ column } IS NOT NULL", "%#{ value }%" ]
183
+ }
184
+ end
185
+ end
186
+
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,431 @@
1
+ ########################################################################
2
+ # File:: secure.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Support mixin for models subclassed from ActiveRecord::Base
6
+ # providing enhanced find mechanisms for +show+ and +list+
7
+ # action handling.
8
+ # ----------------------------------------------------------------------
9
+ # 25-Nov-2014 (ADH): Created.
10
+ ########################################################################
11
+
12
+ module Hoodoo
13
+ module ActiveRecord
14
+
15
+ # Support mixin for models subclassed from ActiveRecord::Base providing
16
+ # a core out-of-box Hoodoo data access security model. See
17
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure for details.
18
+ #
19
+ # See also:
20
+ #
21
+ # * http://guides.rubyonrails.org/active_record_basics.html
22
+ #
23
+ module Secure
24
+
25
+ # Instantiates this module when it is included:
26
+ #
27
+ # Example:
28
+ #
29
+ # class SomeModel < ActiveRecord::Base
30
+ # include Hoodoo::ActiveRecord::Secure
31
+ # # ...
32
+ # end
33
+ #
34
+ # +model+:: The ActiveRecord::Base descendant that is including
35
+ # this module.
36
+ #
37
+ def self.included( model )
38
+ model.class_attribute(
39
+ :nz_co_loyalty_hoodoo_secure_with,
40
+ {
41
+ :instance_predicate => false,
42
+ :instance_accessor => false
43
+ }
44
+ )
45
+
46
+ instantiate( model ) unless model == Hoodoo::ActiveRecord::Base
47
+ super( model )
48
+ end
49
+
50
+ # When instantiated in an ActiveRecord::Base subclass, all of the
51
+ # Hoodoo::ActiveRecord::Secure::ClassMethods methods are defined as
52
+ # class methods on the including class.
53
+ #
54
+ # +model+:: The ActiveRecord::Base descendant that is including
55
+ # this module.
56
+ #
57
+ def self.instantiate( model )
58
+ model.extend( ClassMethods )
59
+ end
60
+
61
+ # Collection of class methods that get defined on an including class via
62
+ # Hoodoo::ActiveRecord::Secure::included.
63
+ #
64
+ module ClassMethods
65
+
66
+ # Internal.
67
+ #
68
+ # See #secure for details - this is the Proc used by default if no
69
+ # alternative argument generator is given in the longhand form's
70
+ # value Hash's +:using+ key.
71
+ #
72
+ DEFAULT_SECURE_PROC = Proc.new { | model_class, database_column_name, session_field_value | [ { database_column_name => session_field_value } ] }
73
+
74
+ # The core of out-of-the-box Hoodoo data access security layer.
75
+ #
76
+ # Parameters:
77
+ #
78
+ # +context+:: Hoodoo::Services::Context instance describing a call
79
+ # context. This is typically a value passed to one of
80
+ # the Hoodoo::Services::Implementation instance methods
81
+ # that a resource subclass implements.
82
+ #
83
+ # == Overview
84
+ #
85
+ # In most non-trivial systems, people calling into the system under
86
+ # a Session will have limited access to resource records. Often the
87
+ # broad pattern is: Someone can only see what they create. Maybe
88
+ # there's a superuser-like monitoring concept of someone who can
89
+ # see what everyone creates... In any event, there needs to be some
90
+ # kind of support for this.
91
+ #
92
+ # In the Hoodoo generic case, it's tackled at several levels.
93
+ #
94
+ # * A Caller object can describe fields that are identify who that
95
+ # Caller is (which may be as simple as the Caller instance's
96
+ # resource UUID, or may include additional concepts specific to
97
+ # the API being designed/implemented).
98
+ #
99
+ # * A Session instance is bound to a particular Caller. Someone
100
+ # calling the API creates a Session using a caller ID and secret,
101
+ # and gains whatever access permissions and data privileges it
102
+ # describes.
103
+ #
104
+ # * Custom implementations of a Session resource and Caller resource
105
+ # endpoint might add in other identifying fields to the session
106
+ # payload too. That's what the Session's +identity+ section is
107
+ # for. See Hoodoo::Services::Session#identity.
108
+ #
109
+ # * When resource endpoint implementations create data, they have
110
+ # an opportunity to use a database field to record (say) the
111
+ # caller UUID and/or some other session value(s) in indexed table
112
+ # columns along the lines of "creating_caller_uuid", or similar.
113
+ # This way, the "who made me" information is preserved.
114
+ #
115
+ # * When resource endpoints read back any data from the database
116
+ # (for show, list, update or delete actions) the "who made me"
117
+ # information needs to be compared against 'what the session is
118
+ # allowed to see'. That's in the Session's +scoping+ section.
119
+ # See Hoodoo::Services::Session#scoping. For example, a custom
120
+ # Session resource endpoint might record one or more caller
121
+ # UUIDs in "scoping.authorised_caller_uuids".
122
+ #
123
+ # Given things along this line, resource endpoints would have to
124
+ # individually scope ActiveRecord +find+ calls to make sure that it
125
+ # only dealt with database records where the 'who made me' data
126
+ # matched up with the 'what can this Session see'. That can be done
127
+ # but it might be error prone, especially if a lot of resource
128
+ # endpoints all have the same data access scoping rules.
129
+ #
130
+ # == Automatic session-based finder scoping
131
+ #
132
+ # That's where the ActiveRecord secure context extension comes in.
133
+ # Models declare _mappings_ between database fields and fields in
134
+ # the Session's +scoping+ container. An ActiveRecord::Relation is
135
+ # returned which produces a simple query along the lines of:
136
+ #
137
+ # Model.where( :database_field => session.scoping.scoped_field )
138
+ #
139
+ # At the time of writing, only simple matches of as shown above can
140
+ # be defined; bespoke resource endpoint implementation code would be
141
+ # needed for something more complex. All you can do is make sure
142
+ # that one or more fields in the database match with one more fields
143
+ # in the Session scoping data.
144
+ #
145
+ # Taking the examples of a database column +creating_caller_uuid+
146
+ # and a Session scoping entry called +authorised_caller_uuids+, a
147
+ # model would do the following to declare the mapped connection
148
+ # between database and session:
149
+ #
150
+ # class Audit < ActiveRecord::Base
151
+ # include Hoodoo::ActiveRecord::Secure
152
+ #
153
+ # secure_with( {
154
+ # :creating_caller_uuid => :authorised_caller_uuids
155
+ # } )
156
+ # end
157
+ #
158
+ # Then, inside subclass implementation of (for example)
159
+ # Hoodoo::Services::Implementation#list:
160
+ #
161
+ # def list( context )
162
+ # secure_scope = Audit.secure( context )
163
+ # end
164
+ #
165
+ # The 'secure_scope' is just an ActiveRecord::Relation instance;
166
+ # you could call +to_sql+ on the result for debugging and print the
167
+ # result to console if you wanted to see the query built up so far.
168
+ # Otherwise, any of the ActiveRecord::QueryMethods can be called;
169
+ # see:
170
+ #
171
+ # http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html
172
+ #
173
+ # The most common use cases, though, involve finding a specific
174
+ # record or listing records. Hoodoo::ActiveRecord::Finder provides
175
+ # much higher level constructs that build on top of #secure and
176
+ # you are strongly encouraged to use these wherever possible, rather
177
+ # than calling #secure directly.
178
+ #
179
+ # For more advanced query conditions that a single database column
180
+ # checked against a session value with an implicit +AND+, see later.
181
+ #
182
+ # == Important!
183
+ #
184
+ # If you state a model must be secured by one or more fields, then:
185
+ #
186
+ # * If there is no session at all in the given context, _or_
187
+ # * The session has no scoping data, _or_
188
+ # * The session scoping data does not have one or more of the
189
+ # fields that the #secure_with map's values describe, _then_
190
+ #
191
+ # ...the returned scope *will* *find* *no* *results*, by design.
192
+ # The default failure mode is to reveal no data at all.
193
+ #
194
+ # == Rendering resources
195
+ #
196
+ # Models aren't directly connected to Resource representations, but
197
+ # since the security later interfaces with session data herein, there
198
+ # is clearly an intersection of concepts. Even though fields in a
199
+ # Model may not map directly to fields in a related Resource (or
200
+ # many Models might contribute to a Resource), the security scoping
201
+ # rules that led to the limitations on data retrieval may be useful
202
+ # to an API caller. The API basic definitions support this through
203
+ # a +secured_with+ standard (but optional) resource field.
204
+ #
205
+ # The +secured_with+ field's value is an object of key/value pairs.
206
+ # Its contents depend on how the #secure_with method is used in a
207
+ # model. The #secure_with call actually supports _two_ modes of
208
+ # operation. One is as already shown above; suppose we have:
209
+ #
210
+ # secure_with( {
211
+ # :creating_caller_uuid => :authorised_caller_uuids,
212
+ # :programme_code => :authorised_programme_codes
213
+ # } )
214
+ #
215
+ # If Hoodoo::Presenters::Base::render_in is called and an instance of
216
+ # a model with the above declaration is passed in the +secured_with+
217
+ # option, then the keys from the declaration appear in the resource
218
+ # representation's +secured_with+ field's object and the values are
219
+ # the _actual_ scoping values which were used, i.e. the rendered
220
+ # data would contain:
221
+ #
222
+ # {
223
+ # "id": "<UUID>",
224
+ # "kind": "Example",
225
+ # "created_at": "2015-04-30T16:25:17+12:00",
226
+ # "secured_with": {
227
+ # "creating_caller_uuid": "<UUID>",
228
+ # "programme_code": "<code>"
229
+ # },
230
+ # ...
231
+ # }
232
+ #
233
+ # This binds the field values in the model to the values in the
234
+ # rendered resource representation, though; and what if we only wanted
235
+ # (say) the "creating_caller_uuid" to be revealed, but did not want to
236
+ # show the "programme_code" value? To do this, instead of passing a
237
+ # Symbol in the values of the #secure_with options, you provide a Hash
238
+ # of options for that particular security entry. Option keys are
239
+ # Symbols:
240
+ #
241
+ # +session_field_name+:: This is the field that's looked up in the
242
+ # session's scoping section.
243
+ #
244
+ # +resource_field_name+:: This is the name that'll appear in the
245
+ # rendered resource.
246
+ #
247
+ # +hide_from_resource+:: If present and set to +true+, the entry will
248
+ # not be shown; else it is shown by default
249
+ # (if you're passing in a model instance to a
250
+ # render call via the +secured_with+ option it
251
+ # is assumed that you explicitly _do_ want to
252
+ # include this kind of information rather than
253
+ # hide it).
254
+ #
255
+ # +using+:: See the _Advanced_ _query_ _conditions_
256
+ # section later for details.
257
+ #
258
+ # To help clarify the above, the following two calls to #secure_with
259
+ # have exactly the same effect.
260
+ #
261
+ # secure_with( {
262
+ # :creating_caller_uuid => :authorised_caller_uuids
263
+ # } )
264
+ #
265
+ # # ...is equivalent to...
266
+ #
267
+ # secure_with( {
268
+ # :creating_caller_uuid => {
269
+ # :session_field_name => :authorised_caller_uuids,
270
+ # :resource_field_name => :creating_caller_uuid, # (Or just omit this option)
271
+ # :hide_from_resource => false # (Or just omit this option)
272
+ # }
273
+ # } )
274
+ #
275
+ # Taking the previous example, let's change the name of the field shown
276
+ # in the resource and hide the "programme_code" entry:
277
+ #
278
+ # secure_with( {
279
+ # :creating_caller_uuid => {
280
+ # :session_field_name => :authorised_caller_uuids,
281
+ # :resource_field_name => :caller_id # Note renaming of field
282
+ # },
283
+ # :programme_code => {
284
+ # :session_field_name => :authorised_programme_codes,
285
+ # :hide_from_resource => true
286
+ # }
287
+ # } )
288
+ #
289
+ # ...would lead to a rendered resource looking something like this:
290
+ #
291
+ # {
292
+ # "id": "<UUID>",
293
+ # "kind": "Example",
294
+ # "created_at": "2015-04-30T16:25:17+12:00",
295
+ # "secured_with": {
296
+ # "caller_id": "<UUID>",
297
+ # },
298
+ # ...
299
+ # }
300
+ #
301
+ # == Advanced query conditions
302
+ #
303
+ # A simple implicit +AND+ clause on a single database column might
304
+ # not be sufficient for your scoping. In this case, the "longhand"
305
+ # Hash form described for rendering is used, this time including the
306
+ # key +:using+ to specify a Proc that is executed to return an array
307
+ # of parameters for <tt>where</tt>. For example:
308
+ #
309
+ # secure_with( {
310
+ # :creating_caller_uuid => :authorised_caller_uuids
311
+ # } )
312
+ #
313
+ # # ...has this minimal longhand equivalent...
314
+ #
315
+ # secure_with( {
316
+ # :creating_caller_uuid => {
317
+ # :session_field_name => :authorised_caller_uuids
318
+ # }
319
+ # } )
320
+ #
321
+ # This leads to SQL along the following lines:
322
+ #
323
+ # AND ("model_table"."creating_caller_uuid" = '[val]')
324
+ #
325
+ # ...where <tt>val</tt> is from the Session +authorised_caller_uuids+
326
+ # data in the +scoping+ section (so this might be an SQL +IN+ rather
327
+ # than <tt>=</tt> if that data is a multi-element array). Suppose you
328
+ # need to change this to check that value _or_ something else? Use the
329
+ # +:using+ key and a Proc. Since ActiveRecord at the time of writing
330
+ # lacks a high level way to do 'OR' via methods, it's easiest and most
331
+ # flexible just to give up and fall to an SQL string:
332
+ #
333
+ # or_matcher = Proc.new do | model_class, database_column_name, session_field_value |
334
+ #
335
+ # # This example works for non-array and array field values.
336
+ # #
337
+ # session_field_value = [ session_field_value ].flatten
338
+ #
339
+ # [
340
+ # "\"#{ database_column_name }\" IN (?) OR \"other_column_name\" IN (?)",
341
+ # session_field_value,
342
+ # session_field_value
343
+ # ]
344
+ # end
345
+ #
346
+ # secure_with( {
347
+ # :creating_caller_uuid => {
348
+ # :session_field_name => :authorised_caller_uuids,
349
+ # :using => or_matcher
350
+ # }
351
+ # } )
352
+ #
353
+ # ...yields something like:
354
+ #
355
+ # AND ( "model_table"."creating_caller_uuid" = '[val]' OR "model_table"."other_column_name" = '[val]' )
356
+ #
357
+ # A Proc specified with +:using+ is called with:
358
+ #
359
+ # * The model class which is involved in the query.
360
+ #
361
+ # * The name of the database column specified in the +secure_with+
362
+ # Hash as the top-level key (e.g. +creating_called_uuid+ above).
363
+ #
364
+ # * The session field _value_ that was recovered under the given key -
365
+ # the value of +session.scoping.authorised_caller_uuids+ in the
366
+ # example above.
367
+ #
368
+ # You must return _AN ARRAY_ of arguments that will be passed to
369
+ # +where+ via <tt>where( *returned_values )</tt> as part of the wider
370
+ # query chain.
371
+ #
372
+ def secure( context )
373
+ prevailing_scope = all() # "Model.all" -> returns anonymous scope
374
+ extra_scope_map = secured_with()
375
+
376
+ unless extra_scope_map.nil?
377
+ return none() if context.session.nil? || context.session.scoping.nil?
378
+
379
+ extra_scope_map.each do | model_field_name, key_or_options |
380
+ params_proc = DEFAULT_SECURE_PROC
381
+
382
+ if key_or_options.is_a?( Hash )
383
+ session_scoping_key = key_or_options[ :session_field_name ]
384
+ params_proc = key_or_options[ :using ] if key_or_options.has_key?( :using )
385
+ else
386
+ session_scoping_key = key_or_options
387
+ end
388
+
389
+ if context.session.scoping.respond_to?( session_scoping_key )
390
+ args = params_proc.call(
391
+ self,
392
+ model_field_name,
393
+ context.session.scoping.send( session_scoping_key )
394
+ )
395
+ prevailing_scope = prevailing_scope.where( *args )
396
+ else
397
+ prevailing_scope = none()
398
+ break
399
+ end
400
+ end
401
+ end
402
+
403
+ return prevailing_scope
404
+ end
405
+
406
+ # Declare the mapping between database columns and Session scoping
407
+ # entries. See #secure for details and examples.
408
+ #
409
+ # Parameters:
410
+ #
411
+ # +map+:: A Hash of String or Symbol keys and values that gives the
412
+ # secure mapping details. The keys are names of fields in
413
+ # the model. The values are names of fields in the
414
+ # Hoodoo::Services::Session#scoping object, or can be a Hash
415
+ # of options; see #secure for full details and examples.
416
+ #
417
+ def secure_with( map )
418
+ self.nz_co_loyalty_hoodoo_secure_with = map
419
+ end
420
+
421
+ # Retrieve the mapping declared between database columns and
422
+ # Session scoping entries via #secure_with. Returns a map as passed
423
+ # to #secure_with, or +nil+ if no such declaration has been made.
424
+ #
425
+ def secured_with
426
+ self.nz_co_loyalty_hoodoo_secure_with
427
+ end
428
+ end
429
+ end
430
+ end
431
+ end