Skip to content

Commit 0411324

Browse files
authored
Merge pull request #98 from ioet/feature/enforce-authentication-by-jwt#94
Close #94 Enforce authentication by JWT
2 parents 41a73da + be9fec2 commit 0411324

File tree

15 files changed

+645
-237
lines changed

15 files changed

+645
-237
lines changed

commons/data_access_layer/database.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ def delete(self, id):
3636

3737

3838
def init_app(app: Flask) -> None:
39-
init_sql(app) # TODO Delete after the migration to Cosmos DB has finished.
4039
init_cosmos_db(app)
4140

4241

commons/data_access_layer/sql.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
from flask_sqlalchemy import SQLAlchemy
55

66
from commons.data_access_layer.database import CRUDDao, ID_MAX_LENGTH
7-
from time_tracker_api.security import current_user_id
87

98
db: SQLAlchemy = None
10-
AuditedSQLModel = None
119

1210

1311
def handle_commit_issues(f):
@@ -25,16 +23,6 @@ def init_app(app: Flask) -> None:
2523
global db
2624
db = SQLAlchemy(app)
2725

28-
global AuditedSQLModel
29-
30-
class AuditedSQLModelClass():
31-
created_at = db.Column(db.DateTime, server_default=db.func.now())
32-
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
33-
created_by = db.Column(db.String(ID_MAX_LENGTH), default=current_user_id)
34-
updated_by = db.Column(db.String(ID_MAX_LENGTH), onupdate=current_user_id)
35-
36-
AuditedSQLModel = AuditedSQLModelClass
37-
3826

3927
class SQLRepository():
4028
def __init__(self, model_type: type):

requirements/time_tracker_api/prod.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ Flask-Script==2.0.6
2626
#Semantic versioning
2727
python-semantic-release==5.2.0
2828

29-
# The Debug Toolbar
29+
#The Debug Toolbar
3030
Flask-DebugToolbar==0.11.0
3131

3232
#CORS
33-
flask-cors==3.0.8
33+
flask-cors==3.0.8
34+
35+
#JWT
36+
PyJWT==1.7.1

tests/commons/data_access_layer/sql_test.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ def test_create(sql_repository):
1616

1717
assert result is not None
1818
assert result.id is not None
19-
assert result.created_at is not None
20-
assert result.created_by is not None
21-
assert result.updated_at is None
22-
assert result.updated_by is None
2319

2420
existing_elements_registry.append(result)
2521

@@ -43,9 +39,6 @@ def test_update(sql_repository):
4339
assert updated_element.id == existing_element.id
4440
assert updated_element.name == "Jon Snow"
4541
assert updated_element.age == 34
46-
assert updated_element.updated_at is not None
47-
assert updated_element.updated_at > updated_element.created_at
48-
assert updated_element.updated_by is not None
4942

5043

5144
def test_find_all(sql_repository):

tests/conftest.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
from datetime import datetime, timedelta
2+
3+
import jwt
14
import pytest
25
from faker import Faker
3-
from flask import Flask
6+
from flask import Flask, url_for
47
from flask.testing import FlaskClient
58

69
from commons.data_access_layer.cosmos_db import CosmosDBRepository, datetime_str, current_datetime
10+
from commons.data_access_layer.database import init_sql
711
from time_tracker_api import create_app
12+
from time_tracker_api.security import get_or_generate_dev_secret_key
813
from time_tracker_api.time_entries.time_entries_model import TimeEntryCosmosDBRepository
914

1015
fake = Faker()
@@ -23,9 +28,12 @@ def client(app: Flask) -> FlaskClient:
2328

2429

2530
@pytest.fixture(scope="module")
26-
def sql_model_class():
27-
from commons.data_access_layer.sql import db, AuditedSQLModel
28-
class PersonSQLModel(db.Model, AuditedSQLModel):
31+
def sql_model_class(app: Flask):
32+
with app.app_context():
33+
init_sql(app)
34+
35+
from commons.data_access_layer.sql import db
36+
class PersonSQLModel(db.Model):
2937
__tablename__ = 'test'
3038
id = db.Column(db.Integer, primary_key=True)
3139
name = db.Column(db.String(80), unique=False, nullable=False)
@@ -146,5 +154,20 @@ def running_time_entry(time_entry_repository: TimeEntryCosmosDBRepository,
146154

147155
yield created_time_entry
148156

149-
time_entry_repository.delete(id=created_time_entry.id,
157+
time_entry_repository.delete_permanently(id=created_time_entry.id,
150158
partition_key_value=tenant_id)
159+
160+
161+
@pytest.fixture(scope="session")
162+
def valid_jwt(app: Flask, tenant_id: str, owner_id: str) -> str:
163+
expiration_time = datetime.utcnow() + timedelta(seconds=3600)
164+
return jwt.encode({
165+
"iss": "https://securityioet.b2clogin.com/%s/v2.0/" % tenant_id,
166+
"oid": owner_id,
167+
'exp': expiration_time
168+
}, key=get_or_generate_dev_secret_key()).decode("UTF-8")
169+
170+
171+
@pytest.fixture(scope="session")
172+
def valid_header(valid_jwt: str) -> dict:
173+
return {'Authorization': "Bearer %s" % valid_jwt}

tests/time_tracker_api/activities/activities_namespace_test.py

Lines changed: 88 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
from flask_restplus._http import HTTPStatus
55
from pytest_mock import MockFixture
66

7-
from time_tracker_api.security import current_user_tenant_id
8-
97
fake = Faker()
108

119
valid_activity_data = {
@@ -19,45 +17,63 @@
1917
}).update(valid_activity_data)
2018

2119

22-
def test_create_activity_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture):
20+
def test_create_activity_should_succeed_with_valid_request(client: FlaskClient,
21+
mocker: MockFixture,
22+
valid_header: dict):
2323
from time_tracker_api.activities.activities_namespace import activity_dao
2424
repository_create_mock = mocker.patch.object(activity_dao.repository,
2525
'create',
2626
return_value=fake_activity)
2727

28-
response = client.post("/activities", json=valid_activity_data, follow_redirects=True)
28+
response = client.post("/activities",
29+
headers=valid_header,
30+
json=valid_activity_data,
31+
follow_redirects=True)
2932

3033
assert HTTPStatus.CREATED == response.status_code
3134
repository_create_mock.assert_called_once()
3235

3336

34-
def test_create_activity_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
37+
def test_create_activity_should_reject_bad_request(client: FlaskClient,
38+
mocker: MockFixture,
39+
valid_header: dict):
3540
from time_tracker_api.activities.activities_namespace import activity_dao
3641
repository_create_mock = mocker.patch.object(activity_dao.repository,
3742
'create',
3843
return_value=fake_activity)
3944

40-
response = client.post("/activities", json=None, follow_redirects=True)
45+
response = client.post("/activities",
46+
headers=valid_header,
47+
json=None,
48+
follow_redirects=True)
4149

4250
assert HTTPStatus.BAD_REQUEST == response.status_code
4351
repository_create_mock.assert_not_called()
4452

4553

46-
def test_list_all_activities(client: FlaskClient, mocker: MockFixture):
54+
def test_list_all_activities(client: FlaskClient,
55+
mocker: MockFixture,
56+
tenant_id: str,
57+
valid_header: dict):
4758
from time_tracker_api.activities.activities_namespace import activity_dao
4859
repository_find_all_mock = mocker.patch.object(activity_dao.repository,
4960
'find_all',
5061
return_value=[])
5162

52-
response = client.get("/activities", follow_redirects=True)
63+
response = client.get("/activities",
64+
headers=valid_header,
65+
follow_redirects=True)
5366

5467
assert HTTPStatus.OK == response.status_code
5568
json_data = json.loads(response.data)
5669
assert [] == json_data
57-
repository_find_all_mock.assert_called_once()
70+
repository_find_all_mock.assert_called_once_with(partition_key_value=tenant_id)
5871

5972

60-
def test_get_activity_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
73+
def test_get_activity_should_succeed_with_valid_id(client: FlaskClient,
74+
mocker: MockFixture,
75+
tenant_id: str,
76+
valid_header: dict):
6177
from time_tracker_api.activities.activities_namespace import activity_dao
6278

6379
valid_id = fake.random_int(1, 9999)
@@ -66,15 +82,19 @@ def test_get_activity_should_succeed_with_valid_id(client: FlaskClient, mocker:
6682
'find',
6783
return_value=fake_activity)
6884

69-
response = client.get("/activities/%s" % valid_id, follow_redirects=True)
85+
response = client.get("/activities/%s" % valid_id,
86+
headers=valid_header,
87+
follow_redirects=True)
7088

7189
assert HTTPStatus.OK == response.status_code
7290
fake_activity == json.loads(response.data)
73-
repository_find_mock.assert_called_once_with(str(valid_id),
74-
partition_key_value=current_user_tenant_id())
91+
repository_find_mock.assert_called_once_with(str(valid_id), partition_key_value=tenant_id)
7592

7693

77-
def test_get_activity_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
94+
def test_get_activity_should_return_not_found_with_invalid_id(client: FlaskClient,
95+
mocker: MockFixture,
96+
tenant_id: str,
97+
valid_header: dict):
7898
from time_tracker_api.activities.activities_namespace import activity_dao
7999
from werkzeug.exceptions import NotFound
80100

@@ -84,14 +104,17 @@ def test_get_activity_should_return_not_found_with_invalid_id(client: FlaskClien
84104
'find',
85105
side_effect=NotFound)
86106

87-
response = client.get("/activities/%s" % invalid_id, follow_redirects=True)
107+
response = client.get("/activities/%s" % invalid_id,
108+
headers=valid_header,
109+
follow_redirects=True)
88110

89111
assert HTTPStatus.NOT_FOUND == response.status_code
90112
repository_find_mock.assert_called_once_with(str(invalid_id),
91-
partition_key_value=current_user_tenant_id())
113+
partition_key_value=tenant_id)
92114

93115

94-
def test_get_activity_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture):
116+
def test_get_activity_should_return_422_for_invalid_id_format(client: FlaskClient,
117+
mocker: MockFixture):
95118
from time_tracker_api.activities.activities_namespace import activity_dao
96119
from werkzeug.exceptions import UnprocessableEntity
97120

@@ -104,41 +127,54 @@ def test_get_activity_should_return_422_for_invalid_id_format(client: FlaskClien
104127
response = client.get("/activities/%s" % invalid_id, follow_redirects=True)
105128

106129
assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code
107-
repository_find_mock.assert_called_once_with(str(invalid_id),
108-
partition_key_value=current_user_tenant_id())
130+
repository_find_mock.assert_not_called()
109131

110132

111-
def test_update_activity_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture):
133+
def test_update_activity_should_succeed_with_valid_data(client: FlaskClient,
134+
tenant_id: str,
135+
mocker: MockFixture,
136+
valid_header: dict):
112137
from time_tracker_api.activities.activities_namespace import activity_dao
113138

114139
repository_update_mock = mocker.patch.object(activity_dao.repository,
115140
'partial_update',
116141
return_value=fake_activity)
117142

118-
valid_id = fake.random_int(1, 9999)
119-
response = client.put("/activities/%s" % valid_id, json=valid_activity_data, follow_redirects=True)
143+
valid_id = fake.uuid4()
144+
response = client.put("/activities/%s" % valid_id,
145+
headers=valid_header,
146+
json=valid_activity_data,
147+
follow_redirects=True)
120148

121149
assert HTTPStatus.OK == response.status_code
122150
fake_activity == json.loads(response.data)
123151
repository_update_mock.assert_called_once_with(str(valid_id),
124152
changes=valid_activity_data,
125-
partition_key_value=current_user_tenant_id())
153+
partition_key_value=tenant_id)
126154

127155

128-
def test_update_activity_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
156+
def test_update_activity_should_reject_bad_request(client: FlaskClient,
157+
mocker: MockFixture,
158+
valid_header: dict):
129159
from time_tracker_api.activities.activities_namespace import activity_dao
130160
repository_update_mock = mocker.patch.object(activity_dao.repository,
131161
'partial_update',
132162
return_value=fake_activity)
133163

134164
valid_id = fake.random_int(1, 9999)
135-
response = client.put("/activities/%s" % valid_id, json=None, follow_redirects=True)
165+
response = client.put("/activities/%s" % valid_id,
166+
headers=valid_header,
167+
json=None,
168+
follow_redirects=True)
136169

137170
assert HTTPStatus.BAD_REQUEST == response.status_code
138171
repository_update_mock.assert_not_called()
139172

140173

141-
def test_update_activity_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
174+
def test_update_activity_should_return_not_found_with_invalid_id(client: FlaskClient,
175+
tenant_id: str,
176+
mocker: MockFixture,
177+
valid_header: dict):
142178
from time_tracker_api.activities.activities_namespace import activity_dao
143179
from werkzeug.exceptions import NotFound
144180

@@ -149,16 +185,20 @@ def test_update_activity_should_return_not_found_with_invalid_id(client: FlaskCl
149185
side_effect=NotFound)
150186

151187
response = client.put("/activities/%s" % invalid_id,
188+
headers=valid_header,
152189
json=valid_activity_data,
153190
follow_redirects=True)
154191

155192
assert HTTPStatus.NOT_FOUND == response.status_code
156193
repository_update_mock.assert_called_once_with(str(invalid_id),
157194
changes=valid_activity_data,
158-
partition_key_value=current_user_tenant_id())
195+
partition_key_value=tenant_id)
159196

160197

161-
def test_delete_activity_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
198+
def test_delete_activity_should_succeed_with_valid_id(client: FlaskClient,
199+
mocker: MockFixture,
200+
tenant_id: str,
201+
valid_header: dict):
162202
from time_tracker_api.activities.activities_namespace import activity_dao
163203

164204
valid_id = fake.random_int(1, 9999)
@@ -167,15 +207,20 @@ def test_delete_activity_should_succeed_with_valid_id(client: FlaskClient, mocke
167207
'delete',
168208
return_value=None)
169209

170-
response = client.delete("/activities/%s" % valid_id, follow_redirects=True)
210+
response = client.delete("/activities/%s" % valid_id,
211+
headers=valid_header,
212+
follow_redirects=True)
171213

172214
assert HTTPStatus.NO_CONTENT == response.status_code
173215
assert b'' == response.data
174216
repository_remove_mock.assert_called_once_with(str(valid_id),
175-
partition_key_value=current_user_tenant_id())
217+
partition_key_value=tenant_id)
176218

177219

178-
def test_delete_activity_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
220+
def test_delete_activity_should_return_not_found_with_invalid_id(client: FlaskClient,
221+
mocker: MockFixture,
222+
tenant_id: str,
223+
valid_header: dict):
179224
from time_tracker_api.activities.activities_namespace import activity_dao
180225
from werkzeug.exceptions import NotFound
181226

@@ -185,14 +230,19 @@ def test_delete_activity_should_return_not_found_with_invalid_id(client: FlaskCl
185230
'delete',
186231
side_effect=NotFound)
187232

188-
response = client.delete("/activities/%s" % invalid_id, follow_redirects=True)
233+
response = client.delete("/activities/%s" % invalid_id,
234+
headers=valid_header,
235+
follow_redirects=True)
189236

190237
assert HTTPStatus.NOT_FOUND == response.status_code
191238
repository_remove_mock.assert_called_once_with(str(invalid_id),
192-
partition_key_value=current_user_tenant_id())
239+
partition_key_value=tenant_id)
193240

194241

195-
def test_delete_activity_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture):
242+
def test_delete_activity_should_return_422_for_invalid_id_format(client: FlaskClient,
243+
mocker: MockFixture,
244+
tenant_id: str,
245+
valid_header: dict):
196246
from time_tracker_api.activities.activities_namespace import activity_dao
197247
from werkzeug.exceptions import UnprocessableEntity
198248

@@ -202,8 +252,10 @@ def test_delete_activity_should_return_422_for_invalid_id_format(client: FlaskCl
202252
'delete',
203253
side_effect=UnprocessableEntity)
204254

205-
response = client.delete("/activities/%s" % invalid_id, follow_redirects=True)
255+
response = client.delete("/activities/%s" % invalid_id,
256+
headers=valid_header,
257+
follow_redirects=True)
206258

207259
assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code
208260
repository_remove_mock.assert_called_once_with(str(invalid_id),
209-
partition_key_value=current_user_tenant_id())
261+
partition_key_value=tenant_id)

0 commit comments

Comments
 (0)