Skip to content

Commit cf79a5f

Browse files
committed
abort on update to protected properties, some refactoring
1 parent 584a3e0 commit cf79a5f

File tree

3 files changed

+99
-84
lines changed

3 files changed

+99
-84
lines changed

lib/pbench/server/api/resources/users_api.py

Lines changed: 44 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from flask import request, jsonify, make_response
33
from flask_restful import Resource, abort
44
from flask_bcrypt import check_password_hash
5-
from email_validator import validate_email, EmailNotValidError
5+
from email_validator import EmailNotValidError
66
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
77
from configparser import NoOptionError
88
from pbench.server.database.models.users import User
@@ -119,40 +119,19 @@ def post(self):
119119
400, message="Missing lastName field",
120120
)
121121

122-
# validate the email field
123-
try:
124-
valid = validate_email(email_id)
125-
email = valid.email
126-
except EmailNotValidError:
127-
self.logger.warning("Invalid email {}", email_id)
128-
abort(400, message=f"Invalid email: {email_id}")
129-
130-
# check if user already exist
131-
try:
132-
user = User.query(username=user_data.get("username"))
133-
except Exception:
134-
self.logger.exception("Exception while querying user")
135-
abort(500, message="INTERNAL ERROR")
136-
137-
if user:
138-
self.logger.warning(
139-
"A user tried to re-register. Username: {}", user.username
140-
)
141-
abort(403, message="A user with that name already exists.")
142-
143122
try:
144123
user = User(
145124
username=username,
146125
password=password,
147126
first_name=first_name,
148127
last_name=last_name,
149-
email=email,
128+
email=email_id,
150129
)
151130

152131
# insert the user
153132
user.add()
154133
self.logger.info(
155-
"New user registered, username: {}, email: {}", username, email
134+
"New user registered, username: {}, email: {}", username, email_id
156135
)
157136

158137
response_object = {
@@ -162,6 +141,9 @@ def post(self):
162141
response = jsonify(response_object)
163142
response.status_code = 201
164143
return make_response(response, 201)
144+
except EmailNotValidError:
145+
self.logger.warning("Invalid email {}", email_id)
146+
abort(400, message=f"Invalid email: {email_id}")
165147
except Exception:
166148
self.logger.exception("Exception while registering a user")
167149
abort(500, message="INTERNAL ERROR")
@@ -385,17 +367,27 @@ def get(self, username):
385367
abort(500, message="INTERNAL ERROR")
386368

387369
# TODO: Check if the user has the right privileges
388-
if verified or user.is_admin():
370+
if verified:
371+
return_data = user.get_json()
389372
response_object = {
390373
"status": "success",
391-
"data": {
392-
"email": user.email,
393-
"first_name": user.first_name,
394-
"last_name": user.last_name,
395-
"registered_on": user.registered_on,
396-
},
374+
"data": return_data,
397375
}
398376
return make_response(jsonify(response_object), 200)
377+
elif user.is_admin():
378+
try:
379+
target_user = User.query(username=username)
380+
return_data = target_user.get_json()
381+
response_object = {
382+
"status": "success",
383+
"data": return_data,
384+
}
385+
return make_response(jsonify(response_object), 200)
386+
except Exception:
387+
self.logger.exception(
388+
"Exception occurred while querying the user. Username: {}", username
389+
)
390+
abort(500, message="INTERNAL ERROR")
399391
else:
400392
self.logger.warning(
401393
"Username retrieved from the auth token {} is different from the username provided",
@@ -448,23 +440,28 @@ def put(self, username):
448440

449441
# TODO: Check if the user has the right privileges
450442
if verified:
451-
try:
452-
# Log if the user payload contain fields that are either non-updatabale or
453-
# are not present in the user db.
454-
non_updatable = list(
455-
set(post_data.keys()) - set(User.__table__.columns.keys())
443+
# Check if the user payload contain fields that are either non-updatabale or
444+
# are not present in the user db. If any key in the payload does not match
445+
# with the column name we will abort the update request.
446+
non_updatable = list(
447+
set(post_data.keys()) - set(User.__table__.columns.keys())
448+
)
449+
if non_updatable:
450+
self.logger.warning(
451+
"User trying to update fields that are not present in the user database. Fields: {}",
452+
non_updatable,
456453
)
457-
if "registered_on" in post_data.keys():
458-
non_updatable.append("registered_on")
459-
if non_updatable:
460-
self.logger.warning(
461-
"User trying to update fields that are either non-updatable or does not present in the user database. Fields: {}",
462-
non_updatable,
463-
)
454+
abort(400, message="Invalid fields in update request payload")
455+
try:
464456
# We will update the user object with the keys and values provided in the request payload.
465-
# THe keys need to match with the column names in the user model. However, if any key in
466-
# the payload does not match with the column name we just skip that field.
457+
# THe keys need to match with the column names in the user model.
467458
user.update(**post_data)
459+
except ValueError:
460+
self.logger.warning(
461+
"Either provided values to update the user does not adhere to the user model "
462+
"datatype or user attempted to update the protected properties."
463+
)
464+
abort(400, message="Invalid fields in update request payload")
468465
except Exception:
469466
self.logger.exception("Exception occurred during updating user object")
470467
abort(500, message="INTERNAL ERROR")
@@ -477,15 +474,10 @@ def put(self, username):
477474
403, message="Authentication token does not belong to the current user"
478475
)
479476

477+
return_data = user.get_json()
480478
response_object = {
481479
"status": "success",
482-
"data": {
483-
"username": user.username,
484-
"email": user.email,
485-
"first_name": user.first_name,
486-
"last_name": user.last_name,
487-
"registered_on": user.registered_on,
488-
},
480+
"data": return_data,
489481
}
490482
return make_response(jsonify(response_object), 200)
491483

lib/pbench/server/database/models/users.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import datetime
22
from flask_bcrypt import generate_password_hash
3+
from email_validator import validate_email
34
from sqlalchemy import Column, Integer, String, DateTime, LargeBinary
45
from pbench.server.database.database import Database
56
from sqlalchemy.orm import relationship
7+
from sqlalchemy.orm import validates
68

79

810
class User(Database.Base):
@@ -31,17 +33,27 @@ def __init__(self, **kwargs):
3133
def __str__(self):
3234
return f"User, id: {self.id}, username: {self.username}"
3335

36+
def get_json(self):
37+
return {
38+
"username": self.username,
39+
"email": self.email,
40+
"first_name": self.first_name,
41+
"last_name": self.last_name,
42+
"registered_on": self.registered_on,
43+
}
44+
3445
@staticmethod
35-
def query(**kwargs):
46+
def query(id=None, username=None, email=None):
3647
# Currently we would only query with single argument. Argument need to be either username/id/email
3748
try:
38-
key, value = next(iter(kwargs.items()))
39-
if key == "username":
40-
user = Database.db_session.query(User).filter_by(username=value).first()
41-
elif key == "id":
42-
user = Database.db_session.query(User).filter_by(id=value).first()
43-
elif key == "email":
44-
user = Database.db_session.query(User).filter_by(email=value).first()
49+
if username:
50+
user = (
51+
Database.db_session.query(User).filter_by(username=username).first()
52+
)
53+
elif id:
54+
user = Database.db_session.query(User).filter_by(id=id).first()
55+
elif email:
56+
user = Database.db_session.query(User).filter_by(email=email).first()
4557
else:
4658
user = None
4759
except Exception as e:
@@ -60,6 +72,25 @@ def add(self):
6072
Database.db_session.rollback()
6173
raise e
6274

75+
# Prevent update on "registered_on" field
76+
@validates("registered_on")
77+
def evaluate_registered_on(self, key, value):
78+
if self.registered_on: # Field already exists
79+
if self.registered_on.strftime("%Y-%m-%d %H:%M:%S.%f") != value:
80+
raise ValueError("registered_on cannot be modified.")
81+
return value
82+
83+
# validate the email field
84+
@validates("email")
85+
def evaluate_email(self, key, value):
86+
try:
87+
valid = validate_email(value)
88+
email = valid.email
89+
except Exception as e:
90+
raise e
91+
else:
92+
return email
93+
6394
def update(self, **kwargs):
6495
"""
6596
Update the current user object with given keyword arguments
@@ -72,9 +103,6 @@ def update(self, **kwargs):
72103
Database.db_session.add(value)
73104
elif key == "password":
74105
setattr(self, key, generate_password_hash(value))
75-
# Prevent update on "registered_on" field
76-
elif key == "registered_on":
77-
continue
78106
else:
79107
setattr(self, key, value)
80108
Database.db_session.commit()

lib/pbench/test/unit/server/test_user_auth.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -161,34 +161,26 @@ def test_user_relogin(client, server_config):
161161
assert response.content_type == "application/json"
162162
assert response.status_code == 200
163163

164-
# Immediate re-login with auth header
164+
# Re-login with auth header
165+
time.sleep(1)
165166
response = client.post(
166167
f"{server_config.rest_uri}/login",
167168
headers=dict(Authorization="Bearer " + data["auth_token"]),
168169
json={"username": "user", "password": "12345"},
169170
)
170171
data = response.json
171-
assert data["message"] == "Retry login after some time"
172-
assert response.status_code == 403
172+
assert data["message"] == "Successfully logged in."
173+
assert response.status_code == 200
173174

174-
# Re-login after a second
175+
# Re-login without auth header
175176
time.sleep(1)
176177
response = client.post(
177178
f"{server_config.rest_uri}/login",
178179
json={"username": "user", "password": "12345"},
179180
)
180181
data = response.json
181-
assert data["auth_token"]
182-
assert data["status"] == "success"
183-
184-
# Immediate re-login without auth header
185-
response = client.post(
186-
f"{server_config.rest_uri}/login",
187-
json={"username": "user", "password": "12345"},
188-
)
189-
data = response.json
190-
assert data["message"] == "Retry login after some time"
191-
assert response.status_code == 409
182+
assert data["message"] == "Successfully logged in."
183+
assert response.status_code == 200
192184

193185
@staticmethod
194186
def test_user_login_with_wrong_password(client, server_config):
@@ -261,6 +253,8 @@ def test_get_user(client, server_config):
261253
assert data["status"] == "success"
262254
assert data["data"] is not None
263255
assert data["data"]["email"] == "[email protected]"
256+
assert data["data"]["username"] == "username"
257+
assert data["data"]["first_name"] == "firstname"
264258
assert response.status_code == 200
265259

266260
@staticmethod
@@ -291,19 +285,20 @@ def test_update_user(client, server_config):
291285
headers=dict(Authorization="Bearer " + data_login["auth_token"]),
292286
)
293287
data = response.json
294-
assert data["status"] == "success"
295-
assert data["data"] is not None
296-
assert data["data"]["first_name"] == "newname"
297-
# registered_on is not updatable
298-
assert data["data"]["registered_on"] != new_registration_time
299-
assert response.status_code == 200
288+
assert response.status_code == 400
289+
assert data["message"] == "Invalid fields in update request payload"
300290

301291
# Test password update
302292
response = client.put(
303293
f"{server_config.rest_uri}/user/username",
304294
json={"password": "newpass"},
305295
headers=dict(Authorization="Bearer " + data_login["auth_token"]),
306296
)
297+
data = response.json
298+
assert response.status_code == 200
299+
assert data["status"] == "success"
300+
assert data["data"]["first_name"] == "firstname"
301+
assert data["data"]["email"] == "[email protected]"
307302
time.sleep(1)
308303
response = login_user(client, server_config, "username", "newpass")
309304
data_login = response.json

0 commit comments

Comments
 (0)