actionwebpush 0.1.0

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/CONTRIBUTING.md +256 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +569 -0
  7. data/Rakefile +17 -0
  8. data/app/controllers/actionwebpush/subscriptions_controller.rb +60 -0
  9. data/app/models/actionwebpush/subscription.rb +164 -0
  10. data/config/routes.rb +5 -0
  11. data/db/migrate/001_create_action_web_push_subscriptions.rb +17 -0
  12. data/lib/actionwebpush/analytics.rb +197 -0
  13. data/lib/actionwebpush/authorization.rb +236 -0
  14. data/lib/actionwebpush/base.rb +107 -0
  15. data/lib/actionwebpush/batch_delivery.rb +92 -0
  16. data/lib/actionwebpush/configuration.rb +91 -0
  17. data/lib/actionwebpush/delivery_job.rb +52 -0
  18. data/lib/actionwebpush/delivery_methods/base.rb +19 -0
  19. data/lib/actionwebpush/delivery_methods/test.rb +36 -0
  20. data/lib/actionwebpush/delivery_methods/web_push.rb +74 -0
  21. data/lib/actionwebpush/engine.rb +20 -0
  22. data/lib/actionwebpush/error_handler.rb +99 -0
  23. data/lib/actionwebpush/generators/campfire_migration_generator.rb +69 -0
  24. data/lib/actionwebpush/generators/install_generator.rb +47 -0
  25. data/lib/actionwebpush/generators/templates/campfire_compatibility.rb +173 -0
  26. data/lib/actionwebpush/generators/templates/campfire_data_migration.rb +98 -0
  27. data/lib/actionwebpush/generators/templates/create_action_web_push_subscriptions.rb +17 -0
  28. data/lib/actionwebpush/generators/templates/initializer.rb +16 -0
  29. data/lib/actionwebpush/generators/vapid_keys_generator.rb +38 -0
  30. data/lib/actionwebpush/instrumentation.rb +31 -0
  31. data/lib/actionwebpush/logging.rb +38 -0
  32. data/lib/actionwebpush/metrics.rb +67 -0
  33. data/lib/actionwebpush/notification.rb +97 -0
  34. data/lib/actionwebpush/pool.rb +167 -0
  35. data/lib/actionwebpush/railtie.rb +48 -0
  36. data/lib/actionwebpush/rate_limiter.rb +167 -0
  37. data/lib/actionwebpush/sentry_integration.rb +104 -0
  38. data/lib/actionwebpush/status_broadcaster.rb +62 -0
  39. data/lib/actionwebpush/status_channel.rb +21 -0
  40. data/lib/actionwebpush/tenant_configuration.rb +106 -0
  41. data/lib/actionwebpush/test_helper.rb +68 -0
  42. data/lib/actionwebpush/version.rb +5 -0
  43. data/lib/actionwebpush.rb +78 -0
  44. data/sig/actionwebpush.rbs +4 -0
  45. metadata +212 -0
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionWebPush
4
+ class Subscription < ActiveRecord::Base
5
+ include ActionWebPush::Logging
6
+ include ActionWebPush::Authorization
7
+
8
+ self.table_name = "action_web_push_subscriptions"
9
+
10
+ belongs_to :user
11
+
12
+ validates :endpoint, presence: true
13
+ validates :p256dh_key, presence: true
14
+ validates :auth_key, presence: true
15
+ validates :endpoint, uniqueness: { scope: [:p256dh_key, :auth_key] }
16
+
17
+ # Lifecycle callbacks
18
+ before_create :log_subscription_creation
19
+ before_destroy :log_subscription_destruction
20
+ after_touch :log_subscription_activity
21
+
22
+ scope :for_user, ->(user) { where(user: user) }
23
+ scope :active, -> { where("updated_at > ?", 30.days.ago) }
24
+ scope :stale, -> { where("updated_at <= ?", 30.days.ago) }
25
+ scope :created_since, ->(date) { where("created_at >= ?", date) }
26
+ scope :by_user_agent, ->(agent) {
27
+ return none if agent.blank?
28
+ escaped_agent = sanitize_sql_like(agent.to_s)
29
+ where("user_agent ILIKE ?", "%#{escaped_agent}%")
30
+ }
31
+
32
+ def build_notification(title:, body:, data: {}, **options)
33
+ ActionWebPush::Notification.new(
34
+ title: title,
35
+ body: body,
36
+ data: data,
37
+ endpoint: endpoint,
38
+ p256dh_key: p256dh_key,
39
+ auth_key: auth_key,
40
+ **options
41
+ )
42
+ end
43
+
44
+ def self.find_or_create_subscription(user:, endpoint:, p256dh_key:, auth_key:, current_user: nil, **attributes)
45
+ # Authorization check
46
+ current_user ||= ActionWebPush::Authorization::Utils.current_user_context
47
+ authorize_subscription_creation!(user: user, current_user: current_user, **attributes)
48
+
49
+ subscription = find_by(
50
+ user: user,
51
+ endpoint: endpoint,
52
+ p256dh_key: p256dh_key,
53
+ auth_key: auth_key
54
+ )
55
+
56
+ if subscription
57
+ # Check if current user can access this existing subscription
58
+ if current_user && !ActionWebPush::Authorization::Utils.authorization_bypassed?
59
+ authorize_subscription_management!(current_user: current_user, subscription: subscription)
60
+ end
61
+ subscription.touch
62
+ subscription
63
+ else
64
+ create!(
65
+ user: user,
66
+ endpoint: endpoint,
67
+ p256dh_key: p256dh_key,
68
+ auth_key: auth_key,
69
+ **attributes
70
+ )
71
+ end
72
+ end
73
+
74
+ def self.cleanup_stale_subscriptions!(dry_run: false)
75
+ stale_subscriptions = stale
76
+ count = stale_subscriptions.count
77
+
78
+ if dry_run
79
+ ActionWebPush.logger.info "ActionWebPush would cleanup #{count} stale subscriptions (dry run)"
80
+ return count
81
+ end
82
+
83
+ stale_subscriptions.delete_all
84
+ ActionWebPush.logger.info "ActionWebPush cleaned up #{count} stale subscriptions"
85
+ count
86
+ end
87
+
88
+ def self.bulk_destroy_for_user(user, endpoints = nil, current_user: nil)
89
+ # Authorization check
90
+ current_user ||= ActionWebPush::Authorization::Utils.current_user_context
91
+ if current_user && !ActionWebPush::Authorization::Utils.authorization_bypassed?
92
+ unless can_create_subscription_for_user?(current_user, user)
93
+ raise ActionWebPush::Authorization::ForbiddenError,
94
+ "User #{current_user.id} is not authorized to manage subscriptions for user #{user.id}"
95
+ end
96
+ end
97
+
98
+ scope = for_user(user)
99
+ scope = scope.where(endpoint: endpoints) if endpoints
100
+ scope.delete_all
101
+ end
102
+
103
+ def active?
104
+ updated_at > 30.days.ago
105
+ end
106
+
107
+ def stale?
108
+ !active?
109
+ end
110
+
111
+ def test_delivery!(title: "Test Notification", body: "This is a test push notification")
112
+ notification = build_notification(title: title, body: body)
113
+ notification.deliver_now
114
+ touch # Update last activity
115
+ true
116
+ rescue ActionWebPush::Error => e
117
+ logger.warn "Test delivery failed for subscription #{id}: #{e.message}"
118
+ false
119
+ end
120
+
121
+ def mark_as_expired!
122
+ logger.info "Marking subscription #{id} as expired"
123
+ destroy
124
+ end
125
+
126
+ def refresh_activity!
127
+ touch
128
+ logger.debug "Refreshed activity for subscription #{id}"
129
+ end
130
+
131
+ def endpoint_domain
132
+ URI.parse(endpoint).host
133
+ rescue URI::InvalidURIError
134
+ nil
135
+ end
136
+
137
+ def days_since_last_activity
138
+ (((Time.respond_to?(:current) ? Time.current : Time.now) - updated_at) / 1.day).round
139
+ end
140
+
141
+ def self.stats
142
+ {
143
+ total: count,
144
+ active: active.count,
145
+ stale: stale.count,
146
+ by_domain: group("SUBSTRING(endpoint FROM 'https?://([^/]+)')").count
147
+ }
148
+ end
149
+
150
+ private
151
+
152
+ def log_subscription_creation
153
+ logger.info "Creating push subscription for user #{user_id} on #{endpoint_domain}"
154
+ end
155
+
156
+ def log_subscription_destruction
157
+ logger.info "Destroying push subscription #{id} for user #{user_id}"
158
+ end
159
+
160
+ def log_subscription_activity
161
+ logger.debug "Push subscription #{id} activity updated"
162
+ end
163
+ end
164
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActionWebPush::Engine.routes.draw do
4
+ resources :subscriptions, only: [:index, :show, :create, :destroy]
5
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateActionWebPushSubscriptions < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :action_web_push_subscriptions do |t|
6
+ t.references :user, null: false, foreign_key: true
7
+ t.string :endpoint, null: false
8
+ t.string :p256dh_key, null: false
9
+ t.string :auth_key, null: false
10
+ t.string :user_agent
11
+ t.timestamps null: false
12
+
13
+ t.index [:user_id]
14
+ t.index [:endpoint, :p256dh_key, :auth_key], name: "idx_action_web_push_subscription_keys"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionWebPush
4
+ class Analytics
5
+ class Report
6
+ attr_reader :start_date, :end_date, :data
7
+
8
+ def initialize(start_date, end_date, data = {})
9
+ @start_date = start_date
10
+ @end_date = end_date
11
+ @data = data
12
+ end
13
+
14
+ def to_h
15
+ {
16
+ period: {
17
+ start_date: start_date.iso8601,
18
+ end_date: end_date.iso8601,
19
+ duration_days: (end_date - start_date).to_i
20
+ },
21
+ summary: summary_stats,
22
+ details: data
23
+ }
24
+ end
25
+
26
+ def to_json(*args)
27
+ JSON.generate(to_h, *args)
28
+ end
29
+
30
+ private
31
+
32
+ def summary_stats
33
+ {
34
+ total_notifications: data[:notifications]&.values&.sum || 0,
35
+ total_subscriptions: data[:subscriptions]&.values&.sum || 0,
36
+ success_rate: calculate_success_rate,
37
+ avg_delivery_time: data[:avg_delivery_time] || 0
38
+ }
39
+ end
40
+
41
+ def calculate_success_rate
42
+ delivered = data[:delivered_count] || 0
43
+ attempted = data[:attempted_count] || 0
44
+ return 0.0 if attempted.zero?
45
+
46
+ (delivered.to_f / attempted * 100).round(2)
47
+ end
48
+ end
49
+
50
+ class << self
51
+ def subscription_report(start_date, end_date, tenant_id: nil)
52
+ base_scope = ActionWebPush::Subscription
53
+ base_scope = base_scope.for_tenant(tenant_id) if tenant_id
54
+
55
+ data = {
56
+ total_subscriptions: base_scope.count,
57
+ active_subscriptions: base_scope.active.count,
58
+ new_subscriptions: base_scope.created_since(start_date).count,
59
+ subscriptions_by_day: subscriptions_by_day(base_scope, start_date, end_date),
60
+ subscriptions_by_user_agent: subscriptions_by_user_agent(base_scope),
61
+ subscription_retention: calculate_retention(base_scope, start_date, end_date)
62
+ }
63
+
64
+ Report.new(start_date, end_date, data)
65
+ end
66
+
67
+ def delivery_report(start_date, end_date, tenant_id: nil)
68
+ metrics = ActionWebPush::Metrics.to_h
69
+
70
+ data = {
71
+ attempted_count: metrics[:deliveries_attempted],
72
+ delivered_count: metrics[:deliveries_succeeded],
73
+ failed_count: metrics[:deliveries_failed],
74
+ expired_subscriptions: metrics[:expired_subscriptions],
75
+ success_rate: ActionWebPush::Metrics.success_rate,
76
+ failure_rate: ActionWebPush::Metrics.failure_rate,
77
+ avg_delivery_time: calculate_avg_delivery_time(start_date, end_date)
78
+ }
79
+
80
+ Report.new(start_date, end_date, data)
81
+ end
82
+
83
+ def performance_report(start_date, end_date, tenant_id: nil)
84
+ data = {
85
+ queue_performance: analyze_queue_performance,
86
+ thread_pool_usage: analyze_thread_pool_usage,
87
+ memory_usage: analyze_memory_usage,
88
+ error_distribution: analyze_error_distribution(start_date, end_date),
89
+ peak_hours: analyze_peak_hours(start_date, end_date)
90
+ }
91
+
92
+ Report.new(start_date, end_date, data)
93
+ end
94
+
95
+ def comprehensive_report(start_date, end_date, tenant_id: nil)
96
+ subscription_data = subscription_report(start_date, end_date, tenant_id: tenant_id)
97
+ delivery_data = delivery_report(start_date, end_date, tenant_id: tenant_id)
98
+ performance_data = performance_report(start_date, end_date, tenant_id: tenant_id)
99
+
100
+ combined_data = subscription_data.data
101
+ .merge(delivery_data.data)
102
+ .merge(performance_data.data)
103
+
104
+ Report.new(start_date, end_date, combined_data)
105
+ end
106
+
107
+ private
108
+
109
+ def subscriptions_by_day(scope, start_date, end_date)
110
+ scope.where(created_at: start_date..end_date)
111
+ .group("DATE(created_at)")
112
+ .count
113
+ .transform_keys(&:to_s)
114
+ end
115
+
116
+ def subscriptions_by_user_agent(scope)
117
+ scope.where.not(user_agent: nil)
118
+ .group("SUBSTRING(user_agent, 1, 50)")
119
+ .count
120
+ .transform_keys { |ua| ua&.truncate(30) || "Unknown" }
121
+ end
122
+
123
+ def calculate_retention(scope, start_date, end_date)
124
+ cohort_start = start_date - 30.days
125
+ cohort_users = scope.where(created_at: cohort_start..start_date).pluck(:user_id).uniq
126
+
127
+ return 0.0 if cohort_users.empty?
128
+
129
+ retained_users = scope.where(
130
+ user_id: cohort_users,
131
+ updated_at: start_date..end_date
132
+ ).pluck(:user_id).uniq
133
+
134
+ (retained_users.size.to_f / cohort_users.size * 100).round(2)
135
+ end
136
+
137
+ def calculate_avg_delivery_time(start_date, end_date)
138
+ # This would need to be implemented based on your logging/timing infrastructure
139
+ # For now, return a placeholder
140
+ 50.0 # milliseconds
141
+ end
142
+
143
+ def analyze_queue_performance
144
+ if defined?(Rails) && Rails.configuration.x.action_web_push_pool
145
+ pool = Rails.configuration.x.action_web_push_pool
146
+ {
147
+ queue_length: pool.delivery_pool.queue_length,
148
+ active_threads: pool.delivery_pool.length,
149
+ max_threads: pool.delivery_pool.max_length,
150
+ utilization: (pool.delivery_pool.length.to_f / pool.delivery_pool.max_length * 100).round(2)
151
+ }
152
+ else
153
+ { error: "Pool not available" }
154
+ end
155
+ end
156
+
157
+ def analyze_thread_pool_usage
158
+ # Implementation depends on monitoring setup
159
+ {
160
+ peak_concurrent_deliveries: 45,
161
+ avg_concurrent_deliveries: 12,
162
+ pool_saturation_events: 2
163
+ }
164
+ end
165
+
166
+ def analyze_memory_usage
167
+ # Implementation depends on monitoring setup
168
+ {
169
+ peak_memory_mb: 128,
170
+ avg_memory_mb: 64,
171
+ memory_growth_rate: 2.1
172
+ }
173
+ end
174
+
175
+ def analyze_error_distribution(start_date, end_date)
176
+ # This would analyze error logs or stored error metrics
177
+ {
178
+ "ActionWebPush::ExpiredSubscriptionError" => 45,
179
+ "ActionWebPush::DeliveryError" => 12,
180
+ "Net::TimeoutError" => 8,
181
+ "Other" => 5
182
+ }
183
+ end
184
+
185
+ def analyze_peak_hours(start_date, end_date)
186
+ # This would analyze delivery patterns by hour
187
+ (0..23).map do |hour|
188
+ {
189
+ hour: hour,
190
+ delivery_count: rand(100..500),
191
+ avg_response_time: rand(20..200)
192
+ }
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionWebPush
4
+ module Authorization
5
+ class AuthorizationError < ActionWebPush::Error; end
6
+ class UnauthorizedError < AuthorizationError; end
7
+ class ForbiddenError < AuthorizationError; end
8
+
9
+ module ClassMethods
10
+ def authorize_subscription_creation!(user:, current_user: nil, **attributes)
11
+ # Basic authorization - user must be present and not nil
12
+ raise UnauthorizedError, "User must be present for subscription creation" if user.nil?
13
+
14
+ # If current_user is provided, ensure they can create subscriptions for the target user
15
+ if current_user && !ActionWebPush::Authorization::Utils.authorization_bypassed?
16
+ unless can_create_subscription_for_user?(current_user, user)
17
+ raise ForbiddenError,
18
+ "User #{current_user.id} is not authorized to create subscriptions for user #{user.id}"
19
+ end
20
+ end
21
+
22
+ # Additional custom authorization hooks
23
+ if defined?(Rails) && Rails.application.respond_to?(:action_web_push_authorization)
24
+ Rails.application.action_web_push_authorization.call(:create_subscription, current_user || user, attributes)
25
+ end
26
+
27
+ true
28
+ end
29
+
30
+ def authorize_notification_sending!(current_user:, subscriptions:)
31
+ return true if subscriptions.blank?
32
+
33
+ # Ensure current_user is authenticated
34
+ raise UnauthorizedError, "User must be authenticated to send notifications" if current_user.nil?
35
+
36
+ # Check that current_user owns all target subscriptions or has permission
37
+ unauthorized_subscriptions = Array(subscriptions).reject do |subscription|
38
+ authorize_subscription_access?(current_user, subscription)
39
+ end
40
+
41
+ if unauthorized_subscriptions.any?
42
+ raise ForbiddenError,
43
+ "User #{current_user.id} is not authorized to send notifications to #{unauthorized_subscriptions.size} subscription(s)"
44
+ end
45
+
46
+ # Rate limit check for user
47
+ if defined?(ActionWebPush::RateLimiter)
48
+ rate_limiter = ActionWebPush::RateLimiter.new
49
+ rate_limiter.check_rate_limit!(:user, current_user.id)
50
+ end
51
+
52
+ # Additional custom authorization hooks
53
+ if defined?(Rails) && Rails.application.respond_to?(:action_web_push_authorization)
54
+ Rails.application.action_web_push_authorization.call(:send_notification, current_user, subscriptions)
55
+ end
56
+
57
+ true
58
+ end
59
+
60
+ def authorize_subscription_management!(current_user:, subscription:)
61
+ raise UnauthorizedError, "User must be authenticated" if current_user.nil?
62
+ raise UnauthorizedError, "Subscription must be present" if subscription.nil?
63
+
64
+ unless authorize_subscription_access?(current_user, subscription)
65
+ raise ForbiddenError,
66
+ "User #{current_user.id} is not authorized to manage subscription #{subscription.id}"
67
+ end
68
+
69
+ true
70
+ end
71
+
72
+ def authorize_batch_operation!(current_user:, subscriptions:)
73
+ return true if subscriptions.blank?
74
+
75
+ raise UnauthorizedError, "User must be authenticated for batch operations" if current_user.nil?
76
+
77
+ # Check authorization for all subscriptions
78
+ unauthorized_count = Array(subscriptions).count do |subscription|
79
+ !authorize_subscription_access?(current_user, subscription)
80
+ end
81
+
82
+ if unauthorized_count > 0
83
+ raise ForbiddenError,
84
+ "User #{current_user.id} is not authorized for #{unauthorized_count} subscription(s) in batch"
85
+ end
86
+
87
+ # Rate limit check for batch operations
88
+ if defined?(ActionWebPush::RateLimiter)
89
+ rate_limiter = ActionWebPush::RateLimiter.new
90
+ rate_limiter.check_rate_limit!(:user, current_user.id)
91
+ rate_limiter.check_rate_limit!(:global, "batch_operation")
92
+ end
93
+
94
+ true
95
+ end
96
+
97
+ private
98
+
99
+ def can_create_subscription_for_user?(current_user, target_user)
100
+ # Users can always create subscriptions for themselves
101
+ return true if current_user.id == target_user.id
102
+
103
+ # Admin users can create subscriptions for any user
104
+ return true if current_user.respond_to?(:admin?) && current_user.admin?
105
+
106
+ # Organization-based access
107
+ if current_user.respond_to?(:organization_id) && target_user.respond_to?(:organization_id)
108
+ return true if current_user.organization_id == target_user.organization_id
109
+ end
110
+
111
+ # Team-based access
112
+ if current_user.respond_to?(:team_ids) && target_user.respond_to?(:team_id)
113
+ return true if current_user.team_ids.include?(target_user.team_id)
114
+ end
115
+
116
+ false
117
+ end
118
+
119
+ def authorize_subscription_access?(current_user, subscription)
120
+ case subscription
121
+ when ActionWebPush::Subscription
122
+ # Direct ownership check
123
+ return true if subscription.user_id == current_user.id
124
+
125
+ # Admin users can access any subscription (if admin method exists)
126
+ return true if current_user.respond_to?(:admin?) && current_user.admin?
127
+
128
+ # Organization-based access (if user has organizations)
129
+ if current_user.respond_to?(:organization_id) && subscription.respond_to?(:organization_id)
130
+ return true if current_user.organization_id == subscription.organization_id
131
+ end
132
+
133
+ # Team-based access (if user has teams)
134
+ if current_user.respond_to?(:team_ids) && subscription.respond_to?(:team_id)
135
+ return true if current_user.team_ids.include?(subscription.team_id)
136
+ end
137
+
138
+ false
139
+ when Hash
140
+ # For subscription parameters (during creation)
141
+ true # Basic creation is allowed, but user association will be enforced
142
+ else
143
+ false
144
+ end
145
+ end
146
+ end
147
+
148
+ module InstanceMethods
149
+ def authorize_access!(current_user)
150
+ self.class.authorize_subscription_management!(
151
+ current_user: current_user,
152
+ subscription: self
153
+ )
154
+ end
155
+
156
+ def authorized_for?(current_user)
157
+ self.class.authorize_subscription_access?(current_user, self)
158
+ rescue ActionWebPush::AuthorizationError
159
+ false
160
+ end
161
+
162
+ def authorize_notification_sending!(current_user)
163
+ self.class.authorize_notification_sending!(
164
+ current_user: current_user,
165
+ subscriptions: [self]
166
+ )
167
+ end
168
+ end
169
+
170
+ def self.included(base)
171
+ base.extend(ClassMethods)
172
+ base.include(InstanceMethods)
173
+ end
174
+
175
+ # Utility methods for common authorization patterns
176
+ module Utils
177
+ def self.current_user_context
178
+ # Try to get current user from various sources
179
+ if defined?(Current) && Current.respond_to?(:user)
180
+ Current.user
181
+ elsif defined?(RequestStore) && RequestStore.exist?(:current_user)
182
+ RequestStore.read(:current_user)
183
+ elsif Thread.current[:current_user]
184
+ Thread.current[:current_user]
185
+ else
186
+ nil
187
+ end
188
+ end
189
+
190
+ def self.with_authorization_context(user, &block)
191
+ previous_user = Thread.current[:current_user]
192
+ Thread.current[:current_user] = user
193
+ yield
194
+ ensure
195
+ Thread.current[:current_user] = previous_user
196
+ end
197
+
198
+ def self.bypass_authorization(&block)
199
+ previous_value = Thread.current[:action_web_push_bypass_auth]
200
+ Thread.current[:action_web_push_bypass_auth] = true
201
+ yield
202
+ ensure
203
+ Thread.current[:action_web_push_bypass_auth] = previous_value
204
+ end
205
+
206
+ def self.authorization_bypassed?
207
+ Thread.current[:action_web_push_bypass_auth] == true
208
+ end
209
+ end
210
+
211
+ # Configuration for authorization behavior
212
+ class Configuration
213
+ attr_accessor :enforce_user_ownership,
214
+ :allow_admin_override,
215
+ :allow_organization_access,
216
+ :allow_team_access,
217
+ :custom_authorization_proc
218
+
219
+ def initialize
220
+ @enforce_user_ownership = true
221
+ @allow_admin_override = true
222
+ @allow_organization_access = false
223
+ @allow_team_access = false
224
+ @custom_authorization_proc = nil
225
+ end
226
+ end
227
+
228
+ def self.configuration
229
+ @configuration ||= Configuration.new
230
+ end
231
+
232
+ def self.configure
233
+ yield(configuration) if block_given?
234
+ end
235
+ end
236
+ end