activity_notification 2.6.1 → 2.7.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
  SHA256:
3
- metadata.gz: fa4bf56cbe8f0a7b85c9d582615af447e15acc1193c94b9e12bdb0a1e2dcbfe9
4
- data.tar.gz: 7e5db0f7477270c39626132b255b7a21d11c290a9f7aee9dc18d1042f2969df5
3
+ metadata.gz: 56122565c61de552c48285f449429a7d64616f5c13f04f0e023a68b33273db6f
4
+ data.tar.gz: 6462762a8f2d8b7b68b82be0632c1dd3d89379aa7fe904c6f4492053be2633fb
5
5
  SHA512:
6
- metadata.gz: 4c9ca166727a66e201f9b1af1588a262223a69331999b272a24c1d7508fcad7a1ffa47e5f9b4bb1eae8d93e1e062cdb84ba4fd0c6bee892dc17e2dce87eb2ee0
7
- data.tar.gz: 927fbf635366912c5d8c1f4f1fe8d48d638978dc37d1c6d8192226d00aea7db163f300ea29722cec1aa32643b3021aa4070aafc9284bf5b5634be00934c2cb42
6
+ metadata.gz: fd7b507f00663ebcf26694a0c8c98a0051f51f7bf35e67687a77e668fb61f500f1cf84e2fa433ecbc39bf59357449fcd0cfd0303b107b7e16ea9838d39e3848f
7
+ data.tar.gz: 12cd4b4e589aa66e4ccb6108beb5624d5b13f3d327234491520551c5d9134afabe718367ee3d5193a92e9a1fd2491c9bf5d59250e8e2bb8588c3bcdfaf8565b7
data/README.md CHANGED
@@ -1,12 +1,10 @@
1
1
  # ActivityNotification
2
2
 
3
- [![Build Status](https://github.com/simukappu/activity_notification/actions/workflows/build.yml/badge.svg)](https://github.com/simukappu/activity_notification/actions/workflows/build.yml)
4
- [![Coverage Status](https://coveralls.io/repos/github/simukappu/activity_notification/badge.svg?branch=master)](https://coveralls.io/github/simukappu/activity_notification?branch=master)
5
- [![Dependency](https://img.shields.io/depfu/simukappu/activity_notification.svg)](https://depfu.com/repos/simukappu/activity_notification)
6
- [![Inline docs](http://inch-ci.org/github/simukappu/activity_notification.svg?branch=master)](http://inch-ci.org/github/simukappu/activity_notification)
7
3
  [![Gem Version](https://badge.fury.io/rb/activity_notification.svg)](https://rubygems.org/gems/activity_notification)
8
4
  [![Gem Downloads](https://img.shields.io/gem/dt/activity_notification.svg)](https://rubygems.org/gems/activity_notification)
9
- [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](MIT-LICENSE)
5
+ [![Build Status](https://github.com/simukappu/activity_notification/actions/workflows/build.yml/badge.svg)](https://github.com/simukappu/activity_notification/actions/workflows/build.yml)
6
+ [![Coverage Status](https://coveralls.io/repos/github/simukappu/activity_notification/badge.svg?branch=master)](https://coveralls.io/github/simukappu/activity_notification?branch=master)
7
+ [![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](MIT-LICENSE)
10
8
 
11
9
  *activity_notification* provides integrated user activity notifications for [Ruby on Rails](https://rubyonrails.org). You can easily use it to configure multiple notification targets and make activity notifications with notifiable models, like adding comments, responding etc.
12
10
 
@@ -4,6 +4,8 @@ class ActivityNotification::NotificationApiChannel < ActivityNotification::Notif
4
4
  # ActionCable::Channel::Base#subscribed
5
5
  # @see https://api.rubyonrails.org/classes/ActionCable/Channel/Base.html#method-i-subscribed
6
6
  def subscribed
7
+ # before_subscribe callbacks do not halt the chain on reject
8
+ return if subscription_rejected?
7
9
  stream_from "#{ActivityNotification.config.notification_api_channel_prefix}_#{@target.to_class_name}#{ActivityNotification.config.composite_key_delimiter}#{@target.id}"
8
10
  rescue
9
11
  reject
@@ -7,6 +7,8 @@ if defined?(ActionCable)
7
7
  # ActionCable::Channel::Base#subscribed
8
8
  # @see https://api.rubyonrails.org/classes/ActionCable/Channel/Base.html#method-i-subscribed
9
9
  def subscribed
10
+ # before_subscribe callbacks do not halt the chain on reject
11
+ return if subscription_rejected?
10
12
  stream_from "#{ActivityNotification.config.notification_channel_prefix}_#{@target.to_class_name}#{ActivityNotification.config.composite_key_delimiter}#{@target.id}"
11
13
  rescue
12
14
  reject
@@ -5,7 +5,6 @@ module ActivityNotification
5
5
  include Swagger::NotificationsApi
6
6
  # Include CommonApiController to select target and define common methods
7
7
  include CommonApiController
8
- protect_from_forgery except: [:open_all]
9
8
  rescue_from ActivityNotification::NotifiableNotFoundError, with: :render_notifiable_not_found
10
9
 
11
10
  # Returns notification index of the target.
@@ -135,14 +134,26 @@ module ActivityNotification
135
134
  # Returns options for notification JSON
136
135
  # @api protected
137
136
  def notification_json_options
137
+ include_options =
138
+ if ActivityNotification.config.restrict_api_response_fields
139
+ {
140
+ target: { only: [:id], methods: [:printable_type, :printable_target_name] },
141
+ notifiable: { only: [:id], methods: [:printable_type] },
142
+ group: { only: [:id], methods: [:printable_type, :printable_group_name] },
143
+ notifier: { only: [:id], methods: [:printable_type, :printable_notifier_name] },
144
+ group_members: { only: [:id] }
145
+ }
146
+ else
147
+ {
148
+ target: { methods: [:printable_type, :printable_target_name] },
149
+ notifiable: { methods: [:printable_type] },
150
+ group: { methods: [:printable_type, :printable_group_name] },
151
+ notifier: { methods: [:printable_type, :printable_notifier_name] },
152
+ group_members: {}
153
+ }
154
+ end
138
155
  {
139
- include: {
140
- target: { methods: [:printable_type, :printable_target_name] },
141
- notifiable: { methods: [:printable_type] },
142
- group: { methods: [:printable_type, :printable_group_name] },
143
- notifier: { methods: [:printable_type, :printable_notifier_name] },
144
- group_members: {}
145
- },
156
+ include: include_options,
146
157
  methods: [:notifiable_path, :printable_notifiable_name, :group_member_notifier_count, :group_notification_count]
147
158
  }
148
159
  end
@@ -47,7 +47,7 @@ module ActivityNotification
47
47
  # @option params [String] :reload ('true') Whether notification index will be reloaded
48
48
  # @return [Response] JavaScript view for ajax request or redirects to back as default
49
49
  def open_all
50
- @opened_notifications = @target.open_all_notifications(params)
50
+ @opened_notifications = @target.open_all_notifications(bulk_action_options)
51
51
  return_back_or_ajax
52
52
  end
53
53
 
@@ -70,7 +70,7 @@ module ActivityNotification
70
70
  # @option params [String] :reload ('true') Whether notification index will be reloaded
71
71
  # @return [Response] JavaScript view for ajax request or redirects to back as default
72
72
  def destroy_all
73
- @destroyed_notifications = @target.destroy_all_notifications(params)
73
+ @destroyed_notifications = @target.destroy_all_notifications(bulk_action_options)
74
74
  set_index_options
75
75
  load_index if params[:reload].to_s.to_boolean(true)
76
76
  return_back_or_ajax
@@ -152,6 +152,21 @@ module ActivityNotification
152
152
  validate_target(@notification = Notification.with_target.find(params[:id]))
153
153
  end
154
154
 
155
+ # Builds a permitted options hash for bulk actions (open_all / destroy_all)
156
+ # from request parameters.
157
+ # Only known-safe filter keys are permitted here. Request parameters must
158
+ # never be forwarded to the model layer as-is, because :custom_filter is
159
+ # passed straight to ActiveRecord's where as a raw SQL fragment and would
160
+ # allow SQL injection from untrusted input.
161
+ # @api protected
162
+ # @return [Hash] permitted options for bulk actions
163
+ def bulk_action_options
164
+ params.permit(:filter, :limit, :reverse, :without_grouping, :with_group_members,
165
+ :filtered_by_type, :filtered_by_group_type, :filtered_by_group_id, :filtered_by_key,
166
+ :later_than, :earlier_than, :routing_scope, :devise_default_routes, ids: [])
167
+ .to_h.symbolize_keys
168
+ end
169
+
155
170
  # Sets options to load notification index from request parameters.
156
171
  # @api protected
157
172
  # @return [Hash] options to load notification index
@@ -5,7 +5,6 @@ module ActivityNotification
5
5
  include Swagger::SubscriptionsApi
6
6
  # Include CommonApiController to select target and define common methods
7
7
  include CommonApiController
8
- protect_from_forgery except: [:create]
9
8
  before_action :set_subscription, except: [:index, :create, :find, :optional_target_names]
10
9
  before_action ->{ validate_param(:key) }, only: [:find, :optional_target_names]
11
10
 
@@ -171,7 +170,11 @@ module ActivityNotification
171
170
  # Returns include option for subscription JSON
172
171
  # @api protected
173
172
  def subscription_json_include_option
174
- [:target].freeze
173
+ if ActivityNotification.config.restrict_api_response_fields
174
+ { target: { only: [:id], methods: [:printable_type, :printable_target_name] } }.freeze
175
+ else
176
+ [:target].freeze
177
+ end
175
178
  end
176
179
 
177
180
  # Returns methods option for subscription JSON
@@ -40,7 +40,7 @@
40
40
  </style>
41
41
 
42
42
  <% if @target.notification_action_cable_allowed? %>
43
- <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/push.js/1.0.9/push.min.js"></script>
43
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/push.js/1.0.9/push.min.js" integrity="sha512-eiqtDDb4GUVCSqOSOTz/s/eiU4B31GrdSb17aPAA4Lv/Cjc8o+hnDvuNkgXhSI5yHuDvYkuojMaQmrB5JB31XQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
44
44
  <script>
45
45
  App.activity_notification = App.cable.subscriptions.create(
46
46
  {
data/docs/Functions.md CHANGED
@@ -336,6 +336,8 @@ You can also add conditions to filter notifications, like this:
336
336
  User.send_batch_unopened_notification_email(batch_key: 'batch.comment.post', filtered_by_key: 'comment.post', custom_filter: ["created_at >= ?", time.hour.ago])
337
337
  ```
338
338
 
339
+ *custom_filter* accepts an array of a SQL fragment with bind parameters or a hash of conditions. Write it in your application code and never build it from request parameters, since it is passed to the ORM query layer as is. A raw SQL string, an array without bind parameters and MongoDB JavaScript operators (`$where`, `$function` and `$accumulator`) are rejected with `ArgumentError`.
340
+
339
341
  #### Batch sender configuration
340
342
 
341
343
  *activity_notification* uses same sender configuration of real-time email notification as batch email sender.
@@ -709,6 +711,22 @@ When you want to use REST API backend integrated with Devise authentication, see
709
711
 
710
712
  You can see [sample single page application](/spec/rails_app/app/javascript/) using [Vue.js](https://vuejs.org) as a part of example Rails application. This sample application works with *activity_notification* REST API backend.
711
713
 
714
+ #### Securing the REST API
715
+
716
+ The REST API endpoints do not authenticate requests on their own. Authenticating requests and authorizing access to a target's notifications and subscriptions is the responsibility of your application. The recommended way is to use the *:with_devise* option so that only the authenticated resource can access its own records. See [REST API backend with Devise Token Auth](#rest-api-backend-with-devise-token-auth) for the setup. The controllers shipped for *:with_devise* return HTTP 403 for a target that does not belong to the authenticated resource.
717
+
718
+ By default, notification and subscription responses embed the associated records (target, notifiable, notifier, group, group members and subscription target) with all of their database columns. When those associated records are your user model, the response can include columns you may not want to expose to the frontend, such as authentication identifiers. Two options are available to reduce this exposure.
719
+
720
+ The simplest option is to enable *config.restrict_api_response_fields*, which restricts every embedded associated record to its *id* and *printable_\** method values instead of all columns. It is disabled by default to preserve the existing response shape:
721
+
722
+ ```ruby
723
+ ActivityNotification.configure do |config|
724
+ config.restrict_api_response_fields = true
725
+ end
726
+ ```
727
+
728
+ If you need finer control over the response shape, override the serialization options in your own API controllers instead. *ActivityNotification::NotificationsApiController#notification_json_options* and *ActivityNotification::SubscriptionsApiController#subscription_json_include_option* are protected methods that you can override to return only the fields your frontend consumes.
729
+
712
730
  #### API reference as OpenAPI Specification
713
731
 
714
732
  *activity_notification* provides API reference as [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification).
@@ -991,7 +1009,7 @@ Then, *activity_notification* will broadcast configured notifications to target
991
1009
  You can simply create subscriptions for the specified target in your view like this:
992
1010
 
993
1011
  ```js
994
- <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/push.js/1.0.9/push.min.js"></script>
1012
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/push.js/1.0.9/push.min.js" integrity="sha512-eiqtDDb4GUVCSqOSOTz/s/eiU4B31GrdSb17aPAA4Lv/Cjc8o+hnDvuNkgXhSI5yHuDvYkuojMaQmrB5JB31XQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
995
1013
  <script>
996
1014
  App.activity_notification = App.cable.subscriptions.create(
997
1015
  {
@@ -1029,7 +1047,7 @@ You can simply create subscriptions for the specified target in your view like t
1029
1047
  or create subscriptions in your single page application with API channels like this:
1030
1048
 
1031
1049
  ```js
1032
- // Vue.js implementation with actioncable-vue
1050
+ // Vue.js implementation with a small Action Cable plugin (see spec/rails_app/app/javascript/plugins/action-cable.js)
1033
1051
  export default {
1034
1052
  // ...
1035
1053
  mounted () {
@@ -135,7 +135,7 @@ module ActivityNotification
135
135
  filtered_notifications = filtered_notifications.earlier_than(Time.iso8601(options[:earlier_than]))
136
136
  end
137
137
  if options.has_key?(:custom_filter)
138
- filtered_notifications = filtered_notifications.where(options[:custom_filter])
138
+ filtered_notifications = filtered_notifications.where(ActivityNotification.validate_custom_filter(options[:custom_filter]))
139
139
  end
140
140
  filtered_notifications
141
141
  }
@@ -31,7 +31,7 @@ module ActivityNotification
31
31
  filtered_subscriptions = filtered_subscriptions.filtered_by_key(options[:filtered_by_key])
32
32
  end
33
33
  if options.has_key?(:custom_filter)
34
- filtered_subscriptions = filtered_subscriptions.where(options[:custom_filter])
34
+ filtered_subscriptions = filtered_subscriptions.where(ActivityNotification.validate_custom_filter(options[:custom_filter]))
35
35
  end
36
36
  filtered_subscriptions
37
37
  }
@@ -47,6 +47,51 @@ module ActivityNotification
47
47
  end
48
48
  end
49
49
 
50
+ # MongoDB query operators which execute JavaScript on the database server.
51
+ JAVASCRIPT_QUERY_OPERATORS = %w[$where $function $accumulator].freeze
52
+
53
+ # Validates a :custom_filter value before it is passed to the ORM query layer.
54
+ # custom_filter is a trusted input written by the application developer and
55
+ # must not be built from request parameters. This validation rejects the forms
56
+ # that run raw SQL or server-side JavaScript:
57
+ # - A String, since ActiveRecord's where treats it as a raw SQL fragment
58
+ # - An Array without bind parameters (e.g. ["1=1"]), which is also a raw SQL fragment
59
+ # - A Hash containing MongoDB JavaScript operators ($where, $function and $accumulator)
60
+ # The supported forms are an Array of a SQL fragment with bind parameters
61
+ # (e.g. ["created_at >= ?", time]) or a Hash of conditions.
62
+ # @param [Array, Hash] custom_filter Custom filter value
63
+ # @return [Array, Hash] The same value when it is safe to use
64
+ # @raise [ArgumentError] When custom_filter is not in the supported forms
65
+ def self.validate_custom_filter(custom_filter)
66
+ valid =
67
+ case custom_filter
68
+ when Array then custom_filter.size >= 2 && custom_filter.first.is_a?(String)
69
+ when Hash then !contains_javascript_query_operator?(custom_filter)
70
+ else false
71
+ end
72
+ unless valid
73
+ raise ArgumentError,
74
+ ":custom_filter must be an array of a SQL fragment and bind parameters " \
75
+ "(e.g. [\"created_at >= ?\", time]) or a hash of conditions without JavaScript operators"
76
+ end
77
+ custom_filter
78
+ end
79
+
80
+ # Returns whether the value contains MongoDB query operators which execute JavaScript.
81
+ # @param [Object] value Value to check
82
+ # @return [Boolean] Whether the value contains JavaScript query operators
83
+ def self.contains_javascript_query_operator?(value)
84
+ case value
85
+ when Hash
86
+ value.any? { |k, v| JAVASCRIPT_QUERY_OPERATORS.include?(k.to_s) || contains_javascript_query_operator?(v) }
87
+ when Array
88
+ value.any? { |v| contains_javascript_query_operator?(v) }
89
+ else
90
+ false
91
+ end
92
+ end
93
+ private_class_method :contains_javascript_query_operator?
94
+
50
95
  # Casts to indifferent hash
51
96
  # @param [ActionController::Parameters, Hash] hash
52
97
  # @return [HashWithIndifferentAccess] Converted indifferent hash
@@ -240,6 +240,15 @@ module ActivityNotification
240
240
  # @return [Boolean] Whether activity_notification internally rescues optional target errors.
241
241
  attr_accessor :rescue_optional_target_errors
242
242
 
243
+ # @overload restrict_api_response_fields
244
+ # Returns whether the REST API restricts embedded associated records to their id and printable_* method values
245
+ # @return [Boolean] Whether the REST API restricts embedded associated records to their id and printable_* method values.
246
+ # @overload restrict_api_response_fields=(value)
247
+ # Sets whether the REST API restricts embedded associated records (target, notifiable, notifier, group, group members and subscription target) in JSON responses to their id and printable_* method values instead of all database columns. This is an opt-in hardening option to avoid exposing columns the frontend does not need (e.g. authentication identifiers on an associated user record). It is disabled by default to preserve the existing response shape.
248
+ # @param [Boolean] restrict_api_response_fields The new restrict_api_response_fields
249
+ # @return [Boolean] Whether the REST API restricts embedded associated records to their id and printable_* method values.
250
+ attr_accessor :restrict_api_response_fields
251
+
243
252
  # Initialize configuration for ActivityNotification.
244
253
  # These configuration can be overridden in initializer.
245
254
  # @return [Config] A new instance of Config
@@ -272,6 +281,7 @@ module ActivityNotification
272
281
  @notification_channel_prefix = 'activity_notification_channel'
273
282
  @notification_api_channel_prefix = 'activity_notification_api_channel'
274
283
  @rescue_optional_target_errors = true
284
+ @restrict_api_response_fields = false
275
285
  end
276
286
 
277
287
  # Sets ORM name for ActivityNotification (:active_record, :mongoid or :dynamodb)
@@ -21,7 +21,7 @@ module ActivityNotification
21
21
  notifications.render self, options
22
22
  elsif notifications.respond_to?(:map)
23
23
  return nil if (notifications.respond_to?(:empty?) ? notifications.empty? : notifications.to_a.empty?)
24
- notifications.map {|notification| notification.render self, options.dup }.join.html_safe
24
+ safe_join(notifications.map {|notification| notification.render self, options.dup })
25
25
  end
26
26
  end
27
27
  alias_method :render_notifications, :render_notification
@@ -332,7 +332,7 @@ module Dynamoid # :nodoc: all
332
332
  filtered_notifications = filtered_notifications.earlier_than(Time.iso8601(options[:earlier_than]))
333
333
  end
334
334
  if options.has_key?(:custom_filter)
335
- filtered_notifications = filtered_notifications.where(options[:custom_filter])
335
+ filtered_notifications = filtered_notifications.where(ActivityNotification.validate_custom_filter(options[:custom_filter]))
336
336
  end
337
337
  filtered_notifications
338
338
  end
@@ -1,3 +1,3 @@
1
1
  module ActivityNotification
2
- VERSION = "2.6.1"
2
+ VERSION = "2.7.1"
3
3
  end
@@ -121,4 +121,12 @@ ActivityNotification.configure do |config|
121
121
  # See https://github.com/simukappu/activity_notification/issues/155 for more details.
122
122
  config.rescue_optional_target_errors = true
123
123
 
124
+ # Configure if the REST API restricts embedded associated records in JSON responses.
125
+ # When set true, associated records (target, notifiable, notifier, group, group members
126
+ # and subscription target) are serialized with only their id and printable_* method values
127
+ # instead of all database columns, to avoid exposing columns the frontend does not need.
128
+ # Note that authentication and authorization of the REST API is still the responsibility of
129
+ # your application (see the :with_devise routing option). Default value is false.
130
+ # config.restrict_api_response_fields = false
131
+
124
132
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activity_notification
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.1
4
+ version: 2.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shota Yamazaki
@@ -311,7 +311,7 @@ description: Integrated user activity notifications for Ruby on Rails. Provides
311
311
  to configure multiple notification targets and make activity notifications with
312
312
  notifiable models, like adding comments, responding etc.
313
313
  email:
314
- - shota.yamazaki.8@gmail.com
314
+ - shota.yamazaki.work@gmail.com
315
315
  executables: []
316
316
  extensions: []
317
317
  extra_rdoc_files: []