-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'refactor-to-module-layout' into staging
- Loading branch information
Showing
19 changed files
with
1,767 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from webqueue2api.parser import Item, Queue, load_queues | ||
from .config import config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# WSGI App | ||
from .app import app | ||
|
||
# Configuration | ||
from .config import config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from flask import Flask | ||
from flask_restful import Api | ||
from flask_jwt_extended import JWTManager | ||
from .config import config | ||
from .resources import Login, RefreshAccessToken, Item, Queue, QueueList | ||
|
||
app = Flask(__name__) | ||
api = Api(app) | ||
|
||
# Set JWT secret key and create JWT manager | ||
app.config["JWT_SECRET_KEY"] = config.jwt_secret_key | ||
# The JWT RFC uses the "sub" key for identity claims. However, | ||
# Flask-JWT-Extended uses "identity" by default for compatibility reasons so | ||
# we ovverride the default claim key to comply with the RFC | ||
app.config["JWT_IDENTITY_CLAIM"] = "sub" | ||
# Set the key for error messages generated by Flask-JWT-Extended | ||
app.config["JWT_ERROR_MESSAGE_KEY"] = "message" | ||
|
||
# Look for JWTs in headers (for access) then cookies (for refresh) | ||
app.config["JWT_TOKEN_LOCATION"] = ["headers", "cookies"] | ||
# Restrict cookies to HTTPS in prod, allow HTTP in dev | ||
app.config["JWT_COOKIE_SECURE"] = False if config.jwt_secret_key == "dev" else True | ||
# Restrict cookies using SameSite=strict flag | ||
app.config["JWT_COOKIE_SAMESITE"] = "strict" | ||
# Restrict refresh tokens to /token/refresh endpoint | ||
app.config["JWT_REFRESH_COOKIE_PATH"] = '/tokens/refresh' | ||
# Set the cookie key for CRSF validation string | ||
# This is the default value. Adding it for easy reference | ||
app.config["JWT_REFRESH_CSRF_HEADER_NAME"] = "X-CSRF-TOKEN" | ||
|
||
tokenManager = JWTManager(app) | ||
|
||
api.add_resource(Login, "/api/login") | ||
api.add_resource(RefreshAccessToken, "/api/tokens/refresh") | ||
api.add_resource(Item, "/api/data/<string:queue>/<int:number>") | ||
api.add_resource(Queue, "/api/data/<string:queues>") | ||
api.add_resource(QueueList, "/api/data/get_queues") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
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 | ||
|
||
|
||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
"""Stores API configuartion data.""" | ||
from dataclasses import dataclass | ||
import random, string | ||
|
||
|
||
|
||
def generate_random_string(length=16) -> str: | ||
"""Generate random string of letters and numbers of specified length. | ||
Example: | ||
generate_random_string() -> "aud04ki947rrje3k9" | ||
Args: | ||
length (int, optional): Number of characters to generate. Defaults to 16. | ||
Returns: | ||
str: Random string. | ||
""" | ||
possible_characters = string.ascii_letters + string.digits + "!@#$%^&*" | ||
random_string = '' | ||
for number in range(length): | ||
random_string += random.choice(possible_characters) | ||
return random_string | ||
|
||
@dataclass | ||
class Configuraton: | ||
"""Stores API configuration. | ||
Args: | ||
jwt_secret_key (str): The key used to confirm JWT validity. | ||
environment (str): The type of environment to run in. "prod" or "dev" | ||
""" | ||
jwt_secret_key: str | ||
environment: str | ||
|
||
config = Configuraton( | ||
jwt_secret_key = generate_random_string(), | ||
environment = "prod" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from .login import Login | ||
from .refresh_access_token import RefreshAccessToken | ||
from .item import Item | ||
from .queue import Queue | ||
from .queue_list import QueueList |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
from flask import request | ||
from flask_restful import Resource | ||
from flask_jwt_extended import jwt_required | ||
# To avoid naming conflicts | ||
from webqueue2api.parser import Item as _Item | ||
from webqueue2api.parser.errors import ItemDoesNotExistError | ||
|
||
class Item(Resource): | ||
@jwt_required | ||
def get(self, queue: str, number: int) -> tuple: | ||
"""Returns the JSON representation of the item requested. | ||
Return Codes: | ||
200 (OK): On success. | ||
404 (Not Found): When an Item does not exist. | ||
Example: | ||
/api/ce/100 returns: | ||
{ | ||
"lastUpdated": "07-23-20 10:11 PM", | ||
"headers": [...], | ||
"content": [...], | ||
"isLocked": "ce 100 is locked by knewell using qvi", | ||
"userEmail": "campb303@purdue.edu", | ||
"userName": "Justin Campbell", | ||
"userAlias": "campb303", | ||
"assignedTo": "campb303", | ||
"subject": "Beepboop", | ||
"status": "Dont Delete", | ||
"priority": "", | ||
"deparment": "", | ||
"building": "", | ||
"dateReceived": "Tue, 23 Jun 2020 13:25:51 -0400" | ||
} | ||
Args: | ||
queue (str): The queue of the item requested. | ||
item (int): The number of the item requested. | ||
Returns: | ||
tuple: Item as JSON and HTTP response code. | ||
""" | ||
headers_only = True if request.args.get("headers_only") == "True" else False | ||
|
||
try: | ||
return (_Item(queue, number, headers_only=headers_only).to_json(), 200) | ||
except ItemDoesNotExistError: | ||
return ({"message": f"Item {queue}{number} not found."}, 404) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
from flask import request, after_this_request | ||
from flask_restful import Resource | ||
from flask_jwt_extended import create_access_token, create_refresh_token, set_refresh_cookies | ||
from ..auth import user_is_valid | ||
|
||
|
||
|
||
class Login(Resource): | ||
def post(self) -> tuple: | ||
"""Validates username/password, sets refresh token cookie and returns access token. | ||
Return Codes: | ||
200 (OK): On success. | ||
401 (Unauthroized): When username or password are incorrect. | ||
422 (Unprocessable Entitiy): When the username or password can't be parsed. | ||
Example: | ||
curl -X POST | ||
-H "Content-Type: application/json" | ||
-d '{"username": "bob", "password": "super_secret"}' | ||
{ "access_token": fjr09hfp09h932jp9ruj3.3r8ihf8h0w8hr08ifhj804h8i.8h48ith08ity409hip0t4 } | ||
Returns: | ||
tuple: Response containing tokens and HTTP response code. | ||
""" | ||
if not request.is_json: | ||
return ({ "message": "JSON missing from request body"}, 422) | ||
|
||
data = request.json | ||
|
||
fields_to_check = ["username", "password"] | ||
for field in fields_to_check: | ||
if field not in data.keys(): | ||
return ({ "message": f"{field} missing from request body"}, 422) | ||
|
||
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"]) | ||
|
||
# This decorator is needed because Flask-RESTful's 'resourceful routing` | ||
# doesn't allow for direct modification to the Flask response object. | ||
# See: https://flask-restful.readthedocs.io/en/latest/quickstart.html#resourceful-routing | ||
@after_this_request | ||
def set_refresh_cookie_callback(response): | ||
set_refresh_cookies(response, refresh_token) | ||
return response | ||
|
||
return ({ "access_token": access_token }, 200) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from flask import request | ||
from flask_restful import Resource | ||
from flask_jwt_extended import jwt_required | ||
# To avoid naming conflicts | ||
from webqueue2api.parser import Queue as _Queue | ||
from webqueue2api.parser.errors import QueueDoesNotExistError | ||
|
||
|
||
|
||
class Queue(Resource): | ||
@jwt_required | ||
def get(self, queues: str) -> tuple: | ||
"""Returns the JSON representation of the queue requested. | ||
Return Codes: | ||
200 (OK): On success. | ||
404 (Not Found): When a Queue does not exist. | ||
Args: | ||
queues (str): Plus (+) deliminited list of queues. | ||
Returns: | ||
tuple: Queues as JSON and HTTP response code. | ||
""" | ||
queues_requested = queues.split("+") | ||
headers_only = False if request.args.get("headers_only") == "False" else True | ||
|
||
try: | ||
queue_list = [] | ||
for queue in queues_requested: | ||
queue_list.append(_Queue(queue, headers_only=headers_only).to_json()) | ||
return (queue_list, 200) | ||
except QueueDoesNotExistError: | ||
return ({"message": f"Queue {queue} not found."}, 404) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from flask_restful import Resource | ||
from flask_jwt_extended import jwt_required | ||
from webqueue2api.parser import get_queue_counts | ||
|
||
|
||
class QueueList(Resource): | ||
@jwt_required | ||
def get(self) -> tuple: | ||
"""Returns a list of dictionaries with the number of items in each queue. | ||
Return Codes: | ||
200 (OK): On success. | ||
Example: | ||
[ | ||
{ | ||
name: "me", | ||
number_of_items: 42 | ||
}, | ||
{ | ||
name: "bidc", | ||
number_of_items: 3 | ||
} | ||
] | ||
Returns: | ||
tuple: Queues and item counts as JSON and HTTP response code. | ||
""" | ||
return (get_queue_counts(), 200) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from flask_restful import Resource | ||
from flask_jwt_extended import jwt_refresh_token_required, get_jwt_identity, create_access_token | ||
|
||
class RefreshAccessToken(Resource): | ||
@jwt_refresh_token_required | ||
def post(self): | ||
username = get_jwt_identity() | ||
access_token = create_access_token(username) | ||
return ({"access_token": access_token}, 200) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from dataclasses import dataclass | ||
import webqueue2api.parser.config | ||
import webqueue2api.api.config | ||
|
||
@dataclass | ||
class Configuration: | ||
parser: dataclass | ||
api: dataclass | ||
|
||
config = Configuration( | ||
parser = webqueue2api.parser.config, | ||
api = webqueue2api.api.config | ||
) |
Oops, something went wrong.