Skip to content
This repository was archived by the owner on Dec 19, 2021. It is now read-only.

Commit 017b96e

Browse files
authored
Initial work to remove role column from UserModel (#358)
* Initial work to remove role column from UserModel We don't really have different roles so they are being removed. We have different types of users which have been implemented using STI. This is the first phase to remove roles from the backend. We will still have a role per se as far as the frontend code is concerned. The next phase will be to update the frontend to move off of integers as their Role, and use the typed strings. Along with changing any hard coded dependencies on Roles. The final phase will be back in the backend to remove the final remnants of Roles. Doing it in a 3 phase step as to not break anything for other developers :-) * Refactor UserLogin * We still need to assign a role on update Still need to assign a role until the frontend is updated not to use roles. * User singular name for user_type class
1 parent 7f6ac7d commit 017b96e

15 files changed

+130
-72
lines changed

app.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,27 @@ def create_app(env):
3232
# initialize Mail
3333
app.mail = Mail(app)
3434

35-
# check the user role in the JSON Web Token (JWT)
35+
@app.jwt.user_identity_loader
36+
def user_identity_lookup(user):
37+
return user.id
38+
39+
@app.jwt.user_lookup_loader
40+
def user_lookup_callback(_jwt_header, jwt_data):
41+
user = UserModel.query.filter_by(
42+
id=jwt_data["sub"], archived=False
43+
).one_or_none()
44+
if user and user.type == "user":
45+
return None
46+
else:
47+
return user
48+
3649
@app.jwt.additional_claims_loader
37-
def role_loader(identity):
38-
user = UserModel.find(identity)
50+
def role_loader(user):
3951
return {
4052
"email": user.email,
4153
"phone": user.phone,
4254
"firstName": user.firstName,
4355
"lastName": user.lastName,
44-
"role": user.role.value,
4556
}
4657

4758
# checking if the token's jti (jwt id) is in the set of revoked tokens

models/user.py

+34-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ def has_role(cls, role):
3333
return role in role_values
3434

3535

36+
class UserType(Enum):
37+
ADMIN = "admin"
38+
STAFF = "staff"
39+
PROPERTY_MANAGER = "property_manager"
40+
41+
@classmethod
42+
def get(cls, value, default=None):
43+
return cls._values().get(value, default)
44+
45+
@classmethod
46+
def _values(cls):
47+
return {member.value: member.value for member in cls.__members__.values()}
48+
49+
3650
class UserModel(BaseModel):
3751
__tablename__ = "users"
3852

@@ -117,6 +131,18 @@ def json(self, refresh_token=False):
117131
def serialize(self):
118132
return {}
119133

134+
@staticmethod
135+
def authenticate(user, password):
136+
if user and (user.archived or user.type == "user" or user.role is None):
137+
return {"message": "Invalid user"}, 403
138+
elif user and user.check_pw(password):
139+
access_token = create_access_token(identity=user, fresh=True)
140+
refresh_token = create_refresh_token(user)
141+
user.update_last_active()
142+
return {"access_token": access_token, "refresh_token": refresh_token}, 200
143+
else:
144+
return {"message": "Invalid credentials"}, 401
145+
120146
@classmethod
121147
def find_by_email(cls, email):
122148
return cls.query.filter_by(email=email).first()
@@ -149,11 +175,17 @@ def full_name(self):
149175
def is_admin(self):
150176
return False
151177

178+
def has_staff_privs(self):
179+
return False
180+
181+
def has_pm_privs(self):
182+
return False
183+
152184
def _make_token(self, refresh):
153185
if refresh:
154186
return {
155-
"access_token": create_access_token(identity=self.id, fresh=True),
156-
"refresh_token": create_refresh_token(self.id),
187+
"access_token": create_access_token(identity=self, fresh=True),
188+
"refresh_token": create_refresh_token(self),
157189
}
158190
else:
159191
return {}

models/users/admin.py

+6
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ class Admin(UserModel):
66

77
def is_admin(self):
88
return True
9+
10+
def has_staff_privs(self):
11+
return True
12+
13+
def has_pm_privs(self):
14+
return True

models/users/property_manager.py

+3
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ class PropertyManager(UserModel):
1414
viewonly=True,
1515
)
1616

17+
def has_pm_privs(self):
18+
return True
19+
1720
def serialize(self):
1821
return {"properties": self.properties.json(include_managers=False)}

models/users/staff.py

+6
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ class Staff(UserModel):
1313
collection_class=NobiruList,
1414
viewonly=True,
1515
)
16+
17+
def has_staff_privs(self):
18+
return True
19+
20+
def has_pm_privs(self):
21+
return True

resources/user.py

+23-23
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
from flask import request
33
from schemas import UserRegisterSchema, UserSchema
44
from utils.authorizations import admin_required, pm_level_required
5-
from models.user import UserModel, RoleEnum
5+
from models.user import UserModel, RoleEnum, UserType
66
from flask_jwt_extended import (
77
create_access_token,
8-
create_refresh_token,
9-
get_jwt_identity,
108
jwt_required,
9+
current_user,
1110
)
1211

1312

@@ -25,18 +24,24 @@ def get(self, id):
2524

2625
@pm_level_required
2726
def patch(self, id):
28-
current_user = UserModel.find(get_jwt_identity())
2927
user = UserModel.find(id)
3028
role = request.json.get("role")
29+
user_type = UserType.get(request.json.get("type"))
3130
password = request.json.get("new_password")
31+
role_change = role or user_type
3232

33-
if not self._authorized(current_user=current_user, user=user, role_change=role):
33+
if not self._authorized(
34+
current_user=current_user, user=user, role_change=role_change
35+
):
3436
return {"message": "Not Authorized"}, 403
3537

3638
if role:
3739
user.role = RoleEnum(role)
3840
user.type = RoleEnum(role).name.lower()
3941

42+
if user_type:
43+
user.type = user_type
44+
4045
if password:
4146
if password != request.json.get("confirm_password"):
4247
return {"message": "Unconfirmed password"}, 422
@@ -59,7 +64,7 @@ def _authorized(self, current_user, user, role_change):
5964
def delete(self, id):
6065
user = UserModel.find(id)
6166

62-
if user.id == get_jwt_identity():
67+
if user == current_user:
6368
return {"message": "Cannot delete self"}, 400
6469

6570
user.delete_from_db()
@@ -69,7 +74,7 @@ def delete(self, id):
6974
class ArchiveUser(Resource):
7075
@admin_required
7176
def post(self, user_id):
72-
if user_id == get_jwt_identity():
77+
if user_id == current_user.id:
7378
return {"message": "Cannot archive self"}, 400
7479

7580
user = UserModel.find(user_id)
@@ -83,18 +88,10 @@ def post(self, user_id):
8388

8489
class UserLogin(Resource):
8590
def post(self):
86-
user = UserModel.find_by_email(request.json.get("email", ""))
87-
88-
if user and (user.archived or user.role is None):
89-
return {"message": "Invalid user"}, 403
90-
91-
if user and user.check_pw(request.json.get("password", "")):
92-
access_token = create_access_token(identity=user.id, fresh=True)
93-
refresh_token = create_refresh_token(user.id)
94-
user.update_last_active()
95-
return {"access_token": access_token, "refresh_token": refresh_token}, 200
96-
97-
return {"message": "Invalid credentials"}, 401
91+
return UserModel.authenticate(
92+
UserModel.find_by_email(request.json.get("email", "")),
93+
request.json.get("password", ""),
94+
)
9895

9996

10097
# This endpoint allows the app to use a refresh token to get a new access token
@@ -107,19 +104,22 @@ class UserAccessRefresh(Resource):
107104
# to make a new access token for this identity.
108105
@jwt_required(refresh=True)
109106
def post(self):
110-
current_user = get_jwt_identity()
111107
ret = {"access_token": create_access_token(identity=current_user)}
112108
return ret, 200
113109

114110

115111
class Users(Resource):
116112
@admin_required
117113
def get(self):
118-
if RoleEnum.has_role(int(request.args["r"])):
114+
user_type = UserType.get(request.args.get("type"))
115+
# TODO: This is temporary. Switching to type from role lookup.
116+
if RoleEnum.has_role(int(request.args.get("r", 99))):
119117
return {
120118
"users": UserModel.query.filter_by(
121119
role=RoleEnum(int(request.args["r"]))
122120
).json()
123121
}
124-
else:
125-
return {"message": "Invalid role"}, 400
122+
elif user_type:
123+
return {"users": UserModel.query.filter_by(type=user_type).json()}
124+
125+
return {"message": "Invalid role"}, 400

schemas/property_assignment.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from ma import ma
22
from models.property_assignment import PropertyAssignment
3-
from models.user import UserModel, RoleEnum
3+
from models.user import UserModel
4+
from models.users.property_manager import PropertyManager
45
from models.property import PropertyModel
56
from marshmallow import fields, validates, ValidationError
67

@@ -26,6 +27,5 @@ def validate_is_valid_property(self, value):
2627

2728
@validates("manager_id")
2829
def validate_role_property_manager(self, value):
29-
user = UserModel.query.get(value)
30-
if user.role != RoleEnum.PROPERTY_MANAGER:
30+
if not PropertyManager.query.get(value):
3131
raise ValidationError("User is not a property manager")

schemas/staff_tenants.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from models.tenant import TenantModel
2-
from models.user import UserModel, RoleEnum
2+
from models.users.staff import Staff
33
from marshmallow import Schema, fields, validates, ValidationError
44

55

@@ -19,9 +19,5 @@ def validate_tenant(self, value):
1919

2020
@validates("staff")
2121
def validate_staff(self, value):
22-
staff_query = UserModel.query.filter(
23-
UserModel.id.in_(value), UserModel.role == RoleEnum.STAFF
24-
)
25-
26-
if not len(staff_query.all()) == len(value):
22+
if not len(Staff.query.filter(Staff.id.in_(value)).all()) == len(value):
2723
raise ValidationError(f"{value} contains invalid staff IDs")

schemas/user.py

+4
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ class UserRegisterSchema(UserSchema):
3333
@validates("role")
3434
def user_cannot_register_a_role(self, _):
3535
raise ValidationError("Role is not allowed")
36+
37+
@validates("type")
38+
def user_cannot_register_a_type(self, _):
39+
raise ValidationError("Type is not allowed")

shelltools.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from db import db
2-
from models.user import UserModel, RoleEnum
2+
from models.user import UserModel, UserType
33
from models.users.admin import Admin
44
from models.users.property_manager import PropertyManager
55
from models.users.staff import Staff

tests/authorizations/test_user_authentication.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ def test_jwt_refresh(self, header, create_user):
188188
"""
189189

190190
user = create_user()
191-
original_access_token = create_access_token(identity=user.id, fresh=True)
192-
original_refresh_token = create_refresh_token(user.id)
191+
original_access_token = create_access_token(identity=user, fresh=True)
192+
original_refresh_token = create_refresh_token(user)
193193

194194
newPhone = "555-555-5555"
195195

tests/integration/test_users.py

+13
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ def test_get(self, create_admin_user, header):
8989
)
9090
assert is_valid(unknown_user_response, 400)
9191

92+
def test_get_with_type(self, create_join_staff, valid_header):
93+
staff = create_join_staff()
94+
response = self.client.get("/api/user?type=staff", headers=valid_header)
95+
96+
assert response.status_code == 200
97+
assert response.json == {"users": [staff.json()]}
98+
99+
def test_get_with_invalid_type(self, create_join_staff, valid_header):
100+
response = self.client.get("/api/user?type=big_honcho", headers=valid_header)
101+
102+
assert response.status_code == 400
103+
assert response.json == {"message": "Invalid role"}
104+
92105

93106
@pytest.mark.usefixtures("client_class", "empty_test_db")
94107
class TestArchiveUser:

tests/schemas/test_user_schema.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from schemas.user import UserSchema, UserRegisterSchema
2-
from models.user import RoleEnum
2+
from models.user import RoleEnum, UserType
33

44

55
def user_register_valid_payload(user):
@@ -74,6 +74,14 @@ def test_presence_of_role_key_is_not_valid(self):
7474
assert "role" in validation_errors
7575
assert validation_errors["role"] == ["Role is not allowed"]
7676

77+
def test_presence_of_type_is_not_valid(self):
78+
payload = {"type": UserType.ADMIN.value}
79+
80+
validation_errors = UserRegisterSchema().validate(payload)
81+
82+
assert "type" in validation_errors
83+
assert validation_errors["type"] == ["Type is not allowed"]
84+
7785
def test_password_is_required(self):
7886
validation_errors = UserRegisterSchema().validate({})
7987

tests/unit/test_user.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import jwt
33
import time
44
from unittest.mock import patch
5-
from models.user import UserModel, RoleEnum
5+
from models.user import UserModel
66
from schemas.user import UserSchema
77
from freezegun import freeze_time
88
from tests.unit.base_interface_test import BaseInterfaceTest
@@ -73,7 +73,7 @@ class TestFixtures:
7373
def test_create_admin_user(self, create_admin_user):
7474
admin = create_admin_user()
7575
assert admin
76-
assert admin.role == RoleEnum.ADMIN
76+
assert admin.type == "admin"
7777

7878
def test_multiple_admins_can_be_created():
7979
return create_admin_user()
@@ -83,7 +83,7 @@ def test_multiple_admins_can_be_created():
8383
def test_create_join_staff(self, create_join_staff):
8484
staff = create_join_staff()
8585
assert staff
86-
assert staff.role == RoleEnum.STAFF
86+
assert staff.type == "staff"
8787

8888
def test_multiple_join_staff_can_be_created():
8989
return create_join_staff()
@@ -93,7 +93,7 @@ def test_multiple_join_staff_can_be_created():
9393
def test_create_property_manager(self, create_property_manager):
9494
pm = create_property_manager()
9595
assert pm
96-
assert pm.role == RoleEnum.PROPERTY_MANAGER
96+
assert pm.type == "property_manager"
9797

9898
def test_multiple_pms_can_be_created():
9999
return create_property_manager()
@@ -102,7 +102,7 @@ def test_multiple_pms_can_be_created():
102102

103103
def test_create_unauthorized_user(self, create_unauthorized_user):
104104
user = create_unauthorized_user()
105-
assert user.role is None
105+
assert user.type == "user"
106106

107107
def test_multiple_users_can_be_created():
108108
return create_unauthorized_user()

0 commit comments

Comments
 (0)