diff --git a/app/models/submission.rb b/app/models/submission.rb index 5eae114b..56fd92cf 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -88,6 +88,14 @@ class Submission < ApplicationRecord scope :active, -> { where(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 { where(expire_at: nil).or(where(expire_at: Time.current..)) .where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) diff --git a/app/models/submitter.rb b/app/models/submitter.rb index 0e4bc6c2..55802042 100644 --- a/app/models/submitter.rb +++ b/app/models/submitter.rb @@ -68,6 +68,11 @@ class Submitter < ApplicationRecord 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? } def status diff --git a/lib/ability.rb b/lib/ability.rb index c485c091..cfd013d3 100644 --- a/lib/ability.rb +++ b/lib/ability.rb @@ -29,22 +29,51 @@ class Ability can :destroy, Template, account_id: user.account_id can :manage, TemplateFolder, 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, EncryptedConfig, account_id: user.account_id can :manage, AccountConfig, account_id: user.account_id can :manage, Account, id: user.account_id can :manage, McpToken, user_id: user.id can :manage, WebhookUrl, account_id: user.account_id - - can :manage, :mcp +Submission.visible_to(user) do |submission| + 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 def editor_abilities(user) can %i[read create update], Template, Abilities::TemplateConditions.collection(user) do |template| - Abilities::TemplateConditions.entity(template, user:, ability: 'manage') - end + Abilities::TemplateCoSubmission.visible_to(user) do |submission| + 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, TemplateSharing, template: { account_id: user.account_id } diff --git a/playwright/tests/v0.7.0-submission-visibility.spec.ts b/playwright/tests/v0.7.0-submission-visibility.spec.ts new file mode 100644 index 00000000..5b2df866 --- /dev/null +++ b/playwright/tests/v0.7.0-submission-visibility.spec.ts @@ -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(); + }); +});