add webauthn crud endpoints

added some simple validation to the delete endpoint for sanity, but
generally my assumption is that more validation will happen on the admin
side.

noteably im not checking whether the credentials are duplicated, nor is
there a uniqueness constraint in the database - I'm not sure if the
credential blob will always reliably be equivalent, and I believe the
browser should hopefully take care of dupes.
This commit is contained in:
Leo Hemsted
2021-05-10 22:09:07 +01:00
parent 500feba50d
commit e62e050963
8 changed files with 336 additions and 0 deletions

View File

@@ -74,6 +74,7 @@ from app.models import (
Template,
TemplateFolder,
User,
WebauthnCredential,
)
@@ -1202,3 +1203,22 @@ def create_broadcast_provider_message(
db.session.add(provider_message_number)
db.session.commit()
return provider_message
def create_webauthn_credential(
user,
name='my key',
*,
credential_data='ABC123',
registration_response='DEF456',
):
webauthn_credential = WebauthnCredential(
user=user,
name=name,
credential_data=credential_data,
registration_response=registration_response
)
db.session.add(webauthn_credential)
db.session.commit()
return webauthn_credential

View File

@@ -0,0 +1,173 @@
import uuid
from unittest.mock import ANY
from tests.app.db import create_user, create_webauthn_credential
def test_get_webauthn_credentials_returns_all_credentials_for_user(admin_request, notify_db_session):
me = create_user(email='a')
other = create_user(email='b')
first = create_webauthn_credential(me, '1')
create_webauthn_credential(me, '2')
create_webauthn_credential(other, '3')
response = admin_request.get(
'webauthn.get_webauthn_credentials',
user_id=me.id,
)
creds = sorted(response['data'], key=lambda x: x['name'])
assert len(creds) == 2
assert creds[0] == {
'id': str(first.id),
'user_id': str(me.id),
'name': '1',
'credential_data': 'ABC123',
'registration_response': 'DEF456',
'created_at': ANY,
'updated_at': None
}
assert creds[1]['name'] == '2'
def test_get_webauthn_credentials_returns_empty_list_if_no_creds(admin_request, sample_user):
response = admin_request.get('webauthn.get_webauthn_credentials', user_id=sample_user.id)
assert response == {'data': []}
def test_get_webauthn_credentials_errors_if_user_doesnt_exist(admin_request, sample_user):
create_webauthn_credential(sample_user, '1')
admin_request.get(
'webauthn.get_webauthn_credentials',
user_id=uuid.uuid4(),
_expected_status=404
)
def test_create_webauthn_credential_returns_201(admin_request, sample_user):
response = admin_request.post(
'webauthn.create_webauthn_credential',
user_id=sample_user.id,
_data={
'name': 'my key',
'credential_data': 'ABC123',
'registration_response': 'DEF456',
},
_expected_status=201
)
assert len(sample_user.webauthn_credentials) == 1
new_cred = sample_user.webauthn_credentials[0]
assert new_cred.name == 'my key'
assert new_cred.credential_data == 'ABC123'
assert new_cred.registration_response == 'DEF456'
assert response['data']['id'] == str(new_cred.id)
def test_create_webauthn_credential_errors_if_schema_violation(admin_request, sample_user):
response = admin_request.post(
'webauthn.create_webauthn_credential',
user_id=sample_user.id,
_data={
'name': 'my key',
'credential_data': 'ABC123',
# missing registration_response
},
_expected_status=400
)
assert response['errors'][0] == {
'error': 'ValidationError',
'message': 'registration_response is a required property'
}
def test_update_webauthn_credential_returns_200(admin_request, sample_user):
cred = create_webauthn_credential(sample_user)
assert cred.name != 'new name'
response = admin_request.post(
'webauthn.update_webauthn_credential',
user_id=sample_user.id,
webauthn_credential_id=cred.id,
_data={
'name': 'new name',
},
)
assert response['data']['id'] == str(cred.id)
assert response['data']['name'] == 'new name'
def test_update_webauthn_credential_errors_if_schema_violation(admin_request, sample_user):
cred = create_webauthn_credential(sample_user)
response = admin_request.post(
'webauthn.update_webauthn_credential',
user_id=sample_user.id,
webauthn_credential_id=cred.id,
_data={
'name': 'my key',
# you can't update credential_data
'credential_data': 'NAUGHTY123'
},
_expected_status=400
)
assert response['errors'][0] == {
'error': 'ValidationError',
'message': 'Additional properties are not allowed (credential_data was unexpected)'
}
def test_update_webauthn_credential_errors_if_webauthn_credential_doesnt_exist(admin_request, sample_user):
admin_request.post(
'webauthn.update_webauthn_credential',
user_id=sample_user.id,
webauthn_credential_id=uuid.uuid4(),
_data={
'name': 'my key',
},
_expected_status=404
)
def test_delete_webauthn_credential_returns_204(admin_request, sample_user):
cred1 = create_webauthn_credential(sample_user)
cred2 = create_webauthn_credential(sample_user)
admin_request.delete(
'webauthn.update_webauthn_credential',
user_id=sample_user.id,
webauthn_credential_id=cred1.id,
_expected_status=204
)
assert sample_user.webauthn_credentials == [cred2]
def test_delete_webauthn_credential_errors_if_last_key(admin_request, sample_user):
cred = create_webauthn_credential(sample_user)
response = admin_request.delete(
'webauthn.delete_webauthn_credential',
user_id=sample_user.id,
webauthn_credential_id=cred.id,
_expected_status=400
)
assert response['message'] == 'Cannot delete last remaining webauthn credential for user'
def test_delete_webauthn_credential_errors_if_user_id_doesnt_match(admin_request, notify_db_session):
user_1 = create_user(email='1')
user_2 = create_user(email='2')
cred_1a = create_webauthn_credential(user_1) # noqa
cred_1b = create_webauthn_credential(user_1) # noqa
cred_2a = create_webauthn_credential(user_2)
cred_2b = create_webauthn_credential(user_2) # noqa
response = admin_request.delete(
'webauthn.delete_webauthn_credential',
user_id=user_1.id,
webauthn_credential_id=cred_2a.id,
_expected_status=400
)
assert response['message'] == 'Webauthn credential does not belong to this user'