2020-05-29 08:42:48 +00:00
|
|
|
import flask
|
2020-06-02 01:45:09 +00:00
|
|
|
from flask import g, request
|
2020-02-18 21:38:56 +00:00
|
|
|
|
2020-05-22 11:55:58 +00:00
|
|
|
from crc import app, db
|
2020-02-18 21:38:56 +00:00
|
|
|
from crc.api.common import ApiError
|
|
|
|
from crc.models.user import UserModel, UserModelSchema
|
2020-06-11 15:29:58 +00:00
|
|
|
from crc.services.ldap_service import LdapService, LdapModel
|
2020-03-24 18:15:21 +00:00
|
|
|
|
2020-02-20 20:35:07 +00:00
|
|
|
"""
|
|
|
|
.. module:: crc.api.user
|
|
|
|
:synopsis: Single Sign On (SSO) user login and session handlers
|
|
|
|
"""
|
2020-05-31 20:49:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
def verify_token(token=None):
|
|
|
|
"""
|
|
|
|
Verifies the token for the user (if provided). If in production environment and token is not provided,
|
|
|
|
gets user from the SSO headers and returns their token.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
token: Optional[str]
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
token: str
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
ApiError. If not on production and token is not valid, returns an 'invalid_token' 403 error.
|
|
|
|
If on production and user is not authenticated, returns a 'no_user' 403 error.
|
|
|
|
"""
|
|
|
|
|
2020-06-11 15:29:58 +00:00
|
|
|
failure_error = ApiError("invalid_token", "Unable to decode the token you provided. Please re-authenticate",
|
|
|
|
status_code=403)
|
2020-05-31 20:49:39 +00:00
|
|
|
|
|
|
|
if token:
|
|
|
|
try:
|
|
|
|
token_info = UserModel.decode_auth_token(token)
|
|
|
|
g.user = UserModel.query.filter_by(uid=token_info['sub']).first()
|
|
|
|
except:
|
|
|
|
raise failure_error
|
|
|
|
if g.user is not None:
|
|
|
|
return token_info
|
|
|
|
else:
|
|
|
|
raise failure_error
|
|
|
|
|
|
|
|
# If there's no token and we're in production, get the user from the SSO headers and return their token
|
2020-07-27 15:25:29 +00:00
|
|
|
elif _is_production():
|
2020-05-31 22:01:08 +00:00
|
|
|
uid = _get_request_uid(request)
|
2020-05-31 20:49:39 +00:00
|
|
|
|
|
|
|
if uid is not None:
|
|
|
|
db_user = UserModel.query.filter_by(uid=uid).first()
|
|
|
|
|
|
|
|
if db_user is not None:
|
|
|
|
g.user = db_user
|
|
|
|
token = g.user.encode_auth_token().decode()
|
|
|
|
token_info = UserModel.decode_auth_token(token)
|
|
|
|
return token_info
|
|
|
|
|
|
|
|
else:
|
2020-06-11 17:07:27 +00:00
|
|
|
raise ApiError("no_user", "User not found. Please login via the frontend app before accessing this feature.",
|
2020-06-11 15:29:58 +00:00
|
|
|
status_code=403)
|
2020-05-31 20:49:39 +00:00
|
|
|
|
2020-07-27 15:25:29 +00:00
|
|
|
else:
|
|
|
|
# Fall back to a default user if this is not production.
|
|
|
|
g.user = UserModel.query.first()
|
|
|
|
token = g.user.encode_auth_token()
|
|
|
|
|
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
|
|
|
|
def verify_token_admin(token=None):
|
|
|
|
"""
|
|
|
|
Verifies the token for the user (if provided) in non-production environment. If in production environment,
|
|
|
|
checks that the user is in the list of authorized admins
|
|
|
|
|
|
|
|
Args:
|
|
|
|
token: Optional[str]
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
token: str
|
|
|
|
"""
|
|
|
|
|
|
|
|
# If this is production, check that the user is in the list of admins
|
|
|
|
if _is_production():
|
2020-05-31 22:01:08 +00:00
|
|
|
uid = _get_request_uid(request)
|
2020-05-31 20:49:39 +00:00
|
|
|
|
|
|
|
if uid is not None and uid in app.config['ADMIN_UIDS']:
|
|
|
|
return verify_token()
|
|
|
|
|
|
|
|
# If we're not in production, just use the normal verify_token method
|
2020-02-18 21:38:56 +00:00
|
|
|
else:
|
2020-05-31 20:49:39 +00:00
|
|
|
return verify_token(token)
|
2020-02-18 21:38:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_current_user():
|
|
|
|
return UserModelSchema().dump(g.user)
|
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
|
|
|
|
def login(
|
2020-06-11 15:29:58 +00:00
|
|
|
uid=None,
|
|
|
|
redirect_url=None,
|
2020-05-31 20:49:39 +00:00
|
|
|
):
|
|
|
|
"""
|
|
|
|
In non-production environment, provides an endpoint for end-to-end system testing that allows the system
|
|
|
|
to simulate logging in as a specific user. In production environment, simply logs user in via single-sign-on
|
|
|
|
(SSO) Shibboleth authentication headers.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
uid: Optional[str]
|
|
|
|
redirect_url: Optional[str]
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
str. If not on production, returns the frontend auth callback URL, with auth token appended.
|
|
|
|
If on production and user is authenticated via SSO, returns the frontend auth callback URL,
|
|
|
|
with auth token appended.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
ApiError. If on production and user is not authenticated, returns a 404 error.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# ----------------------------------------
|
|
|
|
# Shibboleth Authentication Headers
|
|
|
|
# ----------------------------------------
|
2020-05-22 11:55:58 +00:00
|
|
|
# X-Remote-Cn: Daniel Harold Funk (dhf8r)
|
|
|
|
# X-Remote-Sn: Funk
|
|
|
|
# X-Remote-Givenname: Daniel
|
|
|
|
# X-Remote-Uid: dhf8r
|
|
|
|
# Eppn: dhf8r@virginia.edu
|
|
|
|
# Cn: Daniel Harold Funk (dhf8r)
|
|
|
|
# Sn: Funk
|
|
|
|
# Givenname: Daniel
|
|
|
|
# Uid: dhf8r
|
|
|
|
# X-Remote-User: dhf8r@virginia.edu
|
|
|
|
# X-Forwarded-For: 128.143.0.10
|
|
|
|
# X-Forwarded-Host: dev.crconnect.uvadcos.io
|
|
|
|
# X-Forwarded-Server: dev.crconnect.uvadcos.io
|
|
|
|
# Connection: Keep-Alive
|
|
|
|
|
2020-06-12 17:46:10 +00:00
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
# If we're in production, override any uid with the uid from the SSO request headers
|
|
|
|
if _is_production():
|
2020-05-31 22:01:08 +00:00
|
|
|
uid = _get_request_uid(request)
|
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
if uid:
|
|
|
|
app.logger.info("SSO_LOGIN: Full URL: " + request.url)
|
|
|
|
app.logger.info("SSO_LOGIN: User Id: " + uid)
|
|
|
|
app.logger.info("SSO_LOGIN: Will try to redirect to : " + str(redirect_url))
|
|
|
|
|
|
|
|
ldap_info = LdapService().user_info(uid)
|
2020-02-18 21:38:56 +00:00
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
if ldap_info:
|
|
|
|
return _handle_login(ldap_info, redirect_url)
|
2020-05-22 11:55:58 +00:00
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
raise ApiError('404', 'unknown')
|
2020-05-22 11:55:58 +00:00
|
|
|
|
2020-02-18 21:38:56 +00:00
|
|
|
|
2020-05-24 04:05:13 +00:00
|
|
|
@app.route('/sso')
|
2020-05-22 11:55:58 +00:00
|
|
|
def sso():
|
|
|
|
response = ""
|
|
|
|
response += "<h1>Headers</h1>"
|
2020-05-22 13:50:18 +00:00
|
|
|
response += "<ul>"
|
2020-05-31 20:49:39 +00:00
|
|
|
for k, v in request.headers:
|
2020-05-22 13:50:18 +00:00
|
|
|
response += "<li><b>%s</b> %s</li>\n" % (k, v)
|
2020-05-22 11:55:58 +00:00
|
|
|
response += "<h1>Environment</h1>"
|
2020-05-31 20:49:39 +00:00
|
|
|
for k, v in request.environ:
|
2020-05-22 13:50:18 +00:00
|
|
|
response += "<li><b>%s</b> %s</li>\n" % (k, v)
|
2020-05-22 11:55:58 +00:00
|
|
|
return response
|
2020-05-21 20:02:45 +00:00
|
|
|
|
2020-02-18 21:38:56 +00:00
|
|
|
|
2020-06-11 15:29:58 +00:00
|
|
|
def _handle_login(user_info: LdapModel, redirect_url=None):
|
2020-05-31 20:49:39 +00:00
|
|
|
"""
|
|
|
|
On successful login, adds user to database if the user is not already in the system,
|
|
|
|
then returns the frontend auth callback URL, with auth token appended.
|
2020-02-20 20:35:07 +00:00
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
Args:
|
|
|
|
user_info - an ldap user_info object.
|
|
|
|
redirect_url: Optional[str]
|
2020-02-20 20:35:07 +00:00
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
Returns:
|
|
|
|
Response. 302 - Redirects to the frontend auth callback URL, with auth token appended.
|
2020-02-20 20:35:07 +00:00
|
|
|
"""
|
2020-06-11 15:29:58 +00:00
|
|
|
user = _upsert_user(user_info)
|
2020-06-12 17:46:10 +00:00
|
|
|
g.user = user
|
2020-06-11 15:29:58 +00:00
|
|
|
|
|
|
|
# Return the frontend auth callback URL, with auth token appended.
|
|
|
|
auth_token = user.encode_auth_token().decode()
|
|
|
|
if redirect_url is not None:
|
|
|
|
if redirect_url.find("http://") != 0 and redirect_url.find("https://") != 0:
|
|
|
|
redirect_url = "http://" + redirect_url
|
|
|
|
url = '%s?token=%s' % (redirect_url, auth_token)
|
|
|
|
app.logger.info("SSO_LOGIN: REDIRECTING TO: " + url)
|
|
|
|
return flask.redirect(url, code=302)
|
|
|
|
else:
|
|
|
|
app.logger.info("SSO_LOGIN: NO REDIRECT, JUST RETURNING AUTH TOKEN.")
|
|
|
|
return auth_token
|
|
|
|
|
|
|
|
|
|
|
|
def _upsert_user(user_info):
|
2020-05-22 13:50:18 +00:00
|
|
|
user = db.session.query(UserModel).filter(UserModel.uid == user_info.uid).first()
|
2020-02-20 20:35:07 +00:00
|
|
|
|
2020-02-21 16:24:39 +00:00
|
|
|
if user is None:
|
|
|
|
# Add new user
|
2020-05-22 13:50:18 +00:00
|
|
|
user = UserModel()
|
2020-06-11 15:29:58 +00:00
|
|
|
else:
|
|
|
|
user = db.session.query(UserModel).filter(UserModel.uid == user_info.uid).with_for_update().first()
|
2020-02-20 20:35:07 +00:00
|
|
|
|
2020-05-22 13:50:18 +00:00
|
|
|
user.uid = user_info.uid
|
|
|
|
user.display_name = user_info.display_name
|
|
|
|
user.email_address = user_info.email_address
|
|
|
|
user.affiliation = user_info.affiliation
|
|
|
|
user.title = user_info.title
|
2020-02-20 20:35:07 +00:00
|
|
|
|
|
|
|
db.session.add(user)
|
|
|
|
db.session.commit()
|
2020-06-11 15:29:58 +00:00
|
|
|
return user
|
2020-02-18 21:38:56 +00:00
|
|
|
|
2020-05-22 13:50:18 +00:00
|
|
|
|
2020-05-31 22:01:08 +00:00
|
|
|
def _get_request_uid(req):
|
|
|
|
uid = None
|
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
if _is_production():
|
2020-05-31 22:01:08 +00:00
|
|
|
|
2020-06-11 15:29:58 +00:00
|
|
|
if 'user' in g and g.user is not None:
|
|
|
|
return g.user.uid
|
|
|
|
|
2020-05-31 22:01:08 +00:00
|
|
|
uid = req.headers.get("Uid")
|
2020-05-31 20:49:39 +00:00
|
|
|
if not uid:
|
2020-05-31 22:01:08 +00:00
|
|
|
uid = req.headers.get("X-Remote-Uid")
|
2020-05-22 13:50:18 +00:00
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
if not uid:
|
|
|
|
raise ApiError("invalid_sso_credentials", "'Uid' nor 'X-Remote-Uid' were present in the headers: %s"
|
2020-05-31 22:01:08 +00:00
|
|
|
% str(req.headers))
|
2020-05-25 16:29:05 +00:00
|
|
|
|
2020-05-31 20:49:39 +00:00
|
|
|
return uid
|
|
|
|
|
|
|
|
|
|
|
|
def _is_production():
|
|
|
|
return 'PRODUCTION' in app.config and app.config['PRODUCTION']
|