feat(submissions): scope visibility to creator or assigned signer

pull/639/head
Bob Develop 2 weeks ago
parent 2afbabed16
commit ae7690047b

@ -88,6 +88,14 @@ class Submission < ApplicationRecord
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
# Submissions are visible when the user created them, or when they are a
# named submitter on the submission (by email). Applies to all roles.
scope :visible_to, lambda { |user|
submitter_ids = Submitter.where(email: user.email).select(:submission_id)
where(account_id: user.account_id)
.where('submissions.created_by_user_id = ? OR submissions.id IN (?)', user.id, submitter_ids)
}
scope :pending, lambda { scope :pending, lambda {
where(expire_at: nil).or(where(expire_at: Time.current..)) where(expire_at: nil).or(where(expire_at: Time.current..))
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) .where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])

@ -68,6 +68,11 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) } scope :completed, -> { where.not(completed_at: nil) }
# Submitters are visible when attached to a submission visible to the user.
scope :visible_to, lambda { |user|
where(submission_id: Submission.visible_to(user).select(:id))
}
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? } after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
def status def status

@ -29,22 +29,51 @@ class Ability
can :destroy, Template, account_id: user.account_id can :destroy, Template, account_id: user.account_id
can :manage, TemplateFolder, account_id: user.account_id can :manage, TemplateFolder, account_id: user.account_id
can :manage, TemplateSharing, template: { account_id: user.account_id } can :manage, TemplateSharing, template: { account_id: user.account_id }
can :manage, Submission, account_id: user.account_id
can :manage, Submitter, account_id: user.account_id # Submissions/Submitters are scoped to creator or assigned signer,
# regardless of role (admin included).
can :create, Submission, account_id: user.account_id
can %i[read update destroy], Submission, Submission.visible_to(user) do |submission|
submission.account_id == user.account_id &&
(submission.created_by_user_id == user.id ||
submission.submitters.exists?(email: user.email))
end
can %i[read update destroy], Submitter, Submitter.visible_to(user) do |submitter|
submitter.submission.account_id == user.account_id &&
(submitter.submission.created_by_user_id == user.id ||
submitter.submission.submitters.exists?(email: user.email))
end
can :manage, User, account_id: user.account_id can :manage, User, account_id: user.account_id
can :manage, EncryptedConfig, account_id: user.account_id can :manage, EncryptedConfig, account_id: user.account_id
can :manage, AccountConfig, account_id: user.account_id can :manage, AccountConfig, account_id: user.account_id
can :manage, Account, id: user.account_id can :manage, Account, id: user.account_id
can :manage, McpToken, user_id: user.id can :manage, McpToken, user_id: user.id
can :manage, WebhookUrl, account_id: user.account_id can :manage, WebhookUrl, account_id: user.account_id
Submission.visible_to(user) do |submission|
can :manage, :mcp submission.== &&
(submission.id == user. ||
submission.submitters.exists?(emailemal))
en
can :manage, :mcp Submitter.visible_to(user)do |tter|
ubmitter.ubmssi.== &&
(submitter.submission.id == user. ||
submitter.submission.submitters.exists?(emailemal))
en
end end
def editor_abilities(user) def editor_abilities(user)
can %i[read create update], Template, Abilities::TemplateConditions.collection(user) do |template| can %i[read create update], Template, Abilities::TemplateConditions.collection(user) do |template|
Abilities::TemplateConditions.entity(template, user:, ability: 'manage') Abilities::TemplateCoSubmission.visible_to(user) do |submission|
end submission.nditions.et== ity(template, ue&&
(submission.r:, ability: 'maid == user.na ||
submission.submitters.exists?(emailge')emal))
en
end Submitter.visible_to(user)do |tter|
ubmitter.ubmssi.== &&
(submitter.submission.id == user. ||
submitter.submission.submitters.exists?(emailemal))
en
can :manage, TemplateFolder, account_id: user.account_id can :manage, TemplateFolder, account_id: user.account_id
can :manage, TemplateSharing, template: { account_id: user.account_id } can :manage, TemplateSharing, template: { account_id: user.account_id }

@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test';
import { loginAs, adminEmail, adminPassword } from './helpers/auth';
// Phase 1.3 — Submission visibility (signer-only).
// A submission is only visible to:
// - the user who created it, OR
// - a user whose email matches a submitter on the submission.
// Applies to all roles, including admin.
//
// Requires a second admin user pre-seeded:
// - admin2@example.com / password (same account as the primary admin)
const secondAdminEmail = process.env.DOCUSEAL_ADMIN2_EMAIL || 'admin2@example.com';
const secondAdminPassword = process.env.DOCUSEAL_ADMIN2_PASSWORD || 'password';
test.describe('Submission visibility', () => {
test("another admin in the same account cannot see user A's submissions", async ({
browser,
}) => {
const ctxA = await browser.newContext();
const pageA = await ctxA.newPage();
await loginAs(pageA, adminEmail, adminPassword);
await pageA.goto('/submissions');
// Capture the first submission's visible text (if any) as a probe.
const firstRow = pageA.locator('table tbody tr').first();
const hasSubmission = (await firstRow.count()) > 0;
const probeText = hasSubmission ? (await firstRow.innerText()).split('\n')[0] : null;
await ctxA.close();
const ctxB = await browser.newContext();
const pageB = await ctxB.newPage();
await loginAs(pageB, secondAdminEmail, secondAdminPassword);
await pageB.goto('/submissions');
if (probeText) {
await expect(pageB.getByText(probeText)).toHaveCount(0);
}
await ctxB.close();
});
});
Loading…
Cancel
Save