From 817dc9a8f3ea07aa7a81adc3b10e0686182451ec Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 17 Feb 2021 17:38:10 +0100 Subject: [PATCH 01/29] up the version --- ereuse_devicehub/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ereuse_devicehub/__init__.py b/ereuse_devicehub/__init__.py index a20c909b..53f0b176 100644 --- a/ereuse_devicehub/__init__.py +++ b/ereuse_devicehub/__init__.py @@ -1 +1 @@ -__version__ = "1.0.4-beta" +__version__ = "1.0.5-beta" From b9bd367487385dca95976e4ac65c15a701342a0f Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 17 Feb 2021 17:38:54 +0100 Subject: [PATCH 02/29] verify one document --- .../resources/documents/documents.py | 26 +++++++++++++++---- .../documents/templates/documents/stamp.html | 24 +++++++++++++++++ ereuse_devicehub/resources/hash_reports.py | 5 ++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index 33eca9b8..11e84602 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -24,7 +24,7 @@ from ereuse_devicehub.resources.device.views import DeviceView from ereuse_devicehub.resources.documents.device_row import DeviceRow, StockRow, ActionRow from ereuse_devicehub.resources.lot import LotView from ereuse_devicehub.resources.lot.models import Lot -from ereuse_devicehub.resources.hash_reports import insert_hash, ReportHash +from ereuse_devicehub.resources.hash_reports import insert_hash, ReportHash, verify_hash class Format(enum.Enum): @@ -244,12 +244,28 @@ class StampsView(View): This view render one public ans static page for see the links for to do the check of one csv file """ - def get(self): + def get_url_path(self): url = urlutils.URL(request.url) url.normalize() url.path_parts = url.path_parts[:-2] + ['check', ''] - url_path = url.to_text() - return flask.render_template('documents/stamp.html', rq_url=url_path) + return url.to_text() + + def get(self): + result = ('', '') + return flask.render_template('documents/stamp.html', rq_url=self.get_url_path(), + result=result) + + def post(self): + result = ('', '') + if 'docUpload' in request.files: + file_check = request.files['docUpload'] + result = ('Bad', 'Sorry, this file has not been produced by this website') + if file_check.mimetype in ['text/csv', 'application/pdf']: + if verify_hash(file_check): + result = ('Ok', 'Yes, this file has been produced by this website') + + return flask.render_template('documents/stamp.html', rq_url=self.get_url_path(), + result=result) class DocumentDef(Resource): @@ -305,7 +321,7 @@ class DocumentDef(Resource): self.add_url_rule('/check/', defaults={}, view_func=check_view, methods=get) stamps_view = StampsView.as_view('StampsView', definition=self, auth=app.auth) - self.add_url_rule('/stamps/', defaults={}, view_func=stamps_view, methods=get) + self.add_url_rule('/stamps/', defaults={}, view_func=stamps_view, methods={'GET', 'POST'}) actions_view = ActionsDocumentView.as_view('ActionsDocumentView', definition=self, diff --git a/ereuse_devicehub/resources/documents/templates/documents/stamp.html b/ereuse_devicehub/resources/documents/templates/documents/stamp.html index c204c2ff..0a7bfaf3 100644 --- a/ereuse_devicehub/resources/documents/templates/documents/stamp.html +++ b/ereuse_devicehub/resources/documents/templates/documents/stamp.html @@ -38,6 +38,17 @@
+ +
+ +
+
+ +
+ +
+
+
diff --git a/ereuse_devicehub/resources/hash_reports.py b/ereuse_devicehub/resources/hash_reports.py index f84e4b18..f44a79be 100644 --- a/ereuse_devicehub/resources/hash_reports.py +++ b/ereuse_devicehub/resources/hash_reports.py @@ -32,3 +32,8 @@ def insert_hash(bfile): db.session.add(db_hash) db.session.commit() db.session.flush() + + +def verify_hash(bfile): + hash3 = hashlib.sha3_256(bfile.read()).hexdigest() + return ReportHash.query.filter(ReportHash.hash3 == hash3).count() From 32c4844aed3e2deecbd50294dcfd34d68eddb967 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 17 Feb 2021 23:14:40 +0100 Subject: [PATCH 03/29] adding test for verify stamp --- tests/test_basic.py | 1 - tests/test_documents.py | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index b5e52805..77103f1b 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -27,7 +27,6 @@ def test_dependencies(): def test_api_docs(client: Client): """Tests /apidocs correct initialization.""" docs, _ = client.get('/apidocs') - # import pdb; pdb.set_trace() assert set(docs['paths'].keys()) == { '/actions/', '/apidocs', diff --git a/tests/test_documents.py b/tests/test_documents.py index fc4f8ea2..173be232 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -1,8 +1,9 @@ import csv import hashlib from datetime import datetime -from io import StringIO +from io import StringIO, BytesIO from pathlib import Path +from flask import url_for import pytest from werkzeug.exceptions import Unauthorized @@ -459,3 +460,40 @@ def test_get_document_lots(user: UserClient, user2: UserClient): assert export_csv[1][3] == 'comments,lot1,testcomment-lot1,' or 'comments,lot2,testcomment-lot2,' assert export2_csv[1][1] == 'Lot3-User2' assert export2_csv[1][3] == 'comments,lot3,testcomment-lot3,' + + +@pytest.mark.mvp +def test_verify_stamp(user: UserClient, client: Client): + """Test verify stamp of one export device information in a csv file.""" + snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot) + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='devices/', + accept='text/csv', + query=[('filter', {'type': ['Computer']})]) + + response, _ = client.post(res=documents.DocumentDef.t, + item='stamps/', + content_type='multipart/form-data', + accept='text/html', + data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')), 'example.csv')]}, + status=200) + assert "alert alert-info" in response + assert not "alert alert-danger" in response + + response, _ = client.post(res=documents.DocumentDef.t, + item='stamps/', + content_type='multipart/form-data', + accept='text/html', + data={'docUpload': [(BytesIO(b'abc'), 'example.csv')]}, + status=200) + + assert not "alert alert-info" in response + assert "alert alert-danger" in response + + response, _ = client.get(res=documents.DocumentDef.t, + item='stamps/', + accept='text/html', + status=200) + + assert not "alert alert-info" in response + assert not "alert alert-danger" in response From 24669631cef11c7c4f14831fbfdb82942af1e638 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 18 Feb 2021 11:36:55 +0100 Subject: [PATCH 04/29] adding hash for all documents than you download --- ereuse_devicehub/resources/documents/documents.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index 11e84602..4e8a073b 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -77,6 +77,7 @@ class DocumentView(DeviceView): res = flask_weasyprint.render_pdf( flask_weasyprint.HTML(string=template), download_filename='{}.pdf'.format(type) ) + insert_hash(res.data) else: res = flask.make_response(template) return res @@ -183,7 +184,9 @@ class LotsDocumentView(LotView): cw.writerow(l.keys()) first = False cw.writerow(l.values()) - output = make_response(data.getvalue()) + bfile = data.getvalue().encode('utf-8') + output = make_response(bfile) + insert_hash(bfile) output.headers['Content-Disposition'] = 'attachment; filename=lots-info.csv' output.headers['Content-type'] = 'text/csv' return output @@ -220,7 +223,9 @@ class StockDocumentView(DeviceView): cw.writerow(d.keys()) first = False cw.writerow(d.values()) - output = make_response(data.getvalue()) + bfile = data.getvalue().encode('utf-8') + output = make_response(bfile) + insert_hash(bfile) output.headers['Content-Disposition'] = 'attachment; filename=devices-stock.csv' output.headers['Content-type'] = 'text/csv' return output From 1cf3c61db53ffa31aac86949d9c54fe92a8797cf Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 18 Feb 2021 11:37:37 +0100 Subject: [PATCH 05/29] adding test for verify all documents --- tests/test_documents.py | 133 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 125 insertions(+), 8 deletions(-) diff --git a/tests/test_documents.py b/tests/test_documents.py index 173be232..c23ab6ba 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -481,19 +481,136 @@ def test_verify_stamp(user: UserClient, client: Client): assert not "alert alert-danger" in response response, _ = client.post(res=documents.DocumentDef.t, - item='stamps/', - content_type='multipart/form-data', - accept='text/html', - data={'docUpload': [(BytesIO(b'abc'), 'example.csv')]}, - status=200) + item='stamps/', + content_type='multipart/form-data', + accept='text/html', + data={'docUpload': [(BytesIO(b'abc'), 'example.csv')]}, + status=200) assert not "alert alert-info" in response assert "alert alert-danger" in response response, _ = client.get(res=documents.DocumentDef.t, - item='stamps/', - accept='text/html', - status=200) + item='stamps/', + accept='text/html', + status=200) assert not "alert alert-info" in response assert not "alert alert-danger" in response + + +@pytest.mark.mvp +def test_verify_stamp_log_info(user: UserClient, client: Client): + """Test verify stamp of one export lots-info in a csv file.""" + + l, _ = user.post({'name': 'Lot1', 'description': 'comments,lot1,testcomment-lot1,'}, res=Lot) + l, _ = user.post({'name': 'Lot2', 'description': 'comments,lot2,testcomment-lot2,'}, res=Lot) + + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='lots/', + accept='text/csv') + + response, _ = client.post(res=documents.DocumentDef.t, + item='stamps/', + content_type='multipart/form-data', + accept='text/html', + data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')), + 'example.csv')]}, + status=200) + assert "alert alert-info" in response + + +@pytest.mark.mvp +def test_verify_stamp_devices_stock(user: UserClient, client: Client): + """Test verify stamp of one export device information in a csv file.""" + + snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot) + + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='stock/', + accept='text/csv', + query=[('filter', {'type': ['Computer']})]) + + response, _ = client.post(res=documents.DocumentDef.t, + item='stamps/', + content_type='multipart/form-data', + accept='text/html', + data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')), + 'example.csv')]}, + status=200) + assert "alert alert-info" in response + + +@pytest.mark.mvp +def test_verify_stamp_csv_actions(user: UserClient, client: Client): + """Test verify stamp of one export device information in a csv file with others users.""" + acer = file('acer.happy.battery.snapshot') + snapshot, _ = user.post(acer, res=Snapshot) + device_id = snapshot['device']['id'] + post_request = {"transaction": "ccc", "name": "John", "endUsers": 1, + "devices": [device_id], "description": "aaa", + "finalUserCode": "abcdefjhi", + "startTime": "2020-11-01T02:00:00+00:00", + "endTime": "2020-12-01T02:00:00+00:00" + } + + user.post(res=Allocate, data=post_request) + hdd = [c for c in acer['components'] if c['type'] == 'HardDrive'][0] + hdd_action = [a for a in hdd['actions'] if a['type'] == 'TestDataStorage'][0] + hdd_action['lifetime'] += 1000 + acer.pop('elapsed') + acer['licence_version'] = '1.0.0' + snapshot, _ = client.post(acer, res=Live) + + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='actions/', + accept='text/csv', + query=[('filter', {'type': ['Computer']})]) + + response, _ = client.post(res=documents.DocumentDef.t, + item='stamps/', + content_type='multipart/form-data', + accept='text/html', + data={'docUpload': [(BytesIO(bytes(csv_str, 'utf-8')), + 'example.csv')]}, + status=200) + assert "alert alert-info" in response + + +@pytest.mark.mvp +def test_verify_stamp_erasure_certificate(user: UserClient, client: Client): + """Test verify stamp of one export certificate in PDF.""" + s = file('erase-sectors.snapshot') + snapshot, response = user.post(s, res=Snapshot) + # import pdb; pdb.set_trace() + + doc, _ = user.get(res=documents.DocumentDef.t, + item='erasures/', + query=[('filter', {'id': [snapshot['device']['id']]})], + accept=ANY) + + response, _ = client.post(res=documents.DocumentDef.t, + item='stamps/', + content_type='multipart/form-data', + accept='text/html', + data={'docUpload': [(BytesIO(bytes(doc, 'utf-8')), + 'example.csv')]}, + status=200) + assert "alert alert-danger" in response + + doc, _ = user.get(res=documents.DocumentDef.t, + item='erasures/', + query=[ + ('filter', {'id': [snapshot['device']['id']]}), + ('format', 'PDF') + ], + accept='application/pdf') + + response, _ = client.post(res=documents.DocumentDef.t, + item='stamps/', + content_type='multipart/form-data', + accept='text/html', + data={'docUpload': [(BytesIO(doc), + 'example.csv')]}, + status=200) + assert "alert alert-info" in response From 929ad8e2e944a99c2dba64beb91dbd49798ef16e Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 24 Feb 2021 20:18:13 +0100 Subject: [PATCH 06/29] . --- ereuse_devicehub/resources/tag/view.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index 2376635e..6be6179e 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -16,8 +16,10 @@ class TagView(View): """Creates a tag.""" num = request.args.get('num', type=int) if num: + # create unnamed tag res = self._create_many_regular_tags(num) else: + # create named tag res = self._post_one() return res @@ -35,6 +37,7 @@ class TagView(View): def _create_many_regular_tags(self, num: int): tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)]) tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id] + import pdb; pdb.set_trace() db.session.add_all(tags) db.session().final_flush() response = things_response(self.schema.dump(tags, many=True, nested=1), code=201) @@ -43,6 +46,7 @@ class TagView(View): def _post_one(self): # todo do we use this? + # import pdb; pdb.set_trace() t = request.get_json() tag = Tag(**t) if tag.like_etag(): @@ -58,6 +62,7 @@ class TagDeviceView(View): def one(self, id): """Gets the device from the tag.""" + import pdb; pdb.set_trace() tag = Tag.from_an_id(id).one() # type: Tag if not tag.device: raise TagNotLinked(tag.id) @@ -68,6 +73,7 @@ class TagDeviceView(View): # noinspection PyMethodOverriding def put(self, tag_id: str, device_id: str): """Links an existing tag with a device.""" + import pdb; pdb.set_trace() tag = Tag.from_an_id(tag_id).one() # type: Tag if tag.device_id: if tag.device_id == device_id: From aa3368838294d0f69a5ee8304c32adac34693880 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 24 Feb 2021 23:22:36 +0100 Subject: [PATCH 07/29] fixed 2 bugs --- ereuse_devicehub/resources/tag/view.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index 6be6179e..6fd46770 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -11,6 +11,13 @@ from ereuse_devicehub.resources.tag import Tag class TagView(View): + def one(self, id): + """Gets the device from the named tag, /tags/namedtag.""" + tag = Tag.from_an_id(id).one() # type: Tag + if not tag.device: + raise TagNotLinked(tag.id) + return redirect(location=url_for_resource(Device, tag.device.id)) + @auth.Auth.requires_auth def post(self): """Creates a tag.""" @@ -37,7 +44,6 @@ class TagView(View): def _create_many_regular_tags(self, num: int): tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)]) tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id] - import pdb; pdb.set_trace() db.session.add_all(tags) db.session().final_flush() response = things_response(self.schema.dump(tags, many=True, nested=1), code=201) @@ -45,8 +51,6 @@ class TagView(View): return response def _post_one(self): - # todo do we use this? - # import pdb; pdb.set_trace() t = request.get_json() tag = Tag(**t) if tag.like_etag(): @@ -62,7 +66,6 @@ class TagDeviceView(View): def one(self, id): """Gets the device from the tag.""" - import pdb; pdb.set_trace() tag = Tag.from_an_id(id).one() # type: Tag if not tag.device: raise TagNotLinked(tag.id) @@ -71,9 +74,9 @@ class TagDeviceView(View): return app.resources[Device.t].schema.jsonify(tag.device) # noinspection PyMethodOverriding + @auth.Auth.requires_auth def put(self, tag_id: str, device_id: str): """Links an existing tag with a device.""" - import pdb; pdb.set_trace() tag = Tag.from_an_id(tag_id).one() # type: Tag if tag.device_id: if tag.device_id == device_id: @@ -81,7 +84,10 @@ class TagDeviceView(View): else: raise LinkedToAnotherDevice(tag.device_id) else: + # Check if this device exist for this woner + Device.query.filter_by(owner=g.user).filter_by(id=device_id).one() tag.device_id = device_id + db.session().final_flush() db.session.commit() return Response(status=204) From 689d0c48c81995f15e30e2a887cb2dd0885684a9 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 26 Feb 2021 11:28:32 +0100 Subject: [PATCH 08/29] change the model of tags from org to user --- .../6a2a939d5668_drop_unique_org_for_tag.py | 33 +++++++++++++++++++ ereuse_devicehub/resources/tag/model.py | 5 +-- ereuse_devicehub/resources/tag/view.py | 13 +++++++- tests/test_tag.py | 28 ++++++++++++---- 4 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py diff --git a/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py b/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py new file mode 100644 index 00000000..417c27f9 --- /dev/null +++ b/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py @@ -0,0 +1,33 @@ +"""drop unique org for tag + +Revision ID: 6a2a939d5668 +Revises: eca457d8b2a4 +Create Date: 2021-02-25 18:47:47.441195 + +""" +from alembic import op +from alembic import context + + +# revision identifiers, used by Alembic. +revision = '6a2a939d5668' +down_revision = 'eca457d8b2a4' +branch_labels = None +depends_on = None + + +def get_inv(): + INV = context.get_x_argument(as_dictionary=True).get('inventory') + if not INV: + raise ValueError("Inventory value is not specified") + return INV + +def upgrade(): + op.drop_constraint('one tag id per organization', 'tag', schema=f'{get_inv()}') + op.drop_constraint('one secondary tag per organization', 'tag', schema=f'{get_inv()}') + op.create_unique_constraint('one tag id per owner', 'tag', ['id', 'owner_id'], schema=f'{get_inv()}') + + +def downgrade(): + op.create_unique_constraint('one tag id per organization', 'tag', ['id', 'org_id'], schema=f'{get_inv()}') + op.create_unique_constraint('one secondary tag per organization', 'tag', ['id', 'secondary'], schema=f'{get_inv()}') diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index 8aae5670..b5128830 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -97,8 +97,9 @@ class Tag(Thing): return url __table_args__ = ( - UniqueConstraint(id, org_id, name='one tag id per organization'), - UniqueConstraint(secondary, org_id, name='one secondary tag per organization') + UniqueConstraint(id, owner_id, name='one tag id per owner'), + # UniqueConstraint(id, org_id, name='one tag id per organization'), + # UniqueConstraint(secondary, org_id, name='one secondary tag per organization') ) @property diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index 6fd46770..2cb3009f 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -77,7 +77,8 @@ class TagDeviceView(View): @auth.Auth.requires_auth def put(self, tag_id: str, device_id: str): """Links an existing tag with a device.""" - tag = Tag.from_an_id(tag_id).one() # type: Tag + # tag = Tag.from_an_id(tag_id).one() # type: Tag + tag = Tag.from_an_id(tag_id).filter_by(owner=g.user).one() # type: Tag if tag.device_id: if tag.device_id == device_id: return Response(status=204) @@ -92,6 +93,16 @@ class TagDeviceView(View): db.session.commit() return Response(status=204) + @auth.Auth.requires_auth + def delete(self, tag_id: str, device_id: str): + tag = Tag.from_an_id(tag_id).filter_by(owner=g.user).one() # type: Tag + device = Device.query.filter_by(owner=g.user).filter_by(id=device_id).one() + if tag.device == device: + tag.device_id = None + db.session().final_flush() + db.session.commit() + return Response(status=204) + def get_device_from_tag(id: str): """Gets the device by passing a tag id. diff --git a/tests/test_tag.py b/tests/test_tag.py index b0336021..dcb8fe24 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -51,13 +51,16 @@ def test_create_tag_default_org(user: UserClient): @pytest.mark.mvp @pytest.mark.usefixtures(conftest.app_context.__name__) -def test_create_tag_no_slash(): - """Checks that no tags can be created that contain a slash.""" - with raises(ValidationError): - Tag('/') - - with raises(ValidationError): - Tag('bar', secondary='/') +def test_create_same_tag_default_org_two_users(user: UserClient, user2: UserClient): + """Creates a tag using the default organization.""" + # import pdb; pdb.set_trace() + tag = Tag(id='foo-1', owner_id=user.user['id']) + tag2 = Tag(id='foo-1', owner_id=user2.user['id']) + db.session.add(tag) + db.session.add(tag2) + db.session.commit() + assert tag.org.name == 'FooOrg' # as defined in the settings + assert tag2.org.name == 'FooOrg' # as defined in the settings @pytest.mark.mvp @@ -78,6 +81,17 @@ def test_create_two_same_tags(user: UserClient): db.session.commit() +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_create_tag_no_slash(): + """Checks that no tags can be created that contain a slash.""" + with raises(ValidationError): + Tag('/') + + with raises(ValidationError): + Tag('bar', secondary='/') + + @pytest.mark.mvp def test_tag_post(app: Devicehub, user: UserClient): """Checks the POST method of creating a tag.""" From 3931d2456fff35fee1b67d028bbed445067ccbef Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 26 Feb 2021 11:29:17 +0100 Subject: [PATCH 09/29] add endpoint for deassociate tags of devices --- ereuse_devicehub/resources/tag/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ereuse_devicehub/resources/tag/__init__.py b/ereuse_devicehub/resources/tag/__init__.py index e6418e54..2423d80a 100644 --- a/ereuse_devicehub/resources/tag/__init__.py +++ b/ereuse_devicehub/resources/tag/__init__.py @@ -48,6 +48,10 @@ class TagDef(Resource): 'device/<{0.ID_CONVERTER.value}:device_id>'.format(DeviceDef), view_func=device_view, methods={'PUT'}) + self.add_url_rule('/<{0.ID_CONVERTER.value}:tag_id>/'.format(self) + + 'device/<{0.ID_CONVERTER.value}:device_id>'.format(DeviceDef), + view_func=device_view, + methods={'DELETE'}) @option('-u', '--owner', help=OWNER_H) @option('-o', '--org', help=ORG_H) From 159eab29c2222c3acf6c6d4c9e182b9f48817a29 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 26 Feb 2021 13:23:50 +0100 Subject: [PATCH 10/29] restrict to named tags --- ereuse_devicehub/resources/tag/view.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index 2cb3009f..a23c50b1 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -97,6 +97,10 @@ class TagDeviceView(View): def delete(self, tag_id: str, device_id: str): tag = Tag.from_an_id(tag_id).filter_by(owner=g.user).one() # type: Tag device = Device.query.filter_by(owner=g.user).filter_by(id=device_id).one() + if tag.provider: + # if is an unamed tag not do nothing + return Response(status=204) + if tag.device == device: tag.device_id = None db.session().final_flush() From ac032c7902efafbe0bd08db46718b4daaf42454a Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 26 Feb 2021 13:25:21 +0100 Subject: [PATCH 11/29] fixing primary keys --- .../versions/6a2a939d5668_drop_unique_org_for_tag.py | 7 +++---- ereuse_devicehub/resources/tag/model.py | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py b/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py index 417c27f9..b6bdabff 100644 --- a/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py +++ b/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py @@ -24,10 +24,9 @@ def get_inv(): def upgrade(): op.drop_constraint('one tag id per organization', 'tag', schema=f'{get_inv()}') - op.drop_constraint('one secondary tag per organization', 'tag', schema=f'{get_inv()}') - op.create_unique_constraint('one tag id per owner', 'tag', ['id', 'owner_id'], schema=f'{get_inv()}') + op.create_primary_key('one tag id per owner', 'tag', ['id', 'owner_id'], schema=f'{get_inv()}'), def downgrade(): - op.create_unique_constraint('one tag id per organization', 'tag', ['id', 'org_id'], schema=f'{get_inv()}') - op.create_unique_constraint('one secondary tag per organization', 'tag', ['id', 'secondary'], schema=f'{get_inv()}') + op.drop_constraint('one tag id per owner', 'tag', schema=f'{get_inv()}') + op.create_primary_key('one tag id per organization', 'tag', ['id', 'org_id'], schema=f'{get_inv()}'), diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index b5128830..5adce308 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -30,12 +30,12 @@ class Tag(Thing): id.comment = """The ID of the tag.""" owner_id = Column(UUID(as_uuid=True), ForeignKey(User.id), + primary_key=True, nullable=False, default=lambda: g.user.id) owner = relationship(User, primaryjoin=owner_id == User.id) org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id), - primary_key=True, # If we link with the Organization object this instance # will be set as persistent and added to session # which is something we don't want to enforce by default @@ -98,8 +98,7 @@ class Tag(Thing): __table_args__ = ( UniqueConstraint(id, owner_id, name='one tag id per owner'), - # UniqueConstraint(id, org_id, name='one tag id per organization'), - # UniqueConstraint(secondary, org_id, name='one secondary tag per organization') + UniqueConstraint(secondary, org_id, name='one secondary tag per organization') ) @property From 8d6e50ece90c855a53c1bc26ca22699eb17cb4b3 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 26 Feb 2021 18:58:10 +0100 Subject: [PATCH 12/29] fixing unique secondary and owner --- .../versions/6a2a939d5668_drop_unique_org_for_tag.py | 4 ++++ ereuse_devicehub/resources/tag/model.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py b/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py index b6bdabff..435cce5d 100644 --- a/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py +++ b/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py @@ -24,9 +24,13 @@ def get_inv(): def upgrade(): op.drop_constraint('one tag id per organization', 'tag', schema=f'{get_inv()}') + op.drop_constraint('one secondary tag per organization', 'tag', schema=f'{get_inv()}') op.create_primary_key('one tag id per owner', 'tag', ['id', 'owner_id'], schema=f'{get_inv()}'), + op.create_unique_constraint('one secondary tag per owner', 'tag', ['secondary', 'owner_id'], schema=f'{get_inv()}'), def downgrade(): op.drop_constraint('one tag id per owner', 'tag', schema=f'{get_inv()}') + op.drop_constraint('one secondary tag per owner', 'tag', schema=f'{get_inv()}') op.create_primary_key('one tag id per organization', 'tag', ['id', 'org_id'], schema=f'{get_inv()}'), + op.create_unique_constraint('one secondary tag per organization', 'tag', ['secondary', 'org_id'], schema=f'{get_inv()}'), diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index 5adce308..7f6acc4f 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -98,7 +98,7 @@ class Tag(Thing): __table_args__ = ( UniqueConstraint(id, owner_id, name='one tag id per owner'), - UniqueConstraint(secondary, org_id, name='one secondary tag per organization') + UniqueConstraint(secondary, owner_id, name='one secondary tag per organization') ) @property From adcb330b87a6bf7d17e4387a764855ebdbe43820 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 26 Feb 2021 18:59:32 +0100 Subject: [PATCH 13/29] fixing same tag 2 users get with a validate user --- ereuse_devicehub/resources/tag/view.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index a23c50b1..2b1d2c1a 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -66,11 +66,19 @@ class TagDeviceView(View): def one(self, id): """Gets the device from the tag.""" + if request.authorization: + return self.one_authorization(id) + tag = Tag.from_an_id(id).one() # type: Tag if not tag.device: raise TagNotLinked(tag.id) - if not request.authorization: - return redirect(location=url_for_resource(Device, tag.device.id)) + return redirect(location=url_for_resource(Device, tag.device.id)) + + @auth.Auth.requires_auth + def one_authorization(self, id): + tag = Tag.from_an_id(id).filter_by(owner=g.user).one() # type: Tag + if not tag.device: + raise TagNotLinked(tag.id) return app.resources[Device.t].schema.jsonify(tag.device) # noinspection PyMethodOverriding From b2859781f05a4745dd7a6e9712b3d442983f296a Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 26 Feb 2021 18:59:57 +0100 Subject: [PATCH 14/29] new tests and fixing others --- tests/test_tag.py | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/tests/test_tag.py b/tests/test_tag.py index dcb8fe24..b3a8fd4d 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -8,7 +8,7 @@ from pytest import raises from teal.db import MultipleResourcesFound, ResourceNotFound, UniqueViolation, DBError from teal.marshmallow import ValidationError -from ereuse_devicehub.client import UserClient +from ereuse_devicehub.client import UserClient, Client from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.action.models import Snapshot @@ -78,7 +78,8 @@ def test_create_two_same_tags(user: UserClient): db.session.add(Tag(id='foo-bar', owner_id=user.user['id'])) org2 = Organization(name='org 2', tax_id='tax id org 2') db.session.add(Tag(id='foo-bar', org=org2, owner_id=user.user['id'])) - db.session.commit() + with raises(DBError): + db.session.commit() @pytest.mark.mvp @@ -145,17 +146,39 @@ def test_tag_get_device_from_tag_endpoint_no_tag(user: UserClient): @pytest.mark.mvp -def test_tag_get_device_from_tag_endpoint_multiple_tags(app: Devicehub, user: UserClient): - """As above, but when there are two tags with the same ID, the +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_tag_get_device_from_tag_endpoint_multiple_tags(app: Devicehub, user: UserClient, user2: UserClient, client: Client): + """As above, but when there are two tags with the secondary ID, the system should not return any of both (to be deterministic) so it should raise an exception. """ - with app.app_context(): - db.session.add(Tag(id='foo-bar', owner_id=user.user['id'])) - org2 = Organization(name='org 2', tax_id='tax id org 2') - db.session.add(Tag(id='foo-bar', org=org2, owner_id=user.user['id'])) + db.session.add(Tag(id='foo', secondary='bar', owner_id=user.user['id'])) + db.session.commit() + + db.session.add(Tag(id='foo', secondary='bar', owner_id=user2.user['id'])) + db.session.commit() + + db.session.add(Tag(id='foo2', secondary='bar', owner_id=user.user['id'])) + with raises(DBError): db.session.commit() - user.get(res=Tag, item='foo-bar/device', status=MultipleResourcesFound) + db.session.rollback() + + tag1 = Tag.from_an_id('foo').filter_by(owner_id=user.user['id']).one() + tag2 = Tag.from_an_id('foo').filter_by(owner_id=user2.user['id']).one() + pc1 = Desktop(serial_number='sn1', chassis=ComputerChassis.Tower, owner_id=user.user['id']) + pc2 = Desktop(serial_number='sn2', chassis=ComputerChassis.Tower, owner_id=user2.user['id']) + pc1.tags.add(tag1) + pc2.tags.add(tag2) + db.session.add(pc1) + db.session.add(pc2) + db.session.commit() + computer, _ = user.get(res=Tag, item='foo/device') + assert computer['serialNumber'] == 'sn1' + computer, _ = user2.get(res=Tag, item='foo/device') + assert computer['serialNumber'] == 'sn2' + + _, status = client.get(res=Tag, item='foo/device', status=MultipleResourcesFound) + assert status.status_code == 422 @pytest.mark.mvp @@ -230,8 +253,7 @@ def test_tag_secondary_workbench_link_find(user: UserClient): t = Tag('foo', secondary='bar', owner_id=user.user['id']) db.session.add(t) db.session.flush() - assert Tag.from_an_id('bar').one() == t - assert Tag.from_an_id('foo').one() == t + assert Tag.from_an_id('bar').one() == Tag.from_an_id('foo').one() with pytest.raises(ResourceNotFound): Tag.from_an_id('nope').one() From b811e0572c1e06ac85825697fd58e99a5c0b7183 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 1 Mar 2021 17:56:55 +0100 Subject: [PATCH 15/29] delete tags named --- ereuse_devicehub/resources/tag/model.py | 25 ++++++++++++++ ereuse_devicehub/resources/tag/view.py | 8 +++++ tests/test_tag.py | 43 +++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index 7f6acc4f..9a5231d7 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -125,6 +125,19 @@ class Tag(Thing): """Return a SQLAlchemy filter expression for printable queries.""" return cls.org_id == Organization.get_default_org_id() + def delete(self): + """Deletes the tag. + + This method removes the tag if is named tag and don't have any linked device. + """ + if self.device: + raise TagLinked(self) + if self.provider: + # if is an unnamed tag not delete + raise TagUnnamed(self.id) + + db.session.delete(self) + def __repr__(self) -> str: return ''.format(self) @@ -133,3 +146,15 @@ class Tag(Thing): def __format__(self, format_spec: str) -> str: return '{0.org.name} {0.id}'.format(self) + + +class TagLinked(ValidationError): + def __init__(self, tag): + message = 'The tag {} is linked to device {}.'.format(tag.id, tag.device.id) + super().__init__(message, field_names=['device']) + + +class TagUnnamed(ValidationError): + def __init__(self, id): + message = 'This tag {} is unnamed tag. It is imposible delete.'.format(id) + super().__init__(message, field_names=['device']) diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index 2b1d2c1a..75f575f5 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -60,6 +60,14 @@ class TagView(View): db.session.commit() return Response(status=201) + @auth.Auth.requires_auth + def delete(self, id): + tag = Tag.from_an_id(id).filter_by(owner=g.user).one() + tag.delete() + db.session().final_flush() + db.session.commit() + return Response(status=204) + class TagDeviceView(View): """Endpoints to work with the device of the tag; /tags/23/device.""" diff --git a/tests/test_tag.py b/tests/test_tag.py index b3a8fd4d..ce6a517e 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -35,6 +35,49 @@ def test_create_tag(user: UserClient): assert tag.provider == URL('http://foo.bar') +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_delete_tags(user: UserClient, client: Client): + """Delete a named tag.""" + # Delete Tag Named + # import pdb; pdb.set_trace() + pc = Desktop(serial_number='sn1', chassis=ComputerChassis.Tower, owner_id=user.user['id']) + db.session.add(pc) + db.session.commit() + tag = Tag(id='bar', owner_id=user.user['id'], device_id=pc.id) + db.session.add(tag) + db.session.commit() + tag = Tag.query.one() + assert tag.id == 'bar' + # Is not possible delete one tag linked to one device + res, _ = user.delete(res=Tag, item=tag.id, status=422) + msg = 'The tag bar is linked to device' + assert msg in res['message'][0] + + tag.device_id = None + db.session.add(tag) + db.session.commit() + # Is not possible delete one tag from an anonymous user + client.delete(res=Tag, item=tag.id, status=401) + + # Is possible delete one normal tag + user.delete(res=Tag, item=tag.id) + user.get(res=Tag, item=tag.id, status=404) + + # Delete Tag UnNamed + org = Organization(name='bar', tax_id='bartax') + tag = Tag(id='bar-1', org=org, provider=URL('http://foo.bar'), owner_id=user.user['id']) + db.session.add(tag) + db.session.commit() + tag = Tag.query.one() + assert tag.id == 'bar-1' + res, _ = user.delete(res=Tag, item=tag.id, status=422) + msg = 'This tag {} is unnamed tag. It is imposible delete.'.format(tag.id) + assert msg in res['message'] + tag = Tag.query.one() + assert tag.id == 'bar-1' + + @pytest.mark.mvp @pytest.mark.usefixtures(conftest.app_context.__name__) def test_create_tag_default_org(user: UserClient): From a73dee899241850550a63d8e9178499f834eb504 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 1 Mar 2021 18:55:44 +0100 Subject: [PATCH 16/29] add test for liink and unlink device to one tag --- tests/test_tag.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/test_tag.py b/tests/test_tag.py index ce6a517e..ce76194c 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -33,6 +33,26 @@ def test_create_tag(user: UserClient): tag = Tag.query.one() assert tag.id == 'bar-1' assert tag.provider == URL('http://foo.bar') + res, _ = user.get(res=Tag, item=tag.id, status=422) + assert res['type'] == 'TagNotLinked' + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_create_tag_with_device(user: UserClient): + """Creates a tag specifying linked with one device.""" + pc = Desktop(serial_number='sn1', chassis=ComputerChassis.Tower, owner_id=user.user['id']) + db.session.add(pc) + db.session.commit() + tag = Tag(id='bar', owner_id=user.user['id']) + db.session.add(tag) + db.session.commit() + data = '{tag_id}/device/{device_id}'.format(tag_id=tag.id, device_id=pc.id) + user.put({}, res=Tag, item=data, status=204) + user.get(res=Tag, item='{}/device'.format(tag.id)) + user.delete({}, res=Tag, item=data, status=204) + res, _ = user.get(res=Tag, item='{}/device'.format(tag.id), status=422) + assert res['type'] == 'TagNotLinked' @pytest.mark.mvp @@ -40,7 +60,6 @@ def test_create_tag(user: UserClient): def test_delete_tags(user: UserClient, client: Client): """Delete a named tag.""" # Delete Tag Named - # import pdb; pdb.set_trace() pc = Desktop(serial_number='sn1', chassis=ComputerChassis.Tower, owner_id=user.user['id']) db.session.add(pc) db.session.commit() @@ -96,7 +115,6 @@ def test_create_tag_default_org(user: UserClient): @pytest.mark.usefixtures(conftest.app_context.__name__) def test_create_same_tag_default_org_two_users(user: UserClient, user2: UserClient): """Creates a tag using the default organization.""" - # import pdb; pdb.set_trace() tag = Tag(id='foo-1', owner_id=user.user['id']) tag2 = Tag(id='foo-1', owner_id=user2.user['id']) db.session.add(tag) From 372427375dfeb85d1da8d516b721c797750a8269 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 4 Mar 2021 10:38:04 +0100 Subject: [PATCH 17/29] correct response for every stamp check --- ereuse_devicehub/resources/documents/documents.py | 8 ++++++-- .../resources/documents/templates/documents/stamp.html | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index 4e8a073b..9d74ca10 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -264,10 +264,14 @@ class StampsView(View): result = ('', '') if 'docUpload' in request.files: file_check = request.files['docUpload'] - result = ('Bad', 'Sorry, this file has not been produced by this website') + bad = 'There are no coincidences. The attached file data does not come \ + from our backend or it has been subsequently modified.' + ok = '100% coincidence. The attached file contains data 100% existing in \ + to our backend' + result = ('Bad', bad) if file_check.mimetype in ['text/csv', 'application/pdf']: if verify_hash(file_check): - result = ('Ok', 'Yes, this file has been produced by this website') + result = ('Ok', ok) return flask.render_template('documents/stamp.html', rq_url=self.get_url_path(), result=result) diff --git a/ereuse_devicehub/resources/documents/templates/documents/stamp.html b/ereuse_devicehub/resources/documents/templates/documents/stamp.html index 0a7bfaf3..0a368f4d 100644 --- a/ereuse_devicehub/resources/documents/templates/documents/stamp.html +++ b/ereuse_devicehub/resources/documents/templates/documents/stamp.html @@ -40,11 +40,11 @@