Skip to content

Commit c823963

Browse files
committed
Temp fix of phone number exception issue
2 parents 3c69ec4 + ab85970 commit c823963

File tree

9 files changed

+204
-86
lines changed

9 files changed

+204
-86
lines changed

src/server/Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ RUN set FLASK_APP=server/app.py
4141
RUN export FLASK_APP
4242

4343
# This abomination ensures that the PG server has finished its restart cycle
44-
CMD echo "SLEEPING 10"; sleep 10; echo "WAKING"; alembic upgrade head ; python -m flask run --host=0.0.0.0
44+
CMD echo "SLEEPING 10"; sleep 10; echo "WAKING"; alembic upgrade head ; python -m flask run --host=0.0.0.0 --no-reload
45+
46+
# --no-reload prevents Flask restart, which usually happens in middle of create_base_users()
47+
#TODO: SECURITY - ensure we are not running in debug mode in production

src/server/api/jwt_ops.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
create_access_token,
77
get_jwt_identity,
88
verify_jwt_in_request,
9-
get_jwt_claims,
9+
get_jwt
10+
1011
)
1112

1213
from app import app, jwt
@@ -16,28 +17,20 @@ def admin_required(fn):
1617
@wraps(fn)
1718
def wrapper(*args, **kwargs):
1819
verify_jwt_in_request()
19-
claims = get_jwt_claims()
20+
claims = get_jwt()
2021
if claims["role"] != "admin": # TODO could be multiple
2122
return jsonify(msg="Admins only!"), 403
2223
else:
2324
return fn(*args, **kwargs)
2425

2526
return wrapper
2627

27-
28-
@jwt.user_claims_loader
29-
def add_claims_to_access_token(accesslevel):
30-
""" Adds role k/v to token """
31-
return {"role": accesslevel}
32-
33-
3428
def create_token(username, accesslevel):
35-
""" Create a JWT *access* token for the specified user and role.
36-
Role is magically added by the user_claims_loader decorator
29+
""" Create a JWT *access* token for the specified user ('sub:') and role ('role:').
3730
"""
38-
# Identity can be any data that is json serializable
39-
new_token = create_access_token(identity=username)
40-
# add_claims_to_access_token(accesslevel)
31+
# Identity can be any data that is json serializable, we just use username
32+
addl_claims = {'role': accesslevel}
33+
new_token = create_access_token(identity=username, additional_claims=addl_claims)
4134
return jsonify(access_token=new_token)
4235

4336

src/server/api/user_api.py

Lines changed: 21 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -67,50 +67,30 @@ def user_test_fail():
6767
return jsonify("Here's your failure"), 401
6868

6969

70-
@user_api.route("/api/user/login", methods=["POST"])
71-
def user_login():
72-
""" Validate user in db, return JWT if legit and active.
73-
Expects non-json form data
74-
"""
75-
76-
with engine.connect() as connection:
77-
78-
pwhash = None
79-
s = text(
80-
"""select password, pdp_user_roles.role, active
81-
from pdp_users
82-
left join pdp_user_roles on pdp_users.role = pdp_user_roles._id
83-
where username=:u """
84-
)
85-
s = s.bindparams(u=request.form["username"])
86-
result = connection.execute(s)
87-
88-
if result.rowcount: # Did we get a match on username?
89-
pwhash, role, is_active = result.fetchone()
90-
else:
91-
log_user_action(request.form["username"], "Failure", "Invalid username")
92-
return jsonify("Bad credentials"), 401
93-
94-
if is_active.lower() == "y" and check_password(request.form["password"], pwhash):
95-
# Yes, user is active and password matches
96-
token = jwt_ops.create_token(request.form["username"], role)
97-
log_user_action(request.form["username"], "Success", "Logged in")
98-
return token
99-
100-
else:
101-
log_user_action(request.form["username"], "Failure", "Bad password or inactive")
102-
return jsonify("Bad credentials"), 401
103-
10470

10571
@user_api.route("/api/user/login_json", methods=["POST"])
10672
def user_login_json():
10773
""" Validate user in db, return JWT if legit and active.
10874
Expects json-encoded form data
10975
"""
11076

111-
post_dict = json.loads(request.data)
112-
username = post_dict["username"]
113-
presentedpw = post_dict["password"]
77+
def dummy_check():
78+
"""Perform a fake password hash check to take as much time as a real one."""
79+
pw_bytes = bytes('password', "utf8")
80+
check_password('password', pw_bytes)
81+
82+
try:
83+
post_dict = json.loads(request.data)
84+
username = post_dict["username"]
85+
presentedpw = post_dict["password"]
86+
except:
87+
dummy_check() # Take the same time as with well-formed requests
88+
return jsonify("Bad credentials"), 401
89+
90+
if not (isinstance(username, str) and isinstance(presentedpw, str) ):
91+
dummy_check() # Take the same time as with well-formed requests
92+
return jsonify("Bad credentials"), 401 # Don't give us ints, arrays, etc.
93+
11494

11595
with engine.connect() as connection:
11696

@@ -128,6 +108,7 @@ def user_login_json():
128108
pwhash, role, is_active = result.fetchone()
129109
else:
130110
log_user_action(username, "Failure", "Invalid username")
111+
dummy_check()
131112
return jsonify("Bad credentials"), 401
132113

133114
if is_active.lower() == "y" and check_password(presentedpw, pwhash):
@@ -138,22 +119,23 @@ def user_login_json():
138119

139120
else:
140121
log_user_action(username, "Failure", "Bad password or inactive")
122+
# No dummy_check needed as we ran a real one to get here
141123
return jsonify("Bad credentials"), 401
142124

143125

144126
### Unexpired JWT required ############################
145127

146128

147129
@user_api.route("/api/user/test_auth", methods=["GET"])
148-
@jwt_ops.jwt_required
130+
@jwt_ops.jwt_required()
149131
def user_test_auth():
150132
""" Liveness test, requires JWT """
151133
return jsonify(("OK from User Test - Auth @" + str(datetime.now())))
152134

153135

154136
# Logout is not strictly needed; client can just delete JWT, but good for logging
155137
@user_api.route("/api/user/logout", methods=["POST"])
156-
@jwt_ops.jwt_required
138+
@jwt_ops.jwt_required()
157139
def user_logout():
158140
username = request.form["username"] # TODO: Should be JSON all throughout
159141
# Log the request

src/server/app.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44

55
from flask_jwt_extended import JWTManager
66

7+
from secrets import JWT_SECRET, APP_SECRET_KEY
78

89
app = Flask(__name__)
910

10-
app.config["JWT_SECRET_KEY"] = "super-secret" # TODO: SECURITY Change this!
11+
app.config["JWT_SECRET_KEY"] = JWT_SECRET
1112
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = 300 # Seconds for timeout. 60 for testing.
1213
jwt = JWTManager(app)
1314

1415

1516
# def create_app():
16-
app.secret_key = "1u9L#*&I3Ntc" # TODO: SECURITY Change this!
17+
app.secret_key = APP_SECRET_KEY
1718
app.config["MAX_CONTENT_LENGTH"] = 500 * 1024 * 1024 # 500 Megs
1819
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
1920
from api.admin_api import admin_api

src/server/datasource_manager.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,20 @@ def __clean_csv_headers(header):
7171
}
7272

7373

74-
def volgistics_address(index, street):
74+
def volgistics_address(street, index):
7575
result = ""
7676

77-
for item in street:
78-
if isinstance(item, str):
79-
if " " in item:
80-
result = item.split()[index]
77+
if isinstance(street, str):
78+
if " " in street:
79+
if index == 1:
80+
result = " ".join(street.split()[1:])
81+
else:
82+
result = street.split()[index]
83+
8184

8285
return result
8386

87+
8488
def normalize_phone_number(number):
8589
if str(number) == 'nan':
8690
return ""
@@ -90,13 +94,14 @@ def normalize_phone_number(number):
9094
return ""
9195
return phonenumbers.format_number(parsed_number, phonenumbers.PhoneNumberFormat.NATIONAL)
9296

97+
9398
SOURCE_NORMALIZATION_MAPPING = {
9499
"salesforcecontacts": {
95100
"source_id": "contact_id",
96101
"first_name": "first_name",
97102
"last_name": "last_name",
98103
"email": "email",
99-
"mobile": lambda df: df["mobile"].combine_first(df["phone"]).apply(normalize_phone_number),
104+
"mobile": lambda df: df["mobile"].combine_first(df["phone"]),
100105
"street_and_number": "mailing_street",
101106
"apartment": "mailing_street",
102107
"city": "mailing_city",
@@ -118,7 +123,7 @@ def normalize_phone_number(number):
118123
"first_name": "firstname",
119124
"last_name": "lastname",
120125
"email": "email",
121-
"mobile": lambda df: df["phone"].apply(normalize_phone_number),
126+
"mobile": lambda df: df["phone"],
122127
"street_and_number": "street",
123128
"apartment": "apartment",
124129
"city": "city",
@@ -133,9 +138,9 @@ def normalize_phone_number(number):
133138
"first_name": "first_name",
134139
"last_name": "last_name",
135140
"email": "email",
136-
"mobile": lambda df: df["cell"].combine_first(df["home"]).apply(normalize_phone_number),
137-
"street_and_number": lambda df: volgistics_address(1, df["street_1"]),
138-
"apartment": lambda df: volgistics_address(0, df["street_1"]),
141+
"mobile": lambda df: df["cell"].combine_first(df["home"]),
142+
"street_and_number": lambda df: df["street_1"].apply(volgistics_address, index=1),
143+
"apartment": lambda df: df["street_1"].apply(volgistics_address, index=0),
139144
"city": "city",
140145
"state": "state",
141146
"zip": "zip",

src/server/pipeline/normalize_matching_data.py

Whitespace-only changes.

src/server/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ xlrd==1.2.0 # currently used for xlsx, but we should consider adjusting code to
1010
openpyxl
1111
requests
1212
pytest
13-
flask-jwt-extended
13+
flask-jwt-extended==4.0.2
1414
alembic
1515
flask-cors
16-
phonenumbers
16+
phonenumbers

0 commit comments

Comments
 (0)