Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REST API cookie authentication and refactor #250

Merged
merged 98 commits into from
Aug 24, 2022
Merged
Show file tree
Hide file tree
Changes from 89 commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
930c251
chore: cleanup
haasal May 30, 2022
9201f2c
dependecy: fastapi-jwt-auth
haasal May 30, 2022
b87f5ee
chore: replace static key from config with automatically generated one
haasal May 30, 2022
f257604
chore: introduce new User classes
haasal May 30, 2022
24a52e4
chore: secret key gets generated at runtime
haasal May 30, 2022
0dcfc0e
feat: Auth by jwt in cookie + name change of route /login
haasal May 30, 2022
c43aceb
feat: More user functions
haasal May 30, 2022
8aabd14
fix: reformat to satisfy flake8
haasal May 31, 2022
5a7da54
fix: second try at satisfying static check
haasal May 31, 2022
11135ef
added myself as contributor
haasal May 31, 2022
6b2b905
fix: remove get secret key tests as it isn't necessary to read it man…
haasal May 31, 2022
99534a3
fix: delete create_access_token tests as they are generated automatic…
haasal May 31, 2022
8f55982
fix: minor adjustements to reflect changed error codes
haasal May 31, 2022
777a09f
message: Non expiring tokens are a security risk and aren't really ne…
haasal May 31, 2022
8afb278
switch machine
haasal May 31, 2022
d533456
fix: rename test_login to test_user + adjusting test to new api
haasal May 31, 2022
5c3c790
todo: maybe make test independent of login
haasal May 31, 2022
cc0e441
fix: adjusting test_resources to new api
haasal May 31, 2022
ac23677
fix: adjusting base test case routers to new api
haasal May 31, 2022
f89b6f9
todo: deleted obvious todo
haasal May 31, 2022
5427350
chore: deleted generate_token cli as it was deemed obscolete
haasal May 31, 2022
bba8c04
chore: deleted generate_token tests
haasal May 31, 2022
3112ccf
fix: delete all traces of secret_key to fix test_service
haasal May 31, 2022
01dfcaf
todo: do some refactoring in the future
haasal May 31, 2022
6ea50ab
refactor: move the test_user into the base test class
haasal May 31, 2022
0464c20
refactor: move login into base test class
haasal May 31, 2022
f1b9571
chore: delete headers as they aren't needed
haasal May 31, 2022
b69f865
feat: added test for all /user functions
haasal May 31, 2022
fb26c24
fix: remove old headers
haasal May 31, 2022
8155995
formatting
haasal May 31, 2022
5ce9361
Merge branch 'master' of https://github.com/haasal/tardis
haasal May 31, 2022
eedc12d
formatting
haasal May 31, 2022
d665cbd
feat: introducing a scope enum for better typesafety
haasal Jun 1, 2022
616ea9e
fix: scopes could't be set manually at login
haasal Jun 1, 2022
6267e72
chore: clean up imports
haasal Jun 2, 2022
95eb89c
chore: changed scopes to Optional parameter but this doesn't seem to …
haasal Jun 2, 2022
ed0500d
chore: rewrote check_authorization for cookie auth
haasal Jun 2, 2022
db0723f
chore: replace int error with fastapi literal
haasal Jun 2, 2022
8df4ff8
chore: reintroduce authorization into api functions
haasal Jun 2, 2022
a2f364b
todo: add functionality to /user/token_scopes
haasal Jun 2, 2022
3a96e55
chore: introduced and fixed enum scopes into api
haasal Jun 2, 2022
0a2411a
note: please check these two lines carefully in case I made an error
haasal Jun 2, 2022
b798388
feat: delete scope added
haasal Jun 2, 2022
dc01e5e
refactoring: minor
haasal Jun 3, 2022
4b9a427
feat: :sparkles: Added new crud functions to retrieve available types…
haasal Jun 7, 2022
81285e8
feat: :sparkles: New router with functions for retrieving types from DB
haasal Jun 7, 2022
7ecce87
fix: :bug: Refresh token didn't update scopes/claims
haasal Jun 7, 2022
c9c375e
fix: :bug: jwt auth fastapi package was using 422 for Unauthorized re…
haasal Jun 24, 2022
6c1ea0c
chore: :fire: Deleted SQL types
haasal Jul 28, 2022
e26e40d
comment
haasal Jul 28, 2022
ef873c2
chore: :fire: Deleted TODO
haasal Jul 28, 2022
efc2bc9
chore: New TODO comment in /token_scopes path
haasal Jul 28, 2022
5e9c142
Merge branch 'MatterMiners:master' into master
haasal Jul 28, 2022
52e7573
chore: :fire: unused import
haasal Jul 29, 2022
ef58a8c
feat: Return required scopes if authorization fails
haasal Jul 29, 2022
cdafd4a
feat: Implemented the get_token_scopes function
haasal Jul 29, 2022
235d644
test: :white_check_mark: Added and updated tests to reflect the chang…
haasal Jul 29, 2022
d8b64a2
style: :art: Fixed linting and format errors
haasal Jul 29, 2022
f38c884
style: B950
haasal Jul 29, 2022
ba68bb7
flake8
haasal Jul 29, 2022
ebf5fbb
chore: :adhesive_bandage: changed delete logout to post
haasal Aug 2, 2022
a402613
chore: :heavy_plus_sign: Added the feature 'rest' as an extra.
haasal Aug 2, 2022
b91cfa3
fix: :heavy_plus_sign: Added dependency to TESTS_REQUIRE
haasal Aug 2, 2022
c58b6dc
docs: Explanation for custom jwt exception handler
haasal Aug 16, 2022
cfd7eb5
chore: deleted unused scope
haasal Aug 16, 2022
05d0e45
fix: changed import order
haasal Aug 16, 2022
34b586a
fix: added type hints
haasal Aug 16, 2022
e2dbabd
fix: added type hints in types.py
haasal Aug 16, 2022
3d47622
chore: deprectate the get_algorithm() function
haasal Aug 16, 2022
b3565ed
fix: code quality in sql_to_list()
haasal Aug 16, 2022
e2670f9
feat: scope guard for user:get scope in login func
haasal Aug 16, 2022
9862907
fix: added rest as a feature properly
haasal Aug 16, 2022
8ffaf2f
fix: DELETE -> PATCH in /drain
haasal Aug 16, 2022
2996a5e
chore: properly deprecated algorithm config
haasal Aug 17, 2022
7648a03
feat: implemented the shutdown_drone() API
haasal Aug 17, 2022
0955055
fix: replaced TardisError with HTTPException
haasal Aug 17, 2022
00ae203
chore: removed unused scope enum variants
haasal Aug 17, 2022
43bb02d
some comments
haasal Aug 18, 2022
0a631fc
fix: deleted init to settings
haasal Aug 18, 2022
1c96bd1
feat: expiration param in /login
haasal Aug 18, 2022
5fe3347
test: fixed failing algo tests
haasal Aug 18, 2022
427afe3
fix: added '!' for consistency
haasal Aug 19, 2022
f6606f0
test: made HTTPException more specific
haasal Aug 19, 2022
cd700f6
feat: utility functions and fixed scopes
haasal Aug 19, 2022
d44f526
test: added passing test for draining drones
haasal Aug 19, 2022
6d7fbef
test: improved authorization tests
haasal Aug 19, 2022
7f54f0f
style: formatting and linting fixes
haasal Aug 19, 2022
071aac4
fix: adjusted type hint for 3.8
haasal Aug 19, 2022
dc16742
chore: removed unused import
haasal Aug 19, 2022
de4a86f
chore: renamed shutdown_drone to drain_drone
haasal Aug 23, 2022
2d493ea
fix: narrowed Exception handling in sql_to_list
haasal Aug 23, 2022
97320d1
tests: asserted that mock_crud was called
haasal Aug 23, 2022
ac0ebb9
flake fix
haasal Aug 23, 2022
e5c90aa
Merge branch 'MatterMiners:master' into master
haasal Aug 23, 2022
26477d5
fix: Removing db files
haasal Aug 23, 2022
52bf67b
Merge branch 'master' of https://github.com/haasal/tardis
haasal Aug 23, 2022
cc65578
fix: 'fixing' flake error
haasal Aug 23, 2022
d28a0c8
Deleted unused test_authorization function
haasal Aug 24, 2022
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
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ Matthias Schnepf <maschnepf@schnepf-net.de>
Peter Wienemann <peter.wienemann@uni-bonn.de>
rfvc <florian.voncube@gmail.com>
PSchuhmacher <leon_schuhmacher@yahoo.de>
Alexander Haas <mail@haas-alexander.com>
Binary file added drone_registry.db-shm
Binary file not shown.
File renamed without changes.
25 changes: 17 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
long_description = read_me.read()

TESTS_REQUIRE = ["flake8", "httpx"]
REST_REQUIRES = [
"fastapi-jwt-auth",
"fastapi",
"python-jose",
"uvicorn[standard]<=0.14.0", # to support python3.6 (Centos 7)
"typer",
"bcrypt",
"python-multipart",
]


def get_cryptography_version():
Expand Down Expand Up @@ -83,12 +92,6 @@ def get_cryptography_version():
"kubernetes_asyncio",
"pydantic",
"asyncstdlib",
"fastapi",
"python-jose",
"uvicorn[standard]<=0.14.0", # to support python3.6 (Centos 7)
"typer",
"bcrypt",
"python-multipart",
"typing_extensions",
"backports.cached_property",
],
Expand All @@ -99,9 +102,15 @@ def get_cryptography_version():
"sphinxcontrib-contentui",
"myst_parser",
],
"rest": REST_REQUIRES,
"test": TESTS_REQUIRE,
"contrib": ["flake8", "flake8-bugbear", "black; implementation_name=='cpython'"]
+ TESTS_REQUIRE,
"contrib": [
"flake8",
"flake8-bugbear",
"black; implementation_name=='cpython'",
]
+ TESTS_REQUIRE
+ REST_REQUIRES,
},
tests_require=TESTS_REQUIRE,
zip_safe=False,
Expand Down
23 changes: 23 additions & 0 deletions tardis/rest/app/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,26 @@ async def get_resources(sql_registry):
JOIN Sites S ON R.site_id = S.site_id
JOIN MachineTypes MT ON R.machine_type_id = MT.machine_type_id"""
return await sql_registry.async_execute(sql_query, {})


async def get_available_states(sql_registry):
sql_query = "SELECT state FROM ResourceStates"
return await sql_registry.async_execute(sql_query, {})


async def get_available_sites(sql_registry):
sql_query = "SELECT site_name FROM Sites"
return await sql_registry.async_execute(sql_query, {})


async def get_available_machine_types(sql_registry):
sql_query = "SELECT machine_type FROM MachineTypes"
return await sql_registry.async_execute(sql_query, {})


async def set_state_to_draining(sql_registry, drone_uuid: str):
sql_query = """
UPDATE Resources
SET state_id = (SELECT state_id FROM ResourceStates WHERE state = 'DrainState')
WHERE drone_uuid = :drone_uuid"""
return await sql_registry.async_execute(sql_query, dict(drone_uuid=drone_uuid))
25 changes: 21 additions & 4 deletions tardis/rest/app/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from ...__about__ import __version__
from .routers import login, resources
from .routers import resources, user, types
from fastapi_jwt_auth.exceptions import AuthJWTException
from fastapi import Request
from fastapi.responses import JSONResponse

from fastapi import FastAPI

Expand All @@ -22,11 +25,25 @@
"description": "Information about the currently managed resources.",
},
{
"name": "login",
"description": "Handles login and creation of limited duration tokens to access APIs.", # noqa B509
"name": "user",
"description": "Handles login, refresh tokens, logout and anything related to the user.", # noqa B509
},
],
)


# the jwt auth package returns 422 on failed auth which doens't make sense
# This code changes that. There is currently an issue running: https://github.com/IndominusByte/fastapi-jwt-auth/issues/20 # noqa B950
@app.exception_handler(AuthJWTException)
def authjwt_exception_handler(request: Request, exc: AuthJWTException):
detail = exc.message

if detail == "Signature verification failed" or detail == "Signature has expired":
haasal marked this conversation as resolved.
Show resolved Hide resolved
exc.status_code = 401

return JSONResponse(status_code=exc.status_code, content={"detail": detail})


app.include_router(resources.router)
app.include_router(login.router)
app.include_router(user.router)
app.include_router(types.router)
18 changes: 0 additions & 18 deletions tardis/rest/app/routers/login.py

This file was deleted.

23 changes: 18 additions & 5 deletions tardis/rest/app/routers/resources.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from .. import security, crud, database
from ....plugins.sqliteregistry import SqliteRegistry
from fastapi import APIRouter, Depends, HTTPException, Path, Security

from fastapi import APIRouter, Depends, HTTPException, Path, Security, status
from ..scopes import Resources
from fastapi_jwt_auth import AuthJWT

router = APIRouter(prefix="/resources", tags=["resources"])

Expand All @@ -10,20 +11,32 @@
async def get_resource_state(
drone_uuid: str = Path(..., regex=r"^\S+-[A-Fa-f0-9]{10}$"),
sql_registry: SqliteRegistry = Depends(database.get_sql_registry()),
_: str = Security(security.check_authorization, scopes=["resources:get"]),
_: AuthJWT = Security(security.check_authorization, scopes=[Resources.get]),
):
query_result = await crud.get_resource_state(sql_registry, drone_uuid)
try:
query_result = query_result[0]
except IndexError:
raise HTTPException(status_code=404, detail="Drone not found") from None
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Drone not found"
) from None
return query_result


@router.get("/", description="Get list of managed resources")
async def get_resources(
sql_registry: SqliteRegistry = Depends(database.get_sql_registry()),
_: str = Security(security.check_authorization, scopes=["resources:get"]),
_: AuthJWT = Security(security.check_authorization, scopes=[Resources.get]),
):
query_result = await crud.get_resources(sql_registry)
return query_result


@router.patch("/{drone_uuid}/drain", description="Gently shut shown drone")
async def shutdown_drone(
haasal marked this conversation as resolved.
Show resolved Hide resolved
drone_uuid: str = Path(..., regex=r"^\S+-[A-Fa-f0-9]{10}$"),
sql_registry: SqliteRegistry = Depends(database.get_sql_registry()),
_: AuthJWT = Security(security.check_authorization, scopes=[Resources.patch]),
):
await crud.set_state_to_draining(sql_registry, drone_uuid)
mschnepf marked this conversation as resolved.
Show resolved Hide resolved
return {"msg": "Drone set to DrainState"}
44 changes: 44 additions & 0 deletions tardis/rest/app/routers/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Dict, List
from tardis.exceptions.tardisexceptions import TardisError
from .. import security
from .. import crud, database
from ....plugins.sqliteregistry import SqliteRegistry
from fastapi import APIRouter, Depends, Security
from ..scopes import Resources
from fastapi_jwt_auth import AuthJWT

router = APIRouter(prefix="/types", tags=["types", "resources"])


def sql_to_list(query_result: List[Dict]) -> List[str]:
haasal marked this conversation as resolved.
Show resolved Hide resolved
try:
return [list(pair.values())[0] for pair in query_result]
except Exception as e:
haasal marked this conversation as resolved.
Show resolved Hide resolved
raise TardisError("Query result has invalid format") from e


@router.get("/states", description="Get all available states")
async def get_resource_state(
sql_registry: SqliteRegistry = Depends(database.get_sql_registry()),
_: AuthJWT = Security(security.check_authorization, scopes=[Resources.get]),
):
query_result = await crud.get_available_states(sql_registry)
return sql_to_list(query_result)


@router.get("/sites", description="Get all available sites")
async def get_resource_sites(
sql_registry: SqliteRegistry = Depends(database.get_sql_registry()),
_: AuthJWT = Security(security.check_authorization, scopes=[Resources.get]),
):
query_result = await crud.get_available_sites(sql_registry)
return sql_to_list(query_result)


@router.get("/machine_types", description="Get all available machine types")
async def get_resource_types(
sql_registry: SqliteRegistry = Depends(database.get_sql_registry()),
_: AuthJWT = Security(security.check_authorization, scopes=[Resources.get]),
):
query_result = await crud.get_available_machine_types(sql_registry)
return sql_to_list(query_result)
101 changes: 101 additions & 0 deletions tardis/rest/app/routers/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from typing import Optional
from ..scopes import User
from .. import security
from fastapi import APIRouter, Depends, Security
from fastapi_jwt_auth import AuthJWT

router = APIRouter(prefix="/user", tags=["user"])


@router.post(
"/login",
description="Sets httponly access token in session cookie. The scopes are optional.", # noqa B950
)
async def login(
login_user: security.LoginUser,
expires_delta: Optional[int] = None,
Authorize: AuthJWT = Depends(),
):
user = security.check_authentication(login_user.user_name, login_user.password)

# set and check the scopes that are applied to the returned token
if login_user.scopes is None:
scopes = {"scopes": user.scopes}
else:
# The next two lines are very critical as if wrongly implemented a user can give his token unlimited scopes. # noqa B950
# This functionality has to be tested thoroughly
security.check_scope_permissions(login_user.scopes, user.scopes)
scopes = {"scopes": login_user.scopes}

access_token = Authorize.create_access_token(
subject=user.user_name, user_claims=scopes, expires_time=expires_delta
)
refresh_token = Authorize.create_refresh_token(
subject=user.user_name, user_claims=scopes
)

Authorize.set_access_cookies(access_token)
Authorize.set_refresh_cookies(refresh_token)

# If the user doesn't have the user:get scope, he can't get the user data. # noqa B950
if User.get not in user.scopes:
return {
"msg": "Successfully logged in!",
}
else:
return {
"msg": "Successfully logged in!",
"user": security.BaseUser(
user_name=user.user_name, scopes=scopes["scopes"]
),
}


@router.post(
"/logout",
description="Logout the current user by deleting all access token cookies",
)
async def logout(Authorize: AuthJWT = Depends()):
Authorize.jwt_required()

Authorize.unset_jwt_cookies()
return {"msg": "Successfully logged out!"}


@router.post(
"/refresh",
description="Use refresh token cookie to refresh expiration on cookie", # noqa B950
)
async def refresh(Authorize: AuthJWT = Depends()):
Authorize.jwt_refresh_token_required()

current_user = Authorize.get_jwt_subject()
scopes = security.get_token_scopes(Authorize)

new_access_token = Authorize.create_access_token(
subject=current_user, user_claims=({"scopes": scopes})
)

Authorize.set_access_cookies(new_access_token)
return {"msg": "Token successfully refreshed"}


@router.get(
"/me",
response_model=security.BaseUser,
description="Get the user data how it's stored in the database (no password)",
)
async def get_user_me(
Authorize: AuthJWT = Security(security.check_authorization, scopes=[User.get]),
):
Authorize.jwt_required()

user_name = Authorize.get_jwt_subject()
return security.get_user(user_name)


@router.get("/token_scopes", description="get scopes of CURRENT token (not of user)")
async def get_token_scopes(Authorize: AuthJWT = Depends()):
Authorize.jwt_required()

return security.get_token_scopes(Authorize)
12 changes: 12 additions & 0 deletions tardis/rest/app/scopes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from enum import Enum

# All available OAuth2 scopes


class Resources(str, Enum):
get = "resources:get"
patch = "resources:patch"


class User(str, Enum):
get = "user:get"
Loading