Skip to content

Commit 2bebab0

Browse files
authored
Merge pull request #318 from CodeForPhilly/290-user_admin
290 user admin
2 parents 414d7fc + 4823510 commit 2bebab0

File tree

3 files changed

+173
-49
lines changed

3 files changed

+173
-49
lines changed

src/server/api/user_api.py

Lines changed: 147 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,32 @@ def log_user_action(user, event_class, detail):
3333
except Exception as e:
3434
print(e)
3535

36+
def password_is_strong(password):
37+
""" Check plain-text password against strength rules."""
38+
39+
def has_digit(test_string):
40+
"""Test if any character is a digit."""
41+
for c in test_string:
42+
if c.isdigit():
43+
return True
44+
return False
45+
46+
def has_alpha(test_string):
47+
"""Test if any character is alphabetic."""
48+
for c in test_string:
49+
if c.isalpha():
50+
return True
51+
return False
52+
53+
if (len(password) > 11
54+
# and has_alpha(password)
55+
# and has_digit(password)
56+
):
57+
return True
58+
59+
else:
60+
return False
61+
3662

3763
def hash_password(password):
3864
""" Generate salt+hash for storing in db"""
@@ -191,6 +217,7 @@ def user_refresh():
191217

192218
if is_active[0].lower() == 'y': # In the user DB and still Active?
193219
token = jwt_ops.create_token(user_name,old_jwt['role'])
220+
log_user_action(user_name, "Success", "Refreshed token")
194221
return token
195222

196223
else:
@@ -208,7 +235,7 @@ def user_create():
208235
209236
Requires admin role
210237
211-
Form POST Parameters
238+
Form POST JSON Parameters
212239
----------
213240
username : str
214241
full_name : str
@@ -222,12 +249,18 @@ def user_create():
222249
Duplicate user: 409 + DB error
223250
224251
"""
225-
new_user = request.form["username"]
226-
fullname = request.form["full_name"]
227-
userpw = request.form["password"]
228-
user_role = request.form["role"]
229252

230-
requesting_user = jwt_ops.get_jwt_user()
253+
try:
254+
post_dict = json.loads(request.data)
255+
new_user = post_dict["username"]
256+
fullname = post_dict["full_name"]
257+
userpw = post_dict["password"]
258+
user_role = post_dict["role"]
259+
except:
260+
return jsonify("Missing one or more parameters"), 400
261+
262+
263+
requesting_user = jwt_ops.validate_decode_jwt()['sub']
231264

232265
pw_hash = hash_password(userpw)
233266

@@ -287,31 +320,100 @@ def get_user_count():
287320
return jsonify(user_count[0])
288321

289322

290-
# TODO: A single do-all update_user()
291-
@user_api.route("/api/admin/user/deactivate", methods=["POST"])
323+
@user_api.route("/api/admin/user/check_name", methods=["POST"])
292324
@jwt_ops.admin_required
293-
def user_deactivate():
294-
"""Mark user as inactive in DB"""
295-
# TODO
296-
return "", 200
325+
def check_username():
326+
"""Return 1 if username exists already, else 0."""
297327

328+
try:
329+
post_dict = json.loads(request.data)
330+
test_username = post_dict["username"]
331+
except:
332+
return jsonify("Missing username"), 400
298333

299-
@user_api.route("/api/admin/user/activate", methods=["POST"])
300-
@jwt_ops.admin_required
301-
def user_activate():
302-
"""Mark user as active in DB"""
303-
# TODO
304-
return "", 200
334+
with engine.connect() as connection:
335+
336+
s = text( """select count(username) from pdp_users where username=:u """ )
337+
s = s.bindparams(u=test_username)
338+
result = connection.execute(s)
339+
340+
if result.rowcount: # As we're doing a count() we *should* get a result
341+
user_exists = result.fetchone()[0]
342+
else:
343+
log_user_action(test_username, "Failure", "Error when checking username")
344+
return jsonify("Error checking username"), 500
345+
346+
return jsonify(user_exists)
347+
348+
@user_api.route("/api/admin/user/update", methods=["POST"])
349+
@jwt_ops.admin_required
350+
def user_update():
351+
"""Update existing user record
352+
"""
353+
354+
post_dict = json.loads(request.data)
355+
356+
try:
357+
username = post_dict["username"]
358+
except:
359+
return jsonify("Must specify username"), 400
360+
361+
update_dict = {}
362+
363+
# Need to be a bit defensive here & select what we want instead of taking what we're given
364+
for key in ["full_name", "active", "role", "password"]:
365+
try:
366+
val = post_dict[key]
367+
update_dict[key] = val
368+
except:
369+
pass
370+
371+
372+
if not update_dict:
373+
return jsonify("No changed items specified") # If nothing to do, declare victory
374+
375+
if "password" in update_dict.keys():
376+
377+
if password_is_strong(update_dict['password']):
378+
update_dict['password'] = hash_password(update_dict['password'])
379+
else:
380+
return jsonify("Password too weak")
381+
382+
383+
384+
# We have a variable number of columns to update.
385+
# We could generate a text query on the fly, but this seems the perfect place to use the ORM
386+
# and let it handle the update for us.
387+
388+
from sqlalchemy import update
389+
from sqlalchemy.orm import Session, sessionmaker
390+
391+
Session = sessionmaker(engine)
392+
393+
session = Session()
394+
# #TODO: Figure out why context manager doesn't work or do try/finally
395+
396+
PU = Table("pdp_users", metadata, autoload=True, autoload_with=engine)
397+
# pr = Table("pdp_user_roles", metadata, autoload=True, autoload_with=engine)
398+
399+
#TODO: Check tendered role or join roles table for update
400+
401+
stmt = update(PU).where(PU.columns.username == username).values(update_dict).\
402+
execution_options(synchronize_session="fetch")
403+
404+
result = session.execute(stmt)
405+
406+
session.commit()
407+
session.close()
408+
409+
return jsonify("Updated")
305410

306411

307412
@user_api.route("/api/admin/user/get_users", methods=["GET"])
308413
@jwt_ops.admin_required
309414
def user_get_list():
310415
"""Return list of users"""
311416

312-
# pu = Table("pdp_users", metadata, autoload=True, autoload_with=engine)
313-
# pr = Table("pdp_user_roles", metadata, autoload=True, autoload_with=engine)
314-
315417
with engine.connect() as connection:
316418

317419
s = text(
@@ -331,3 +433,27 @@ def user_get_list():
331433

332434
return jsonify(ul), 200
333435

436+
@user_api.route("/api/admin/user/get_info/<string:username>", methods=["GET"])
437+
@jwt_ops.admin_required
438+
def user_get_info(username):
439+
"""Return info on a specified user"""
440+
441+
with engine.connect() as connection:
442+
443+
s = text(
444+
""" select username, full_name, active, pr.role
445+
from pdp_users as pu
446+
left join pdp_user_roles as pr on pu.role = pr._id
447+
where username=:u
448+
"""
449+
)
450+
s = s.bindparams(u=username)
451+
result = connection.execute(s)
452+
453+
if result.rowcount:
454+
user_row = result.fetchone()
455+
else:
456+
log_user_action(username, "Failure", "Error when getting user info")
457+
return jsonify("Username not found"), 400
458+
459+
return jsonify( dict(zip(result.keys(), user_row)) ), 200

src/server/test_api.py

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -170,37 +170,35 @@ def test_admingetusers(state: State):
170170
userlist = response.json()
171171
assert len(userlist) > 1
172172

173-
# Endpoints not implemented yet
174-
175-
# def test_check_usernames(state: State):
176-
# """Verify logged-in base_admin can test usernames, gets correct result - existing user """
177-
# # Build auth string value including token from state
178-
# b_string = 'Bearer ' + state.state['base_admin']
173+
def test_check_usernames(state: State):
174+
"""Verify logged-in base_admin can test usernames, gets correct result - existing user """
175+
# Build auth string value including token from state
176+
b_string = 'Bearer ' + state.state['base_admin']
179177

180-
# assert len(b_string) > 24
178+
assert len(b_string) > 24
181179

182-
# auth_hdr = {'Authorization' : b_string}
180+
auth_hdr = {'Authorization' : b_string}
183181

184-
# data = {"username":"base_admin"}
185-
# response = requests.post(SERVER_URL + "/api/admin/user/check_name", headers=auth_hdr, json=data)
186-
# assert response.status_code == 200
182+
data = {"username":"base_admin"}
183+
response = requests.post(SERVER_URL + "/api/admin/user/check_name", headers=auth_hdr, json=data)
184+
assert response.status_code == 200
187185

188-
# is_user = response.json()
189-
# assert is_user == 1
186+
is_user = response.json()
187+
assert is_user == 1
190188

191-
# def test_check_badusernames(state: State):
192-
# """Verify logged-in base_admin can test usernames, gets correct result - nonexistant user """
193-
# # Build auth string value including token from state
194-
# b_string = 'Bearer ' + state.state['base_admin']
195-
# assert len(b_string) > 24
196-
# auth_hdr = {'Authorization' : b_string}
189+
def test_check_badusernames(state: State):
190+
"""Verify logged-in base_admin can test usernames, gets correct result - nonexistant user """
191+
# Build auth string value including token from state
192+
b_string = 'Bearer ' + state.state['base_admin']
193+
assert len(b_string) > 24
194+
auth_hdr = {'Authorization' : b_string}
197195

198-
# data = {"username":"got_no_username_like_this"}
199-
# response = requests.post(SERVER_URL + "/api/admin/user/check_name", headers=auth_hdr, json=data)
200-
# assert response.status_code == 200
196+
data = {"username":"got_no_username_like_this"}
197+
response = requests.post(SERVER_URL + "/api/admin/user/check_name", headers=auth_hdr, json=data)
198+
assert response.status_code == 200
201199

202-
# is_user = response.json()
203-
# assert is_user == 0
200+
is_user = response.json()
201+
assert is_user == 0
204202

205203

206204
def test_usergetusers(state: State):

src/server/user_mgmt/base_users.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,28 +58,28 @@ def create_base_users(): # TODO: Just call create_user for each
5858
# user
5959
pw_hash = user_api.hash_password(BASEUSER_PW)
6060
ins_stmt = pu.insert().values(
61-
username="base_user", password=pw_hash, active="Y", role=1,
61+
username="base_user", full_name="Base User", password=pw_hash, active="Y", role=1,
6262
)
6363
connection.execute(ins_stmt)
6464

6565
# INactive user
6666
# Reuse pw hash
6767
ins_stmt = pu.insert().values(
68-
username="base_user_inact", password=pw_hash, active="N", role=1,
68+
username="base_user_inact", full_name="Inactive User", password=pw_hash, active="N", role=1,
6969
)
7070
connection.execute(ins_stmt)
7171

7272
# editor
7373
pw_hash = user_api.hash_password(BASEEDITOR_PW)
7474
ins_stmt = pu.insert().values(
75-
username="base_editor", password=pw_hash, active="Y", role=2,
75+
username="base_editor", full_name="Base Editor", password=pw_hash, active="Y", role=2,
7676
)
7777
connection.execute(ins_stmt)
7878

7979
# admin
8080
pw_hash = user_api.hash_password(BASEADMIN_PW)
8181
ins_stmt = pu.insert().values(
82-
username="base_admin", password=pw_hash, active="Y", role=9,
82+
username="base_admin", full_name="Base Admin", password=pw_hash, active="Y", role=9,
8383
)
8484
connection.execute(ins_stmt)
8585

0 commit comments

Comments
 (0)