Skip to content

Commit

Permalink
Added: unsign form route (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
signebedi committed Mar 27, 2024
1 parent ebd75f5 commit acd197d
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 20 deletions.
93 changes: 93 additions & 0 deletions libreforms_fastapi/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,10 +1084,14 @@ async def api_form_sign(
except SignatureError as e:
raise HTTPException(status_code=403, detail=f"{e}")

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

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

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

# Send email
if config.SMTP_ENABLED:
Expand Down Expand Up @@ -1118,6 +1122,95 @@ async def api_form_sign(
"document_id": document_id,
}


@app.patch("/api/form/unsign/{form_name}/{document_id}", dependencies=[Depends(api_key_auth)])
async def api_form_sign(
form_name:str,
document_id:str,
background_tasks: BackgroundTasks,
request: Request,
session: SessionLocal = Depends(get_db),
key: str = Depends(X_API_KEY),
):
if form_name not in get_form_names():
raise HTTPException(status_code=404, detail=f"Form '{form_name}' not found")

# Ugh, I'd like to find a more efficient way to get the user data. But alas, that
# the sqlalchemy-signing table is not optimized alongside the user model...
user = session.query(User).filter_by(api_key=key).first()

# Here we validate the user groups permit this level of access to the form
try:
user.validate_permission(form_name=form_name, required_permission="sign_own")
except Exception as e:
raise HTTPException(status_code=403, detail=f"{e}")

metadata={
doc_db.last_editor_field: user.username,
}

# Add the remote addr host if enabled
if config.COLLECT_USAGE_STATISTICS:
metadata[doc_db.ip_address_field] = request.client.host

try:
# Process the request as needed
success = doc_db.sign_document(
form_name=form_name,
document_id=document_id,
metadata=metadata,
username=user.username,
public_key=user.public_key,
private_key_path=user.private_key_ref,
unsign=True,
)

# Unlike other methods, like get_one_document or fuzzy_search_documents, this method raises exceptions when
# it fails to ensure the user knows their operation was not successful.
except DocumentDoesNotExist as e:
raise HTTPException(status_code=404, detail=f"{e}")

except DocumentIsDeleted as e:
raise HTTPException(status_code=410, detail=f"{e}")

except SignatureError as e:
raise HTTPException(status_code=403, detail=f"{e}")

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

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

# Send email
if config.SMTP_ENABLED:
background_tasks.add_task(
mailer.send_mail,
subject="Form Unsigned",
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.",
to_address=user.email,
)

# Write this query to the TransactionLog
if config.COLLECT_USAGE_STATISTICS:

endpoint = request.url.path
remote_addr = request.client.host

background_tasks.add_task(
write_api_call_to_transaction_log,
api_key=key,
endpoint=endpoint,
remote_addr=remote_addr,
query_params={},
)

return {
"message": "Form successfully unsigned",
"document_id": document_id,
}


# Approve form
# This is a metadata-only field. It should not impact the data, just the metadata - namely, to afix
# an approval - in the format of a digital signature - to the form.
Expand Down
57 changes: 37 additions & 20 deletions libreforms_fastapi/utils/document_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ def update_document(
if len(dropping_unchanged_data.keys()) == 0:
raise NoChangesProvided(form_name, document_id)

print("\n\n\nDropping Unchanged Fields: ", dropping_unchanged_data)
# print("\n\n\nDropping Unchanged Fields: ", dropping_unchanged_data)

# Build the journal
journal = document['metadata'].get(self.journal_field)
Expand Down Expand Up @@ -441,6 +441,7 @@ def sign_document(
metadata={},
exclude_deleted=True,
verify_on_sign=True,
unsign=False,
):
"""
Manage signatures existing form in specified form's database.
Expand All @@ -464,25 +465,41 @@ def sign_document(
self.logger.warning(f"Document for {form_name} with document_id {document_id} is deleted and was not updated")
raise DocumentIsDeleted(form_name, document_id)

# Before we even begin, we verify whether a signature exists and only proceed if it doesn't. Otherwise,
# we raise a DocumentAlreadyHasValidSignature exception. The idea here is to avoid spamming signatures if
# there has been no substantive change to the data since a past signature. This will allow the logic here
# to proceed if there is no signature, or if the data has changed since the last signature.
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)

if has_document_already_been_signed:
raise DocumentAlreadyHasValidSignature(form_name, document_id, username)

# Now we afix the signature
try:
r, signature = sign_record(record=document, username=username, env=self.env, private_key_path=private_key_path)

if verify_on_sign:
verify = verify_record_signature(record=r, username=username, env=self.env, public_key=public_key, private_key_path=private_key_path)
assert (verify)
# print ("\n\n\n", a)
except:
raise SignatureError(form_name, document_id, username)
if username != document['metadata'][self.created_by_field]:
if self.use_logger:
self.logger.warning(f"Insufficient permissions to {'unsign' if unsign else 'sign'} document for {form_name} with document_id {document_id}")
raise InsufficientPermissions(form_name, document_id, username)

# If we are trying to unsign the document, then we remove the signature, update the document, and return.
if unsign:

# If the document is not signed, raise a no changes exception
if not document['metadata'][self.signature_field]:
raise NoChangesProvided(form_name, document_id)

signature = None

else:

# Before we even begin, we verify whether a signature exists and only proceed if it doesn't. Otherwise,
# we raise a DocumentAlreadyHasValidSignature exception. The idea here is to avoid spamming signatures if
# there has been no substantive change to the data since a past signature. This will allow the logic here
# to proceed if there is no signature, or if the data has changed since the last signature.
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)

if has_document_already_been_signed:
raise DocumentAlreadyHasValidSignature(form_name, document_id, username)

# Now we afix the signature
try:
r, signature = sign_record(record=document, username=username, env=self.env, private_key_path=private_key_path)

if verify_on_sign:
verify = verify_record_signature(record=r, username=username, env=self.env, public_key=public_key, private_key_path=private_key_path)
assert (verify)
# print ("\n\n\n", a)
except:
raise SignatureError(form_name, document_id, username)

current_timestamp = datetime.now(self.timezone)

Expand Down

0 comments on commit acd197d

Please sign in to comment.