mirror of https://github.com/docusealco/docuseal
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
83 lines
2.7 KiB
83 lines
2.7 KiB
# frozen_string_literal: true
|
|
|
|
# Confines a token-bootstrapped embed session (see EmbedBuilderController) to
|
|
# the single template it was issued for.
|
|
#
|
|
# A valid first-party DocuSeal session is necessary but not sufficient: the
|
|
# session may only reach its own template (by id, or by the immutable
|
|
# external_id for the /new -> create -> edit redirect) plus the create +
|
|
# builder-support paths the editor needs. Listing / enumeration / other
|
|
# templates / submissions / the JSON template API are refused even with a
|
|
# valid session, so one embedder can't browse another's documents in the
|
|
# shared DocuSeal account. Fails closed.
|
|
#
|
|
# The path rules mirror the allow-list the embedding app previously enforced
|
|
# at the edge via Caddy `forward_auth`; moving them in-process lets the embed
|
|
# drop the cross-origin cookie + gate machinery entirely.
|
|
module EmbedScoped
|
|
extend ActiveSupport::Concern
|
|
|
|
SESSION_KEY = 'embed_scope'
|
|
SESSION_TTL = 2.hours
|
|
|
|
TEMPLATE_PATH = %r{\A/templates/(\d+)(?:[/?]|\z)}
|
|
|
|
# Paths an embed session may hit regardless of which template it owns: the
|
|
# create/upload flow, the signing/asset surfaces, and the few endpoints the
|
|
# builder's own browser code calls. None expose another account's templates.
|
|
UNSCOPED_ALLOW = [
|
|
%r{\A/new(?:[/?]|\z)},
|
|
%r{\A/templates/new(?:[/?]|\z)},
|
|
%r{\A/templates_uploads?(?:[/?]|\z)},
|
|
%r{\A/(?:s|d|p|e)(?:[/?]|\z)},
|
|
%r{\A/(?:preview|file|blobs_proxy|submit_form)/},
|
|
%r{\A/verify_pdf_signature(?:[/?]|\z)},
|
|
%r{\A/api/(?:attachments|submitter_email_clicks|submitter_form_views)(?:[/?]|\z)}
|
|
].freeze
|
|
|
|
included do
|
|
before_action :enforce_embed_scope!
|
|
end
|
|
|
|
private
|
|
|
|
def enforce_embed_scope!
|
|
scope = session[SESSION_KEY]
|
|
return if scope.blank?
|
|
|
|
if embed_scope_expired?(scope)
|
|
reset_session
|
|
|
|
return redirect_to(new_user_session_path)
|
|
end
|
|
|
|
head(:forbidden) unless embed_path_authorized?(request.path, scope)
|
|
end
|
|
|
|
def embed_scope_expired?(scope)
|
|
exp = scope['exp']
|
|
|
|
exp.present? && exp.to_i <= Time.now.to_i
|
|
end
|
|
|
|
def embed_path_authorized?(path, scope)
|
|
if (match = TEMPLATE_PATH.match(path))
|
|
return embed_template_in_scope?(match[1].to_i, scope)
|
|
end
|
|
|
|
UNSCOPED_ALLOW.any? { |re| re.match?(path) }
|
|
end
|
|
|
|
# A specific /templates/:id is in scope when the session names that template
|
|
# directly, or when the template's immutable external_id matches the one the
|
|
# token was issued for (covers /new -> create -> redirect to /templates/:id).
|
|
def embed_template_in_scope?(id, scope)
|
|
return true if scope['template_id'].present? && scope['template_id'].to_i == id
|
|
|
|
external_id = scope['external_id']
|
|
return false if external_id.blank?
|
|
|
|
Template.where(id:).pick(:external_id) == external_id
|
|
end
|
|
end
|