Skip to content

Commit 3aa2f91

Browse files
committed
feat: Add user role to JWT token payload.
* This is needed for scoping RBAC for quads-client Change-Id: I3a3769199ee4ec8a53f28912e121e427c798c6f6
1 parent 5468f1d commit 3aa2f91

5 files changed

Lines changed: 83 additions & 18 deletions

File tree

src/quads/server/blueprints/auth.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def register() -> Response:
4040
try:
4141
user = user_datastore.create_user(email=data["email"], password=data["password"], roles=[role])
4242
db.session.commit()
43-
auth_token = User.encode_auth_token(user.id)
43+
auth_token = User.encode_auth_token(user.email, role.name)
4444
response_object = {
4545
"status": "success",
4646
"status_code": 200,
@@ -77,7 +77,8 @@ def login() -> Response:
7777
try:
7878
current_user = basic_auth.username()
7979
user = db.session.query(User).filter(User.email == current_user).first()
80-
auth_token = User.encode_auth_token(user.email)
80+
user_role = user.roles[0].name if user.roles else None
81+
auth_token = User.encode_auth_token(user.email, user_role)
8182
if auth_token:
8283
response_object = {
8384
"status_code": 201,

src/quads/server/database.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ def populate(user_datastore):
4848
admin_role = create_role(admin_role_name, admin_role_description)
4949
user_role = create_role(user_role_name, user_role_description)
5050

51-
regular_user = "gonza@redhat.com"
52-
admin_user = "grafuls@redhat.com"
51+
regular_user = "gonza@example.com"
52+
admin_user = "grafuls@example.com"
5353

5454
admin_user_added = create_user(user_datastore, admin_user, "password", [admin_role])
5555
regular_user_added = create_user(user_datastore, regular_user, "password", [user_role])

src/quads/server/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,15 @@ def verify_password(self, password):
286286
return check_password_hash(self._password, password)
287287

288288
@staticmethod
289-
def encode_auth_token(user_email):
289+
def encode_auth_token(user_email, user_role=None):
290290
try:
291291
payload = {
292292
"exp": datetime.utcnow() + timedelta(days=0, seconds=15600),
293293
"iat": datetime.utcnow(),
294294
"sub": user_email,
295295
}
296+
if user_role:
297+
payload["role"] = user_role
296298
return encode(payload, current_app.config.get("SECRET_KEY"), algorithm="HS256")
297299
except Exception as e:
298300
return e

tests/api/test_auth.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import base64
22
from unittest.mock import patch
33

4+
from jwt import decode
45
from sqlalchemy.exc import SQLAlchemyError
56

67
from tests.config import EXPIRED_TEST_TOKEN
@@ -26,7 +27,7 @@ def raise_exception_stub(ignore1=None):
2627

2728
class UserClassStub:
2829
id = 0
29-
email = "test@redhat.com"
30+
email = "test@example.com"
3031
password = "12345"
3132
active = False
3233

@@ -41,7 +42,7 @@ def get_id(self):
4142

4243
@staticmethod
4344
def decode_auth_token(ignore1=None):
44-
return "test@redhat.com"
45+
return "test@example.com"
4546

4647

4748
class SQLResultStub:
@@ -56,7 +57,7 @@ def filter(self, *ignore):
5657

5758

5859
def query_stub(ignore=None):
59-
user = UserClassStub(1, "test@redhat.com", "password", False)
60+
user = UserClassStub(1, "test@example.com", "password", False)
6061
return SQLResultStub([user])
6162

6263

@@ -118,7 +119,7 @@ def test_invalid_credentials(self, test_client):
118119
| WHEN: User tries to access an endpoint with basic auth and wrong password
119120
| THEN: User should not be able to access the endpoint
120121
"""
121-
credentials = base64.b64encode(b"gonza@redhat.com:12345").decode("utf-8")
122+
credentials = base64.b64encode(b"gonza@example.com:12345").decode("utf-8")
122123
response = unwrap_json(
123124
test_client.post(
124125
"/api/v3/clouds",
@@ -136,7 +137,7 @@ def test_invalid_wrong_role(self, test_client):
136137
| WHEN: User tries to access an endpoint with basic auth, but doesn't have the required role
137138
| THEN: User should not be able to access the endpoint
138139
"""
139-
credentials = base64.b64encode(b"gonza@redhat.com:password").decode("utf-8")
140+
credentials = base64.b64encode(b"gonza@example.com:password").decode("utf-8")
140141
response = unwrap_json(
141142
test_client.post(
142143
"/api/v3/clouds",
@@ -175,7 +176,7 @@ def test_invalid_inactive_user(self, db_session, test_client):
175176
| THEN: User should not be able to access the endpoint
176177
"""
177178
db_session.query.return_value.filter.return_value.first.return_value = UserClassStub(
178-
id=1, email="test@redhat.com", password="password", active=False
179+
id=1, email="test@example.com", password="password", active=False
179180
)
180181
response = unwrap_json(
181182
test_client.post(
@@ -231,14 +232,33 @@ def test_valid(self, test_client):
231232
response = unwrap_json(
232233
test_client.post(
233234
"/api/v3/register",
234-
json=dict(email="test_user@redhat.com", password="password"),
235+
json=dict(email="test_user@example.com", password="password"),
235236
)
236237
)
237238
assert response.status_code == 200
238239
assert response.json["status"] == "success"
239240
assert response.json["message"] == "Successfully registered"
240241
assert response.json["auth_token"] is not None
241242

243+
def test_jwt_contains_role(self, test_client):
244+
"""
245+
| GIVEN: Client with defaults in database
246+
| WHEN: User registers with valid email and password
247+
| THEN: JWT token should contain role field with value "user"
248+
"""
249+
response = unwrap_json(
250+
test_client.post(
251+
"/api/v3/register",
252+
json=dict(email="new_test_user@example.com", password="password"),
253+
)
254+
)
255+
assert response.status_code == 200
256+
auth_token = response.json["auth_token"]
257+
payload = decode(auth_token, options={"verify_signature": False})
258+
assert "role" in payload
259+
assert payload["role"] == "user"
260+
assert payload["sub"] == "new_test_user@example.com"
261+
242262
def test_existing(self, test_client):
243263
"""
244264
| GIVEN: Client with test user in database
@@ -248,7 +268,7 @@ def test_existing(self, test_client):
248268
response = unwrap_json(
249269
test_client.post(
250270
"/api/v3/register",
251-
json=dict(email="test_user@redhat.com", password="password"),
271+
json=dict(email="test_user@example.com", password="password"),
252272
)
253273
)
254274
assert response.status_code == 401
@@ -263,7 +283,7 @@ def test_invalid(self, test_client):
263283
| WHEN: User tries to log in with invalid credentials.
264284
| THEN: User should not be able to log in due to failed basic auth
265285
"""
266-
invalid_credentials = base64.b64encode(b"none@redhat.com:wrong_password").decode("utf-8")
286+
invalid_credentials = base64.b64encode(b"none@example.com:wrong_password").decode("utf-8")
267287
response = unwrap_json(
268288
test_client.post(
269289
"/api/v3/login",
@@ -281,7 +301,7 @@ def test_invalid_exception(self, test_client):
281301
| WHEN: User tries to log in with valid credentials.
282302
| THEN: User should not be able to log in due unexpected exception
283303
"""
284-
valid_credentials = base64.b64encode(b"grafuls@redhat.com:password").decode("utf-8")
304+
valid_credentials = base64.b64encode(b"grafuls@example.com:password").decode("utf-8")
285305
response = unwrap_json(
286306
test_client.post(
287307
"/api/v3/login",
@@ -299,7 +319,7 @@ def test_valid(self, test_client):
299319
| WHEN: User tries to log in with valid email and password
300320
| THEN: User should be able to log in
301321
"""
302-
valid_credentials = base64.b64encode(b"grafuls@redhat.com:password").decode("utf-8")
322+
valid_credentials = base64.b64encode(b"grafuls@example.com:password").decode("utf-8")
303323
response = unwrap_json(
304324
test_client.post(
305325
"/api/v3/login",
@@ -314,6 +334,48 @@ def test_valid(self, test_client):
314334
global auth_token_global
315335
auth_token_global = response.json["auth_token"]
316336

337+
def test_admin_jwt_contains_role(self, test_client):
338+
"""
339+
| GIVEN: Client with admin user in database
340+
| WHEN: Admin user logs in
341+
| THEN: JWT token should contain role field with value "admin"
342+
"""
343+
valid_credentials = base64.b64encode(b"grafuls@example.com:password").decode("utf-8")
344+
response = unwrap_json(
345+
test_client.post(
346+
"/api/v3/login",
347+
json=dict(),
348+
headers={"Authorization": "Basic " + valid_credentials},
349+
)
350+
)
351+
assert response.status_code == 200
352+
auth_token = response.json["auth_token"]
353+
payload = decode(auth_token, options={"verify_signature": False})
354+
assert "role" in payload
355+
assert payload["role"] == "admin"
356+
assert payload["sub"] == "grafuls@example.com"
357+
358+
def test_user_jwt_contains_role(self, test_client):
359+
"""
360+
| GIVEN: Client with regular user in database
361+
| WHEN: Regular user logs in
362+
| THEN: JWT token should contain role field with value "user"
363+
"""
364+
valid_credentials = base64.b64encode(b"gonza@example.com:password").decode("utf-8")
365+
response = unwrap_json(
366+
test_client.post(
367+
"/api/v3/login",
368+
json=dict(),
369+
headers={"Authorization": "Basic " + valid_credentials},
370+
)
371+
)
372+
assert response.status_code == 200
373+
auth_token = response.json["auth_token"]
374+
payload = decode(auth_token, options={"verify_signature": False})
375+
assert "role" in payload
376+
assert payload["role"] == "user"
377+
assert payload["sub"] == "gonza@example.com"
378+
317379

318380
class TestLogout:
319381
def test_invalid_no_token(self, test_client):

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def __init__(self, client):
5151
self._token = None
5252
self._username = ""
5353

54-
def login(self, username="grafuls@redhat.com", password="password"):
54+
def login(self, username="grafuls@example.com", password="password"):
5555
valid_credentials = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
5656
response = unwrap_json(
5757
self._client.post(
@@ -72,7 +72,7 @@ def logout(self):
7272
self._token = None
7373
self._username = ""
7474

75-
def get_auth_header(self, username="grafuls@redhat.com", password="password"):
75+
def get_auth_header(self, username="grafuls@example.com", password="password"):
7676
if self._token is None and self._username == "":
7777
self.login(username, password)
7878
if self._username != username:

0 commit comments

Comments
 (0)