LibreSign / LibreSign/libresign
Define configurable rejection workflow and visibility policies
Nobody has claimed this yet.
- 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:
- resolve the current administrator policy;
- validate requester choices;
- calculate the effective configuration;
- 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
SIGNEDorREJECTED; - 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_choiceresolution;- 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
SIGNEDorREJECTEDresults inFileStatus::SIGNEDforcontinue.
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; canSigndoes not depend ondisplayStatus.
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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