Skip to content

Implement active directory auth #192

Merged
merged 14 commits into from
Feb 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ yarn-error.log*
/api/venv
__pycache__/
venv-manager.log
/api/.env
/api/.env
*.egg*
62 changes: 57 additions & 5 deletions api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
jwt_required, get_jwt_identity, jwt_refresh_token_required,
set_refresh_cookies, unset_refresh_cookies
)
from werkzeug.security import check_password_hash
import os, dotenv
from easyad import EasyAD
from ldap.filter import escape_filter_chars
# pylint says this is an error but it works so ¯\_(ツ)_/¯
from ldap import INVALID_CREDENTIALS as LDAP_INVALID_CREDENTIALS
import ECNQueue

# Load envrionment variables for ./.env
Expand Down Expand Up @@ -47,6 +50,57 @@



def user_is_valid(username: str, password: str) -> bool:
"""Checks if user is valid and in webqueue2 login group.
Args:
username (str): Career account username.
password (str): Career account passphrase.
Returns:
bool: True if user is valid, otherwise False.
"""

# Check for empty arguments
if (username == "" or password == ""):
return False

# Initialize EasyAD
config = {
"AD_SERVER": "boilerad.purdue.edu",
"AD_DOMAIN": "boilerad.purdue.edu"
}
ad = EasyAD(config)

# Prepare search critiera for Active Directory
credentials = {
"username": escape_filter_chars(username),
"password": password
}
attributes = [ 'cn', "memberOf" ]
filter_string = f'(&(objectClass=user)(|(sAMAccountName={username})))'

# Do user search
try:
user = ad.search(credentials=credentials, attributes=attributes, filter_string=filter_string)[0]
except LDAP_INVALID_CREDENTIALS:
return False

# Isolate group names
# Example:
# 'CN=00000227-ECNStuds,OU=BoilerADGroups,DC=BoilerAD,DC=Purdue,DC=edu' becomes
# `00000227-ECNStuds`
user_groups = [ group.split(',')[0].split('=')[1] for group in user["memberOf"] ]

# Check group membership
webqueue_login_group = "00000227-ECN-webqueue"
if webqueue_login_group not in user_groups:
return False

return True



class Login(Resource):
def post(self) -> tuple:
"""Validates username/password and returns both access and refresh tokens.
Expand Down Expand Up @@ -76,10 +130,8 @@ def post(self) -> tuple:
if field not in data.keys():
return ({ "message": f"{field} missing from request body"}, 422)

if data["username"] != os.environ.get("SHARED_USERNAME"):
return ({ "message": "Username invalid"}, 401)
if not check_password_hash(os.environ.get("SHARED_PASSWORD_HASH"), data["password"]):
return ({ "message": "Password invalid"}, 401)
if not user_is_valid(data["username"], data["password"]):
return ({ "message": "Username or password is invalid"}, 401)

access_token = create_access_token(data["username"])
refresh_token = create_refresh_token(data["username"])
Expand Down
47 changes: 25 additions & 22 deletions api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
aniso8601==8.0.0
astroid==2.4.2
click==7.1.2
Flask==1.1.2
Flask-RESTful==0.3.8
Flask-JWT-Extended==3.24.1
gunicorn==20.0.4
isort==4.3.21
itsdangerous==1.1.0
Jinja2==2.11.2
lazy-object-proxy==1.4.3
MarkupSafe==1.1.1
mccabe==0.6.1
pylint==2.5.3
python-dateutil==2.8.1
python-dotenv==0.15.0
pytz==2020.1
six==1.15.0
toml==0.10.1
typed-ast==1.4.1
Werkzeug==1.0.1
wrapt==1.12.1
# The Python virtual environment should be managed via the venv-manager utility, not directly by pip.
# See: https://pip.pypa.io/en/stable/reference/pip_install/#example-requirements-file

# General Utilities
gunicorn
pipdeptree
pylint

# API
python-dotenv
Flask-RESTful
python-dateutil
Flask-JWT-Extended
# Flask-JWT-Extended doesn't support PyJWT 2.x as of 3.25.0
# Prevent upgrade to PyJWT 2.x until Flask-JWT-Extended is updated to support it.
# Check: https://github.com/vimalloc/flask-jwt-extended/tags
PyJWT == 1.*
# Specify pyldap version for custom build. This is not installed by pip.
pyldap == 3.3.1
easyad

# API Documentation
mkdocs
mkdocs-material
mkautodoc
5 changes: 3 additions & 2 deletions src/components/ItemTable/ItemTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ export default function ItemTable({ data, rowCanBeSelected }) {
{ Header: 'Subject', accessor: 'subject' },
{ Header: 'Status', accessor: 'status', },
{ Header: 'Priority', accessor: 'priority' },
{ Header: 'Last Updated', accessor: 'lastUpdated', Cell: ({ value }) => <RelativeTime value={value} /> },
{ Header: 'Last Updated', accessor: 'lastUpdated', sortInverted: true, Cell: ({ value }) => <RelativeTime value={value} /> },
{ Header: 'Department', accessor: 'department' },
{ Header: 'Building', accessor: 'building' },
{ Header: 'Date Received', accessor: 'dateReceived', Cell: ({ value }) => <RelativeTime value={value} /> },
{ Header: 'Date Received', accessor: 'dateReceived', sortInverted: true, Cell: ({ value }) => <RelativeTime value={value} /> },

], []);

const tableInstance = useTable(
Expand Down
Loading