Skip to content

Commit acd197d

Browse files
committed
Added: unsign form route (#75)
1 parent ebd75f5 commit acd197d

File tree

2 files changed

+130
-20
lines changed

2 files changed

+130
-20
lines changed

libreforms_fastapi/app/__init__.py

+93
Original file line numberDiff line numberDiff line change
@@ -1084,10 +1084,14 @@ async def api_form_sign(
10841084
except SignatureError as e:
10851085
raise HTTPException(status_code=403, detail=f"{e}")
10861086

1087+
except InsufficientPermissions as e:
1088+
raise HTTPException(status_code=403, detail=f"{e}")
10871089

10881090
except DocumentAlreadyHasValidSignature as e:
10891091
raise HTTPException(status_code=200, detail=f"{e}")
10901092

1093+
except NoChangesProvided as e:
1094+
raise HTTPException(status_code=200, detail=f"{e}")
10911095

10921096
# Send email
10931097
if config.SMTP_ENABLED:
@@ -1118,6 +1122,95 @@ async def api_form_sign(
11181122
"document_id": document_id,
11191123
}
11201124

1125+
1126+
@app.patch("/api/form/unsign/{form_name}/{document_id}", dependencies=[Depends(api_key_auth)])
1127+
async def api_form_sign(
1128+
form_name:str,
1129+
document_id:str,
1130+
background_tasks: BackgroundTasks,
1131+
request: Request,
1132+
session: SessionLocal = Depends(get_db),
1133+
key: str = Depends(X_API_KEY),
1134+
):
1135+
if form_name not in get_form_names():
1136+
raise HTTPException(status_code=404, detail=f"Form '{form_name}' not found")
1137+
1138+
# Ugh, I'd like to find a more efficient way to get the user data. But alas, that
1139+
# the sqlalchemy-signing table is not optimized alongside the user model...
1140+
user = session.query(User).filter_by(api_key=key).first()
1141+
1142+
# Here we validate the user groups permit this level of access to the form
1143+
try:
1144+
user.validate_permission(form_name=form_name, required_permission="sign_own")
1145+
except Exception as e:
1146+
raise HTTPException(status_code=403, detail=f"{e}")
1147+
1148+
metadata={
1149+
doc_db.last_editor_field: user.username,
1150+
}
1151+
1152+
# Add the remote addr host if enabled
1153+
if config.COLLECT_USAGE_STATISTICS:
1154+
metadata[doc_db.ip_address_field] = request.client.host
1155+
1156+
try:
1157+
# Process the request as needed
1158+
success = doc_db.sign_document(
1159+
form_name=form_name,
1160+
document_id=document_id,
1161+
metadata=metadata,
1162+
username=user.username,
1163+
public_key=user.public_key,
1164+
private_key_path=user.private_key_ref,
1165+
unsign=True,
1166+
)
1167+
1168+
# Unlike other methods, like get_one_document or fuzzy_search_documents, this method raises exceptions when
1169+
# it fails to ensure the user knows their operation was not successful.
1170+
except DocumentDoesNotExist as e:
1171+
raise HTTPException(status_code=404, detail=f"{e}")
1172+
1173+
except DocumentIsDeleted as e:
1174+
raise HTTPException(status_code=410, detail=f"{e}")
1175+
1176+
except SignatureError as e:
1177+
raise HTTPException(status_code=403, detail=f"{e}")
1178+
1179+
except InsufficientPermissions as e:
1180+
raise HTTPException(status_code=403, detail=f"{e}")
1181+
1182+
except NoChangesProvided as e:
1183+
raise HTTPException(status_code=200, detail=f"{e}")
1184+
1185+
# Send email
1186+
if config.SMTP_ENABLED:
1187+
background_tasks.add_task(
1188+
mailer.send_mail,
1189+
subject="Form Unsigned",
1190+
content=f"This email servers to notify you that a form was unsigned at {config.DOMAIN} by the user registered at this email address. The form's document ID is '{document_id}'. If you believe this was a mistake, or did not intend to sign this form, please contact your system administrator.",
1191+
to_address=user.email,
1192+
)
1193+
1194+
# Write this query to the TransactionLog
1195+
if config.COLLECT_USAGE_STATISTICS:
1196+
1197+
endpoint = request.url.path
1198+
remote_addr = request.client.host
1199+
1200+
background_tasks.add_task(
1201+
write_api_call_to_transaction_log,
1202+
api_key=key,
1203+
endpoint=endpoint,
1204+
remote_addr=remote_addr,
1205+
query_params={},
1206+
)
1207+
1208+
return {
1209+
"message": "Form successfully unsigned",
1210+
"document_id": document_id,
1211+
}
1212+
1213+
11211214
# Approve form
11221215
# This is a metadata-only field. It should not impact the data, just the metadata - namely, to afix
11231216
# an approval - in the format of a digital signature - to the form.

libreforms_fastapi/utils/document_database.py

+37-20
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ def update_document(
398398
if len(dropping_unchanged_data.keys()) == 0:
399399
raise NoChangesProvided(form_name, document_id)
400400

401-
print("\n\n\nDropping Unchanged Fields: ", dropping_unchanged_data)
401+
# print("\n\n\nDropping Unchanged Fields: ", dropping_unchanged_data)
402402

403403
# Build the journal
404404
journal = document['metadata'].get(self.journal_field)
@@ -441,6 +441,7 @@ def sign_document(
441441
metadata={},
442442
exclude_deleted=True,
443443
verify_on_sign=True,
444+
unsign=False,
444445
):
445446
"""
446447
Manage signatures existing form in specified form's database.
@@ -464,25 +465,41 @@ def sign_document(
464465
self.logger.warning(f"Document for {form_name} with document_id {document_id} is deleted and was not updated")
465466
raise DocumentIsDeleted(form_name, document_id)
466467

467-
# Before we even begin, we verify whether a signature exists and only proceed if it doesn't. Otherwise,
468-
# we raise a DocumentAlreadyHasValidSignature exception. The idea here is to avoid spamming signatures if
469-
# there has been no substantive change to the data since a past signature. This will allow the logic here
470-
# to proceed if there is no signature, or if the data has changed since the last signature.
471-
has_document_already_been_signed = verify_record_signature(record=document, username=username, env=self.env, public_key=public_key, private_key_path=private_key_path)
472-
473-
if has_document_already_been_signed:
474-
raise DocumentAlreadyHasValidSignature(form_name, document_id, username)
475-
476-
# Now we afix the signature
477-
try:
478-
r, signature = sign_record(record=document, username=username, env=self.env, private_key_path=private_key_path)
479-
480-
if verify_on_sign:
481-
verify = verify_record_signature(record=r, username=username, env=self.env, public_key=public_key, private_key_path=private_key_path)
482-
assert (verify)
483-
# print ("\n\n\n", a)
484-
except:
485-
raise SignatureError(form_name, document_id, username)
468+
if username != document['metadata'][self.created_by_field]:
469+
if self.use_logger:
470+
self.logger.warning(f"Insufficient permissions to {'unsign' if unsign else 'sign'} document for {form_name} with document_id {document_id}")
471+
raise InsufficientPermissions(form_name, document_id, username)
472+
473+
# If we are trying to unsign the document, then we remove the signature, update the document, and return.
474+
if unsign:
475+
476+
# If the document is not signed, raise a no changes exception
477+
if not document['metadata'][self.signature_field]:
478+
raise NoChangesProvided(form_name, document_id)
479+
480+
signature = None
481+
482+
else:
483+
484+
# Before we even begin, we verify whether a signature exists and only proceed if it doesn't. Otherwise,
485+
# we raise a DocumentAlreadyHasValidSignature exception. The idea here is to avoid spamming signatures if
486+
# there has been no substantive change to the data since a past signature. This will allow the logic here
487+
# to proceed if there is no signature, or if the data has changed since the last signature.
488+
has_document_already_been_signed = verify_record_signature(record=document, username=username, env=self.env, public_key=public_key, private_key_path=private_key_path)
489+
490+
if has_document_already_been_signed:
491+
raise DocumentAlreadyHasValidSignature(form_name, document_id, username)
492+
493+
# Now we afix the signature
494+
try:
495+
r, signature = sign_record(record=document, username=username, env=self.env, private_key_path=private_key_path)
496+
497+
if verify_on_sign:
498+
verify = verify_record_signature(record=r, username=username, env=self.env, public_key=public_key, private_key_path=private_key_path)
499+
assert (verify)
500+
# print ("\n\n\n", a)
501+
except:
502+
raise SignatureError(form_name, document_id, username)
486503

487504
current_timestamp = datetime.now(self.timezone)
488505

0 commit comments

Comments
 (0)