Module: Webhookdb::Service::Helpers

Extended by:
Grape::API::Helpers
Includes:
Collection::Helpers
Defined in:
lib/webhookdb/service/helpers.rb

Overview

A collection of helper functions that can be included

Instance Method Summary collapse

Methods included from Collection::Helpers

#present_collection

Instance Method Details

#_check_customer_deleted(user, potential_admin) ⇒ Object

Handle denying authentication if the given user cannot auth. That is:

  • if we have an admin, but they should not be (deleted or missing role), throw unauthed error.

  • if current user is nil, return nil, since the caller can handle it.

  • if current user is deleted and there is no admin, throw unauthed error.

  • if current user is deleted and admin is deleted, throw unauthed error.

  • otherwise, return current user.

The scenarios this covers are:

  • Normal users cannot auth if deleted.

  • Admins can sudo deleted users, and current_customer still works.

  • Deleted admins cannot auth or get their sudo’ed user.

NOTE: It is safe to throw unauthed errors for deleted users- this does not expose whether a user exists or not, because the only way to call this is via cookies, and cookies are encrypted. So it is impossible to force requests trying to auth/check auth for a user without knowing the secret.



64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/webhookdb/service/helpers.rb', line 64

def _check_customer_deleted(user, potential_admin)
  return nil if user.nil?
  if potential_admin && (potential_admin.soft_deleted? || !potential_admin.roles.include?(Webhookdb::Role.admin_role))
    delete_session_cookies
    unauthenticated!
  end
  if user.soft_deleted? && potential_admin.nil?
    delete_session_cookies
    unauthenticated!
  end
  return user
end

#admin_customerObject



31
32
33
# File 'lib/webhookdb/service/helpers.rb', line 31

def admin_customer
  return _check_customer_deleted(env["warden"].authenticate!(scope: :admin), nil)
end

#admin_customer?Boolean

Returns:

  • (Boolean)


35
36
37
# File 'lib/webhookdb/service/helpers.rb', line 35

def admin_customer?
  return _check_customer_deleted(env["warden"].authenticate(scope: :admin), nil)
end

#authenticate!Object



39
40
41
42
43
44
# File 'lib/webhookdb/service/helpers.rb', line 39

def authenticate!
  warden = env["warden"]
  user = warden.authenticate!(scope: :customer)
  warden.set_user(user, scope: :admin) if user.admin?
  return user
end

#check_feature_access!(org, role) ⇒ Object



145
146
147
148
# File 'lib/webhookdb/service/helpers.rb', line 145

def check_feature_access!(org, role)
  return if org.feature_roles.include?(role)
  permission_error!("This feature is not enabled for your organization.")
end

#check_role!(customer, role_name) ⇒ Object



97
98
99
100
101
102
103
# File 'lib/webhookdb/service/helpers.rb', line 97

def check_role!(customer, role_name)
  has_role = customer.roles.find { |r| r.name == role_name }
  return if has_role
  role_exists = !Webhookdb::Role.where(name: role_name).empty?
  raise "The role '#{role_name}' does not exist so cannot be checked. You need to create it first." unless role_exists
  permission_error!("Sorry, this action is unavailable.")
end

#current_customerObject

Return the currently-authenticated user, or respond with a 401 if there is no authenticated user.



21
22
23
# File 'lib/webhookdb/service/helpers.rb', line 21

def current_customer
  return _check_customer_deleted(env["warden"].authenticate!(scope: :customer), admin_customer?)
end

#current_customer?Boolean

Return the currently-authenticated user, or respond nil if there is no authenticated user.

Returns:

  • (Boolean)


27
28
29
# File 'lib/webhookdb/service/helpers.rb', line 27

def current_customer?
  return _check_customer_deleted(env["warden"].user(scope: :customer), admin_customer?)
end

#current_session_idObject



93
94
95
# File 'lib/webhookdb/service/helpers.rb', line 93

def current_session_id
  return env["rack.session"].id
end

#declared_and_provided_params(params, exclude: []) ⇒ Object



226
227
228
229
230
231
# File 'lib/webhookdb/service/helpers.rb', line 226

def declared_and_provided_params(params, exclude: [])
  decl = declared(params)
  exclude.each { |k| decl.delete(k) }
  decl.delete_if { |k| !params.key?(k) }
  return decl
end

#delete_session_cookiesObject



77
78
79
80
81
82
83
84
85
# File 'lib/webhookdb/service/helpers.rb', line 77

def delete_session_cookies
  # Nope, cannot do this through Warden easily.
  # And really we should have server-based sessions we can expire,
  # but in the meantime, stomp on the cookie hard.
  options = env[Rack::RACK_SESSION_OPTIONS]
  options[:drop] = true
  # Rack sends a cookie with an empty session, but let's tell the browser to actually delete the cookie.
  cookies.delete(Webhookdb::Service::SESSION_COOKIE, domain: options[:domain], path: options[:path])
end

#endpoint_removed!Object



170
171
172
173
174
175
176
177
# File 'lib/webhookdb/service/helpers.rb', line 170

def endpoint_removed!
  merror!(
    403,
    "Sorry, this endpoint has been removed. Run `webhookdb update` to upgrade your CLI, " \
    "or file a ticket at #{Webhookdb.oss_repo_url} for help.",
    code: "endpoint_removed",
  )
end

#forbidden!Object



133
134
135
# File 'lib/webhookdb/service/helpers.rb', line 133

def forbidden!
  merror!(403, "Forbidden", code: "forbidden")
end

#invalid!(errors, message: nil) ⇒ Object

Raise a 400 error for unstructured validation.

Parameters:

  • errors (Array<String>, String)

    Error messages, like ‘password is invalid’.

  • message (String) (defaults to: nil)

    If not given, build it from the errors list.



153
154
155
156
157
# File 'lib/webhookdb/service/helpers.rb', line 153

def invalid!(errors, message: nil)
  errors = [errors] unless errors.respond_to?(:to_ary)
  message ||= errors.join(", ").upcase_first
  merror!(400, message, code: "validation_error", more: {errors:, field_errors: {}})
end

#invalid_fields!(field_errors, message: nil) ⇒ Object

Raise a 400 error for structured validation.

Parameters:

  • field_errors (Hash<String, Array<String>>)

    If errors are tied to fields, this is a hash where the key is the field name, and the value is an array of all validation messages. For example, [‘is invalid’]

  • message (String) (defaults to: nil)

    If not given, build it from the errors list.



164
165
166
167
168
# File 'lib/webhookdb/service/helpers.rb', line 164

def invalid_fields!(field_errors, message: nil)
  errors = field_errors.map { |field, field_errs| field_errs.map { |e| "#{field} #{e}" } }.flatten
  message ||= errors.join(", ").upcase_first
  merror!(400, message, code: "validation_error", more: {errors:, field_errors:})
end

#loggerObject



15
16
17
# File 'lib/webhookdb/service/helpers.rb', line 15

def logger
  return Webhookdb::Service.logger
end

#merror!(status, message, code: nil, more: {}, headers: {}, rollback_db: nil, alert: false) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/webhookdb/service/helpers.rb', line 105

def merror!(status, message, code: nil, more: {}, headers: {}, rollback_db: nil, alert: false)
  header "Content-Type", "application/json"
  body = Webhookdb::Service.error_body(status, message, code:, more:)
  if alert
    Sentry.with_scope do |scope|
      scope&.set_extras(**body)
      Sentry.capture_message(message)
    end
  end
  if rollback_db
    Webhookdb::Postgres.defer_after_rollback(rollback_db) do
      error!(body, status, headers)
    end
    raise Sequel::Rollback
  else
    error!(body, status, headers)
  end
end

#not_found!Object



137
138
139
# File 'lib/webhookdb/service/helpers.rb', line 137

def not_found!
  merror!(404, "Not Found", code: "not_found")
end

#order(dataset, params) ⇒ Object



201
202
203
204
# File 'lib/webhookdb/service/helpers.rb', line 201

def order(dataset, params)
  expr = params[:order_direction] == :asc ? Sequel.asc(params[:order_by]) : Sequel.desc(params[:order_by])
  return dataset.order(expr, Sequel.desc(:id))
end

#paginate(dataset, params) ⇒ Object



197
198
199
# File 'lib/webhookdb/service/helpers.rb', line 197

def paginate(dataset, params)
  return dataset.paginate(params[:page], params[:per_page])
end

#permission_error!(message) ⇒ Object



141
142
143
# File 'lib/webhookdb/service/helpers.rb', line 141

def permission_error!(message)
  merror!(403, message, code: "permission_check")
end

#save_or_error!(object) ⇒ Object

If object is valid, save and return it. If not, call invalid! witht the validation errors.



188
189
190
191
192
193
194
195
# File 'lib/webhookdb/service/helpers.rb', line 188

def save_or_error!(object)
  if object.valid?
    object.save_changes
    return object
  else
    invalid_fields!(object.errors.to_h)
  end
end

#search_param_to_sql(params, column, param: :search) ⇒ Object



179
180
181
182
183
184
# File 'lib/webhookdb/service/helpers.rb', line 179

def search_param_to_sql(params, column, param: :search)
  search = params[param]&.strip
  return nil if search.blank? || search == "*"
  term = "%#{search.strip}%"
  return Sequel.ilike(column, term)
end

#set_customer(customer) ⇒ Object



87
88
89
90
91
# File 'lib/webhookdb/service/helpers.rb', line 87

def set_customer(customer)
  warden = env["warden"]
  warden.set_user(customer, scope: :customer)
  warden.set_user(customer, scope: :admin) if customer.admin?
end

#set_declared(model, params, ignore: [:id]) ⇒ Object

Set the provided, declared/valid parameters in params on model. Because Grape’s ‘declared()` function adds parameters that are declared-but-not-provided, and its `params` value includes provided-but-not-declared entries, the fields we set are the intersection of the two.



216
217
218
219
220
221
222
223
224
# File 'lib/webhookdb/service/helpers.rb', line 216

def set_declared(model, params, ignore: [:id])
  # If .to_h is used (rather than Grape's 'params' which is HashWithIndifferentAccess),
  # the keys may be strings. We need to deep symbolize since nested hashes get to_h with 'symbolize_keys'.
  params = params.deep_symbolize_keys
  decl = declared_and_provided_params(params, exclude: ignore)
  ignore.each { |k| decl.delete(k) }
  decl.delete_if { |k| !params.key?(k) }
  model.set(decl)
end

#unauthenticated!Object



124
125
126
# File 'lib/webhookdb/service/helpers.rb', line 124

def unauthenticated!
  merror!(401, "Unauthenticated", code: "unauthenticated")
end

#unauthenticated_with_message!(msg) ⇒ Object



128
129
130
131
# File 'lib/webhookdb/service/helpers.rb', line 128

def unauthenticated_with_message!(msg)
  env["webhookdb.authfailuremessage"] = msg
  unauthenticated!
end

#use_http_expires_caching(expiration) ⇒ Object



206
207
208
209
210
# File 'lib/webhookdb/service/helpers.rb', line 206

def use_http_expires_caching(expiration)
  return unless Webhookdb::Service.endpoint_caching
  header "Cache-Control", "public"
  header "Expires", expiration.from_now.httpdate
end