LibreSign / LibreSign/libresign

Define configurable rejection workflow and visibility policies

Open
#8,405 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
PHP
Stars
818
Forks
146
Avg merge
11h 31m
Merged PRs (30d)
326

Description

Context

#8388 and #8398 showed that rejection is not only a presentation problem.

LibreSign needs a clear contract for:

  • whether rejection is allowed;
  • what happens after a rejection;
  • whether a rejection comment is allowed or required;
  • who can see the rejection;
  • who can see the comment;
  • what the administrator may enforce;
  • what the requester may configure.

The rejection feature has not been released yet, so we can define this contract before release.

Goal

Use the existing LibreSign policy architecture so that:

  • administrators define allowed values and defaults;
  • requesters choose within those limits;
  • the final effective configuration is stored in policy_snapshot;
  • active workflows use the frozen configuration;
  • backend validation enforces all dependencies;
  • workflow state, presentation and signing capability remain separate.

Policy lifecycle

flowchart TD
    A[Administrator policy] --> B[Allowed values and defaults]
    B --> C[Requester configuration]
    C --> D[Validate]
    D --> E[Resolve effective configuration]
    E --> F[Store in policy_snapshot]
    F --> G[Signing workflow]

While the request is still a draft, the requester may change its rejection configuration.

Before the signing workflow starts:

  1. resolve the current administrator policy;
  2. validate requester choices;
  3. calculate the effective configuration;
  4. store it in policy_snapshot.

After the workflow starts, rejection settings are immutable.

Later administrator policy changes affect new workflows only.

A draft must be revalidated against the current administrator policy before it is sent.

Policy model

Use one scalar policy key per configurable setting, grouped as one rejection policy:

rejection.enabled
rejection.behavior
rejection.comment_mode
rejection.visibility
rejection.comment_visibility

Each key should use the existing value_choice resolution mode where applicable.

This allows each setting to have:

  • allowed values;
  • default value;
  • lower-layer override rules.

Do not keep rejection as one composite value where only enabled can be overridden.

Policy precedence

The administrator defines which values are allowed.

Example:

rejection.behavior:
    allowedValues:
        - cancel
    default: cancel

cancel is mandatory.

With:

rejection.behavior:
    allowedValues:
        - cancel
        - continue
    default: cancel

the requester may choose.

Do not create rejection-specific locking rules outside the existing policy system.

Cross-setting validation

Some rules depend on more than one setting.

Use a rejection-specific validator when:

  • administrator policy configuration is saved;
  • requester/file configuration is validated.

The validator must receive the final combined configuration.

Validation must not depend on the order in which individual scalar keys are saved.

Do not add a generic cross-policy validation hook to PolicySpec yet.

1. Enable rejection

rejection.enabled

Values:

true
false

If disabled, no other rejection setting applies.

The frontend should hide dependent settings.

The backend must reject conflicting configuration submitted directly by a client.

2. Rejection behavior

Available when rejection is enabled.

cancel
continue
cancel

The workflow stops after rejection.

No remaining signer may sign.

Signatures already completed remain valid.

The file/envelope uses the existing canceled workflow state.

continue

The rejected signer no longer blocks the workflow.

Other eligible signers may continue.

A rejected signer remains:

REJECTED

and must never be converted to SIGNED.

Completion with continue

When rejection.behavior = continue, the file may complete when:

  • every signer is either SIGNED or REJECTED;
  • at least one signer is SIGNED.

The file-level status becomes:

SIGNED

Example:

A -> SIGNED
B -> REJECTED
C -> SIGNED

FileStatus -> SIGNED

The file status represents that the configured workflow completed.

Individual signer states preserve the real result.

Do not add a new FileStatus only for completed workflows containing rejections.

3. Rejection comment mode

disabled
optional
required
  • disabled: no comment may be provided;
  • optional: a comment may be provided;
  • required: rejection requires a comment.

If disabled, comment visibility does not apply.

4. Rejection visibility

Use one visibility hierarchy for all response channels:

requester
participants
public

The levels are cumulative:

requester < participants < public
requester

Visible only to:

  • the requester;
  • the rejecting signer for their own signer entry.
participants

Visible to:

  • requester;
  • signers in the current signing workflow.
public

Visible to:

  • requester;
  • workflow participants;
  • public validation responses.

This setting defines the maximum audience that may receive rejection-specific information.

Do not use separate workflow and validation visibility settings.

A rejection cannot be hidden from participants while being exposed publicly.

5. Rejection comment visibility

Available when comments are enabled.

Use the same visibility hierarchy:

requester
participants
public

Comment visibility must never be broader than rejection visibility.

Valid:

rejection.visibility = participants
rejection.comment_visibility = requester

Valid:

rejection.visibility = public
rejection.comment_visibility = participants

Invalid:

rejection.visibility = requester
rejection.comment_visibility = participants

Invalid:

rejection.visibility = participants
rejection.comment_visibility = public

The comment must never reveal a rejection to an audience that cannot see the rejection itself.

Visibility rules by viewer

The effective rules are:

Viewer requester participants public
Requester visible visible visible
Rejecting signer, own entry visible visible visible
Other signer hidden visible visible
Authenticated non-participant hidden hidden only through public validation
Anonymous validation viewer hidden hidden visible

Public visibility means exposure through the validation/public response context.

It does not mean that unrelated authenticated users gain access to private signing workflow endpoints.

Existing endpoint authorization still applies.

Signer private comment

A signer may make their own rejection comment more private.

This may only reduce visibility.

If the signer marks the comment private:

effective comment visibility = requester

The comment remains visible to:

  • requester;
  • rejecting signer for their own rejection.

It must not be exposed to:

  • other signers;
  • public validation.

A signer must never be able to increase visibility beyond the configured workflow value.

Configuration dependencies

Setting Requires Rule
behavior rejection enabled cancel or continue
comment mode rejection enabled disabled, optional, required
rejection visibility rejection enabled requester, participants, public
comment visibility comments enabled cannot exceed rejection visibility
signer private comment comments enabled may restrict, never expand

These rules must be enforced in the backend.

The frontend may hide or disable invalid options, but frontend behavior is not authorization.

Requester UI

The configuration can be progressive:

Allow signers to reject
        |
        +-- No
        |
        +-- Yes
             |
             +-- What happens after rejection?
             |      ○ Cancel workflow
             |      ○ Continue with remaining signers
             |
             +-- Rejection comment
             |      ○ Disabled
             |      ○ Optional
             |      ○ Required
             |
             +-- Who can see the rejection?
             |      ○ Only me
             |      ○ Workflow participants
             |      ○ Public validation
             |
             +-- Who can see the comment?
                    ○ Only me
                    ○ Workflow participants
                    ○ Public validation

The available comment visibility values should be limited by the selected rejection visibility.

For example, if rejection visibility is participants, public must not be available for the comment.

Values enforced by administrator policy should be shown as enforced instead of appearing as unexplained disabled controls.

Freeze the resolved configuration

Use the existing policy_snapshot architecture.

The snapshot must contain the final resolved workflow configuration, not only the administrator policy.

Runtime code must not combine a current administrator policy with an old requester choice.

Example:

Day 1:
admin allows cancel + continue
requester selects continue
workflow starts

Resolved behavior:
continue

Day 2:
admin changes policy to cancel only

Existing workflow:
continue

New workflows:
cancel only

Do not recalculate an active workflow against the current administrator policy.

Draft revalidation

A draft is still configurable.

Before it is sent, its rejection configuration must be validated against the current administrator policy.

If the administrator changed the policy after the draft was created, values that are no longer allowed must be corrected before the workflow starts.

Envelope consistency

Rejection configuration is a workflow-level rule.

Envelope children must use the same effective rejection configuration.

A child added later must not independently resolve a newer administrator policy.

Reuse the existing snapshot/envelope resolution pattern from SignatureRejectionPolicyService.

Workflow state, presentation and capability

Keep these concepts separate.

Signer state

SignRequestStatus remains the real signer state:

DRAFT
ABLE_TO_SIGN
SIGNED
REJECTED
File state

FileStatus represents the complete workflow.

With continue, FileStatus::SIGNED may contain signer results with both:

SIGNED
REJECTED
Presentation

displayStatus represents what the current viewer may know.

It must follow rejection.visibility.

It must never be used for authorization.

Capability

settings.canSign represents whether the current viewer may act now.

The frontend must not infer signing permission from displayStatus.

Backend action validation remains authoritative.

For rejection-related paths:

REJECTED -> canSign false

and in sequential signing:

not current turn -> canSign false

without exposing why.

A broader canSign refactor is outside this issue.

Sequential signing

Example:

A -> order 1
B -> order 2

with:

rejection.behavior = continue

If A rejects, B may become eligible to sign.

With private rejection visibility, B may observe:

canSign: false -> true

This is allowed workflow progression.

LibreSign must not expose extra rejection-specific information only to explain that change.

Audit

The real rejection event must remain recorded regardless of visibility.

Privacy controls exposure, not whether the event exists.

The audit trail keeps the real signer result.

Notifications

Notifications must follow rejection.visibility and rejection.comment_visibility.

A signer must not receive rejection-specific information that they would not be allowed to see through the API.

The current rejection implementation mainly affects requester activity; this rule also applies to future signer-facing notifications such as #8161.

Response consistency

Apply the same visibility rules to all relevant response paths.

Review at least:

  • SignersLoader;
  • FileListService;
  • FileService;
  • EnvelopeAssembler;
  • validation responses;
  • signing page responses;
  • file list/sidebar responses.

The same viewer must not gain additional rejection information by switching between equivalent response paths.

Public validation follows the public visibility level.

Current rejection configuration

Replace or map the current rejection concepts:

enabled
comment_mode
cancel_workflow
public_status
show_comment_on_validation
rejectionCommentPrivate

Expected direction:

enabled
    -> rejection.enabled

cancel_workflow
    -> rejection.behavior

comment_mode
    -> rejection.comment_mode

public_status
    -> rejection.visibility

show_comment_on_validation
    -> rejection.comment_visibility

rejectionCommentPrivate
    -> signer-level comment restriction

The old public_status and show_comment_on_validation booleans are replaced by explicit audience levels.

Because the feature has not been released, update the model directly.

Do not add compatibility code for unreleased intermediate snapshot formats.

Impact on #8398

Keep #8398 open.

Most of its presentation work remains useful:

  • SignerDisplayStatus;
  • SignerPresentation;
  • centralized presentation logic;
  • integration in response paths;
  • existing visibility tests.

The source of the visibility decision changes from the old public_status boolean to the effective rejection.visibility.

The same presentation service should evaluate:

  • current viewer;
  • response context;
  • effective visibility level.

Implementation split

Prefer separate PRs:

1. Policy model
  • scalar rejection policy keys;
  • value_choice resolution;
  • compound grouping;
  • rejection-specific cross-setting validator;
  • draft revalidation;
  • snapshot read/write;
  • policy and snapshot tests.
2. Workflow behavior
  • cancel;
  • continue;
  • completion as FileStatus::SIGNED;
  • rejected signer cannot sign again;
  • rejection-related canSign;
  • behavior and capability tests.
3. Visibility

Adapt #8398 to:

  • requester visibility;
  • participant visibility;
  • public visibility;
  • comment visibility ceiling;
  • signer-private comment;
  • requester;
  • rejecting signer;
  • other signer;
  • authenticated non-participant;
  • anonymous validation viewer.

Then continue with the related UI and validation issues.

Tests

Cover at least:

Policy
  • instance policy;
  • group policy;
  • enforced values;
  • requester-overridable values;
  • forbidden requester values;
  • invalid cross-setting combinations.
Draft
  • draft created under an old policy;
  • administrator policy changes;
  • draft is revalidated before sending;
  • invalid old choice cannot start a workflow.
Snapshot
  • effective values frozen when the workflow starts;
  • administrator change does not affect an existing workflow;
  • new workflow uses the new policy;
  • envelope children keep the workflow configuration.
Behavior
  • rejection disabled;
  • cancel workflow;
  • continue workflow;
  • simultaneous signing;
  • sequential signing;
  • rejected signer cannot sign again;
  • existing signatures remain valid after another signer rejects;
  • all remaining signers resolved as SIGNED or REJECTED results in FileStatus::SIGNED for continue.
Comments
  • disabled;
  • optional;
  • required;
  • signer-private comment;
  • comment visibility never exceeds rejection visibility.
Visibility

Cover:

  • requester;
  • rejecting signer;
  • another signer;
  • authenticated non-participant;
  • anonymous validation viewer.

Test all visibility levels:

requester
participants
public

Verify both rejection status and rejection comment.

Capability

Verify:

  • eligible signer gets canSign: true;
  • signer outside their sequential turn gets canSign: false;
  • rejected signer gets canSign: false;
  • canSign does not depend on displayStatus.
Response isolation

Generate responses for different viewers in sequence and verify that privileged data does not leak into another viewer's response.

Design reference

Other signing systems commonly allow administrators or senders to configure rejection-related behavior.

Most systems reviewed treat decline as terminal for the signing transaction.

LibreSign intentionally also supports:

rejection.behavior = continue

for workflows where one signer rejecting should not prevent remaining signers from continuing.

This behavior must be explicit in the workflow configuration instead of being an implicit side effect of REJECTED.

Existing work

#8388 defines the current rejection visibility contract.

#8398 implements that contract and helped expose the need for this broader policy definition.

After the policy model is implemented, adapt #8398 to the final visibility contract.

Related

  • #8388
  • #8398
  • #8160
  • #8161
  • #8162

Backport

None.

The rejection feature has not been released yet.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the existing policy_snapshot architecture and SignatureRejectionPolicyService, then trace how envelope resolution, SignRequestStatus, FileStatus, displayStatus, and settings.canSign currently interact. Define the rejection policy lifecycle, combined validation, visibility rules, draft revalidation, and frozen workflow configuration; done means active workflows use their snapshot while backend actions enforce the stated dependencies.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend, frontend, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.