Skip to content

Commit

Permalink
Merge branch 'add-testing'
Browse files Browse the repository at this point in the history
  • Loading branch information
dianagudu committed Feb 23, 2022
2 parents fd12bb5 + 54ea004 commit a61fb86
Show file tree
Hide file tree
Showing 24 changed files with 1,191 additions and 48 deletions.
4 changes: 4 additions & 0 deletions .env-template
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
OIDC_AGENT_ACCOUNT=egi
MC_SUB=c2370093c19496aeb46103cce3ccdc7b183f54ac9ba9c859dea94dfba23aacd5@egi.eu
MC_VO=urn:mace:egi.eu:group:eosc-synergy.eu
MC_ISS=https://aai.egi.eu/oidc/
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ The logo is licensed under the `Creative Commons Attribution 4.0 International L
..
.. image:: https://i.creativecommons.org/l/by/4.0/88x31.png
:target: http://creativecommons.org/licenses/by/4.0/
:alt: CC BY 4.0
:alt: CC BY 4.0
11 changes: 6 additions & 5 deletions motley_cue/api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from fastapi import FastAPI, Depends, Request, Query, Header
from fastapi.exceptions import RequestValidationError
from pydantic import ValidationError

from .dependencies import mapper, settings
from .dependencies import mapper, Settings
from .routers import user, admin
from .models import Info, InfoAuthorisation, VerifyUser, responses
from .mapper.exceptions import validation_exception_handler
from .mapper.exceptions import validation_exception_handler, request_validation_exception_handler


settings = Settings(docs_url=mapper.config.docs_url)
api = FastAPI(
title=settings.title,
description=settings.description,
Expand All @@ -17,12 +19,11 @@
)

api.include_router(user.api,
prefix="/user",
tags=["user"])
api.include_router(admin.api,
prefix="/admin",
tags=["admin"])
api.add_exception_handler(RequestValidationError, validation_exception_handler)
api.add_exception_handler(RequestValidationError, request_validation_exception_handler)
api.add_exception_handler(ValidationError, validation_exception_handler)


@api.get("/")
Expand Down
19 changes: 15 additions & 4 deletions motley_cue/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Optional
from pydantic import BaseSettings
from pydantic import BaseSettings, validator

from .version import __version__
from .mapper import Mapper
from .mapper import Mapper, Config


class Settings(BaseSettings):
Expand All @@ -13,6 +13,17 @@ class Settings(BaseSettings):
docs_url: Optional[str] = None
redoc_url: Optional[str] = None

@validator("openapi_url", allow_reuse=True)
def must_start_with_slash(cls, url):
if not url.startswith("/"):
raise ValueError("Routed paths must start with '/'")
return url

mapper = Mapper()
settings = Settings(docs_url=mapper.config.docs_url) # only the Swagger docs can be enabled and configured, ReDoc is disabled
@validator("docs_url", "redoc_url", allow_reuse=True)
def must_start_with_slash_or_none(cls, url):
if url is not None and not url.startswith("/"):
raise ValueError("Routed paths must start with '/'")
return url


mapper = Mapper(Config.from_files([]))
12 changes: 6 additions & 6 deletions motley_cue/mapper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,8 @@ class Mapper:
as well as interfacing with the local user management
"""

def __init__(self, config_file=None):
if config_file is None:
self.__config = Config.from_default_files()
else:
self.__config = Config.from_files([config_file])

def __init__(self, config: Config):
self.__config = config
# configure logging
if self.__config.log_file is None or \
self.__config.log_file == "/dev/stderr":
Expand All @@ -47,6 +43,10 @@ def __init__(self, config_file=None):
self.__authorisation = Authorisation(self.__config)
self.__lum = LocalUserManager()

@property
def authorisation(self):
return self.__authorisation

@property
def config(self):
return self.__config
Expand Down
14 changes: 11 additions & 3 deletions motley_cue/mapper/authorisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from flaat import Flaat, tokentools
from aarc_g002_entitlement import Aarc_g002_entitlement, Aarc_g002_entitlement_Error

from .config import canonical_url, to_bool, to_list
from .config import Config, canonical_url, to_bool, to_list
from .exceptions import Unauthorised, BadRequest
from .utils import AuthorisationType, EXACT_OP_URLS

Expand All @@ -19,18 +19,26 @@ class Authorisation(Flaat):
- stringify authorisation info
"""

def __init__(self, config):
def __init__(self, config: Config):
super().__init__()
super().set_web_framework("fastapi")
super().set_cache_lifetime(120) # seconds; default is 300
super().set_trusted_OP_list(config.trusted_ops)
super().set_verbosity(config.verbosity)
self.__authorisation = config.authorisation

@property
def authorisation(self):
return self.__authorisation

def info(self, request):
# get OP from request
token = tokentools.get_access_token_from_request(request)
op_url = self.get_issuer_from_accesstoken(token)
try:
op_url = self.get_issuer_from_accesstoken(token)
except Exception as e:
logging.getLogger(__name__).debug(e)
op_url = None
if op_url is None:
raise Unauthorised("Could not determine token issuer. Token is not a JWT or OP not supported.")
op_authz = self.__authorisation.get(canonical_url(op_url), None)
Expand Down
50 changes: 32 additions & 18 deletions motley_cue/mapper/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@
import logging
from pathlib import Path
from configparser import ConfigParser
from typing import List

from .exceptions import InternalException


class Config():
def __init__(self, config_parser):
try:
config_mapper = config_parser['mapper']
except:
raise InternalException("No [mapper] configuration found in configuration file!")
# log level
self.__log_level = config_parser['mapper'].get(
self.__log_level = config_mapper.get(
'log_level', logging.WARNING)
# log file
self.__log_file = config_parser['mapper'].get(
self.__log_file = config_mapper.get(
'log_file', None
)
# swagger docs
if config_parser['mapper'].get("enable_docs", False):
self.__docs_url = config_parser['mapper'].get("docs_url", "/docs")
if config_mapper.get("enable_docs", False):
self.__docs_url = config_mapper.get("docs_url", "/docs")
else:
self.__docs_url = None
# trusted OPs
Expand Down Expand Up @@ -76,18 +83,16 @@ def verbosity(self):
return 1

@staticmethod
def from_default_files():
return Config.from_files(Config._reload_motley_cue_configs())

@staticmethod
def from_files(list_of_config_files):
def from_files(list_of_config_files: List):
list_of_config_files.extend(Config._reload_motley_cue_configs())
config_parser = ConfigParser()
for f in list_of_config_files:
for filename in list_of_config_files:
f = Path(filename)
if f.exists():
files_read = config_parser.read(f)
logging.getLogger(__name__).debug(F"Read config from {files_read}")
break
return Config(config_parser)
return Config(config_parser)
raise InternalException(f"No configuration file found at given or default locations: {list_of_config_files}")

@staticmethod
def _reload_motley_cue_configs():
Expand All @@ -106,8 +111,8 @@ def _reload_motley_cue_configs():
if filename:
files += [Path(filename)]
files += [
Path('motley_cue.conf'),
Path.home()/'.config'/'motley_cue'/'motley_cue.conf'
Path("motley_cue.conf").absolute(),
Path("~/.config/motley_cue/motley_cue.conf").expanduser()
]
files += [Path("/etc/motley_cue/motley_cue.conf")]
return files
Expand All @@ -131,16 +136,25 @@ def canonical_url(url):
def to_bool(bool_str):
"""Convert a string to bool.
"""
return True if bool_str.lower() == 'true' else False
if bool_str.lower() == "true":
return True
elif bool_str.lower() == "false":
return False
else:
raise InternalException(f"Error reading config file: unrecognised boolean value {bool_str}.")


def to_list(list_str):
"""Convert a string to a list.
"""
# strip list of square brackets and remove all whitespace
# remove all whitespace
stripped_list_str = list_str.replace("\n", "")\
.replace(" ", "").replace("\t", "")\
.strip("][").rstrip(",")
.replace(" ", "").replace("\t", "")
# strip list of square brackets
if stripped_list_str.startswith("[") and stripped_list_str.endswith("]"):
stripped_list_str = stripped_list_str[1:-1].strip(",")
else:
raise InternalException(f"Could not parse string as list, must be contained in square brackets: {list_str}")
# check empty list
if stripped_list_str == "":
return []
Expand Down
24 changes: 22 additions & 2 deletions motley_cue/mapper/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pydantic import ValidationError
from fastapi import Request
from fastapi.exceptions import HTTPException, RequestValidationError
from starlette.responses import JSONResponse
Expand Down Expand Up @@ -38,7 +39,6 @@ class MissingParameter(JSONResponse):
and informative message.
"""
def __init__(self, exc: RequestValidationError):
print(str(exc), exc.errors())
errors = exc.errors()
no_errors = len(errors)
message = f"{no_errors} request validation error{'' if no_errors == 1 else 's'}: " + \
Expand All @@ -48,7 +48,27 @@ def __init__(self, exc: RequestValidationError):
super().__init__(status_code=HTTP_400_BAD_REQUEST, content={"detail": message})


async def validation_exception_handler(request: Request, exc: RequestValidationError):
class InternalException(Exception):
"""Wrapper for internal errors
"""
def __init__(self, message) -> None:
self.message = message
super().__init__(message)


async def validation_exception_handler(request: Request, exc: ValidationError):
"""Replacement callback for handling RequestValidationError exceptions.
:param request: request object that caused the RequestValidationError
:param exc: RequestValidationError containing validation errors
"""
return JSONResponse(
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "Could not validate response model."}
)


async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
"""Replacement callback for handling RequestValidationError exceptions.
:param request: request object that caused the RequestValidationError
Expand Down
8 changes: 6 additions & 2 deletions motley_cue/mapper/local_user_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def login_info(self):
login_info = {}
return login_info

def verify_user(self, userinfo, username):
def verify_user(self, userinfo, username: str):
state = "unknown"
local_username = None
try:
Expand All @@ -74,7 +74,7 @@ def verify_user(self, userinfo, username):
raise InternalServerError("Something went wrong.")
return {
"state": state,
"verified": (local_username == username)
"verified": (local_username == username and username is not None)
}

def _reach_state(self, userinfo, state_target: States):
Expand All @@ -97,6 +97,8 @@ def _reach_state(self, userinfo, state_target: States):
raise Unauthorised(msg)
else:
raise InternalServerError(msg)
except Exception:
raise InternalServerError(f"Something went wrong when trying to reach state {data['state_target']}")
else:
result = result.attributes
logging.getLogger(__name__).info(
Expand Down Expand Up @@ -129,6 +131,8 @@ def _admin_action(self, sub: str, iss: str, action: AdminActions):
raise Unauthorised(msg)
else:
raise InternalServerError(msg)
except Exception:
raise InternalServerError(f"Something went wrong with admin action {action.name}.")
else:
result = result.attributes
logging.getLogger(__name__).info(
Expand Down
6 changes: 3 additions & 3 deletions motley_cue/routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..models import FeudalResponse, responses


api = APIRouter()
api = APIRouter(prefix="/admin")


@api.get("")
Expand All @@ -20,8 +20,8 @@ async def read_root():
"description": "This is the admin API for mapping remote identities to local identities.",
"usage": "All endpoints are available using an OIDC Access Token as a bearer token and need subject and issuer of account to be modified, via 'sub' and 'iss' variables.",
"endpoints": {
"/suspend": "Suspends a local account.",
"/resume": "Restores a suspended local account."
f"{api.prefix}/suspend": "Suspends a local account.",
f"{api.prefix}/resume": "Restores a suspended local account."
}
}

Expand Down
8 changes: 4 additions & 4 deletions motley_cue/routers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..models import FeudalResponse, responses


api = APIRouter()
api = APIRouter(prefix="/user")


@api.get("")
Expand All @@ -20,9 +20,9 @@ async def read_root():
"description": "This is the user API for mapping remote identities to local identities.",
"usage": "All endpoints are available using an OIDC Access Token as a bearer token.",
"endpoints": {
"/get_status": "Get information about your local account.",
"/deploy": "Provision local account.",
"/suspend": "Suspend local account."
f"{api.prefix}/get_status": "Get information about your local account.",
f"{api.prefix}/deploy": "Provision local account.",
f"{api.prefix}/suspend": "Suspend local account."
}
}

Expand Down
9 changes: 9 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[pytest]
addopts =
--cov=motley_cue
--cov-report=term-missing:skip-covered
--no-cov-on-fail
--show-capture=log
--log-cli-level=error
env =
FEUDAL_ADAPTER_CONFIG=tests/test_feudal_adapter.conf
5 changes: 5 additions & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
pylint
pytest>=5.4.3
pytest-cov>=2.10.0
pytest-env
python-dotenv
liboidcagent
Empty file added tests/__init__.py
Empty file.
Loading

0 comments on commit a61fb86

Please sign in to comment.