Skip to content

Commit

Permalink
Merge pull request #66 from dianagudu/api_versioning
Browse files Browse the repository at this point in the history
API versioning
  • Loading branch information
dianagudu authored Jan 18, 2024
2 parents d685d56 + 33b4067 commit ea74e0a
Show file tree
Hide file tree
Showing 26 changed files with 425 additions and 228 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.6.1
current_version = 0.7.0
commit = True
tag = False

Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/mirror-repo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Mirror Repository

on:
- push
- delete

jobs:
sync:
runs-on: ubuntu-latest
name: Git Repo Sync
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: wangchucheng/git-repo-sync@v0.1.0
with:
target-url: ${{ secrets.GITLAB_MIRROR_URL }}
target-username: ${{ secrets.GITLAB_MIRROR_USERNAME }}
target-token: ${{ secrets.GITLAB_MIRROR_TOKEN }}
30 changes: 22 additions & 8 deletions .gitlab-ci-scripts/set-prerelease-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,29 @@ done
# Get master branch name:
# use origin if exists
# else use last found remote
REMOTES=$(git remote show)
for R in $REMOTES; do
MASTER=$(git remote show "$R" 2>/dev/null \
| sed -n '/HEAD branch/s/.*: //p')
MASTER_BRANCH="refs/remotes/${R}/${MASTER}"
#echo "Master-branch: ${MASTER_BRANCH}"
[ "x${R}" == "xorigin" ] && break
done
MASTER_BRANCH=""
get_master_branch_of_mteam() {
git remote -vv | awk -F[\\t@:] '{ print $1 " " $3 }' | while read REMOTE HOST; do
# echo " $HOST -- $REMOTE"
MASTER=$(git remote show "$REMOTE" 2>/dev/null \
| sed -n '/HEAD branch/s/.*: //p')
MASTER_BRANCH="refs/remotes/${REMOTE}/${MASTER}"
[ "x${HOST}" == "xcodebase.helmholtz.cloud" ] && {
echo "${MASTER_BRANCH}"
break
}
[ "x${HOST}" == "xgit.scc.kit.edu" ] && {
echo "${MASTER_BRANCH}"
break
}
[ "x${REMOTE}" == "xorigin" ] && {
echo "${MASTER_BRANCH}"
break
}
done
}

MASTER_BRANCH=$(get_master_branch_of_mteam)
PREREL=$(git rev-list --count HEAD ^"$MASTER_BRANCH")

# if we use a version file, things are easy:
Expand Down
16 changes: 8 additions & 8 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
---
include:
- 'https://git.scc.kit.edu/m-team/ci-voodoo/raw/master/ci-include/generic-ci.yml'
- 'https://git.scc.kit.edu/m-team/ci-voodoo/raw/master/ci-include/pipeline-jobs.yml'
- 'https://git.scc.kit.edu/m-team/ci-voodoo/raw/master/ci-include/pipeline-jobs-publish-to-repo.yml'
- "https://codebase.helmholtz.cloud/m-team/tools/ci-voodoo/raw/master/ci-include/generic-ci.yml"
- "https://codebase.helmholtz.cloud/m-team/tools/ci-voodoo/raw/master/ci-include/pipeline-jobs.yml"
- "https://codebase.helmholtz.cloud/m-team/tools/ci-voodoo/raw/master/ci-include/pipeline-jobs-publish-to-repo.yml"
- "https://codebase.helmholtz.cloud/m-team/tools/ci-voodoo/raw/master/ci-include/github-status-sync.yml"

default:
tags:
- linux

variables:
STAGING_BRANCH_NAME: 'staging'
DOCKER_IMAGE_NAMESPACE: 'marcvs/build'
DOCKER_IMAGE_NAME: 'motley-cue'
UPSTREAM_PROJECT: dianagudu/motley_cue
STAGING_BRANCH_NAME: "staging"
DOCKER_IMAGE_NAMESPACE: "marcvs/build"
DOCKER_IMAGE_NAME: "motley-cue"
#PREREL_BRANCH_NAME: 'ci/adapt-to-pam-ssh-oidc'
#TARGET_REPO: 'devel'
# The following varialbes can be overwritten only in specific targets
Expand Down Expand Up @@ -54,5 +56,3 @@ build-ubuntu-bionic:
integration-tests:
extends:
- .trigger-integration-tests-ssh-oidc


2 changes: 1 addition & 1 deletion debian/changelog
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
motley-cue (0.6.1-1) UNRELEASED; urgency=medium
motley-cue (0.7.0-1) UNRELEASED; urgency=medium

[ Marcus Hardt ]
* Initial go for packageing
Expand Down
6 changes: 6 additions & 0 deletions etc/motley_cue.conf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ log_level = WARNING
# enable_docs = False
## location of swagger docs -- default: /docs
# docs_url = /docs
## location of redoc docs -- default: /redoc
# redoc_url = /redoc
##
## API version -- default: v1
## supported versions: v1
# api_version = v1

############
[mapper.otp]
Expand Down
1 change: 1 addition & 0 deletions etc/motley_cue.sudo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
motley_cue ALL=(root) NOPASSWD: /usr/sbin/useradd, /usr/sbin/userdel, /usr/sbin/groupadd, /usr/bin/chage, /usr/bin/pkill, /usr/sbin/usermod
2 changes: 1 addition & 1 deletion motley_cue/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.1
0.7.0
232 changes: 95 additions & 137 deletions motley_cue/api.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
"""
This module contains the definition of motley_cue's REST API.
"""
from fastapi import FastAPI, Depends, Request, Query, Header
from fastapi.exceptions import RequestValidationError, ResponseValidationError
from fastapi.responses import HTMLResponse
import logging
import pkgutil
import importlib
from typing import Union
from pydantic import ValidationError
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError, ResponseValidationError

from .dependencies import mapper, Settings
from .routers import user, admin
from .models import Info, InfoAuthorisation, InfoOp, VerifyUser, responses, ClientError
from .mapper.exceptions import (
validation_exception_handler,
request_validation_exception_handler,
from motley_cue.dependencies import mapper, Settings
from motley_cue.mapper.exceptions import (
InternalException,
InvalidResponse,
MissingParameter,
)
import motley_cue.apis

settings = Settings(docs_url=mapper.config.docs_url)
logger = logging.getLogger(__name__)

# API settings
settings = Settings(
docs_url=mapper.config.docs_url,
redoc_url=mapper.config.redoc_url,
api_version=mapper.config.api_version,
)

# create FastAPI app
api = FastAPI(
title=settings.title,
description=settings.description,
Expand All @@ -24,132 +33,81 @@
redoc_url=settings.redoc_url,
)

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


@api.get("/")
async def read_root():
"""Retrieve general API information:
* description
* available endpoints
* security
"""
return {
"description": "This is the user API for mapping remote identities to local identities.",
"usage": "All endpoints are available via a bearer token.",
"endpoints": {
"/info": "Service-specific information.",
"/info/authorisation": (
"Authorisation information for specific OP; "
"requires valid access token from a supported OP."
),
"/info/op": (
"Information about a specific OP specified via a query parameter 'url'; "
"does not require an access token."
),
"/user": "User API; requires valid access token of an authorised user.",
"/admin": (
"Admin API; requires valid access token of an authorised user with admin role."
),
"/verify_user": (
"Verifies if a given token belongs to a given local account via 'username'."
),
},
}


@api.get("/info", response_model=Info, response_model_exclude_unset=True)
async def info():
"""Retrieve service-specific information:
* login info
* supported OPs
* ops_info per OP information, such as scopes, audience, etc.
"""
return mapper.info()


@api.get(
"/info/authorisation",
dependencies=[Depends(mapper.user_security)],
response_model=InfoAuthorisation,
response_model_exclude_unset=True,
responses={**responses, 200: {"model": InfoAuthorisation}},
)
@mapper.authenticated_user_required
async def info_authorisation(
request: Request,
header: str = Header(..., alias="Authorization", description="OIDC Access Token"),
): # pylint: disable=unused-argument
"""Retrieve authorisation information for specific OP.
Requires:
* that the OP is supported
* authentication with this OP
"""
return mapper.info_authorisation(request)


@api.get(
"/info/op",
response_model=InfoOp,
response_model_exclude_unset=True,
responses={**responses, 200: {"model": InfoOp}},
)
async def info_op(
request: Request,
url: str = Query(..., description="OP URL"),
): # pylint: disable=unused-argument
"""Retrieve additional information for specific OP, such as required scopes.
\f
:param url: OP URL
# get routers for all api versions
api_routers = {}
for version_submodule in pkgutil.iter_modules(motley_cue.apis.__path__):
if version_submodule.name.startswith("v"):
try:
api_routers[version_submodule.name] = importlib.import_module(
f"motley_cue.apis.{version_submodule.name}.api"
).router
except AttributeError as exc:
logger.error(
"API version %s does not have a router",
version_submodule.name,
exc_info=exc,
)
raise InternalException(
f"API version {version_submodule.name} does not have a router."
) from exc
except Exception as exc:
logger.error(
"Could not import API version %s",
version_submodule.name,
exc_info=exc,
)
raise InternalException(
f"Could not import API version {version_submodule.name}"
) from exc

try:
current_api_router = api_routers[settings.api_version]
except KeyError as exc:
raise InternalException(
f"API version {settings.api_version} does not exist."
) from exc

# current API version
api.include_router(current_api_router, prefix="/api", tags=["API"])
# for compatibility with old API, include all endpoints in the root
api.include_router(current_api_router, prefix="", include_in_schema=False)

# all API versions
for api_version, api_router in api_routers.items():
api.include_router(
api_router, prefix=f"/api/{api_version}", tags=[f"API {api_version}"]
)

# Logo for redoc. This must be at the end after all the routes have been set!
api.openapi()["info"]["x-logo"] = {
"url": "https://motley-cue.readthedocs.io/en/latest/_static/logos/motley-cue.png"
}


# Exception handlers
@api.exception_handler(ResponseValidationError)
@api.exception_handler(ValidationError)
async def validation_exception_handler(
request: Request, validation_exc: Union[ResponseValidationError, ValidationError]
):
"""Replacement callback for handling ResponseValidationError exceptions.
:param request: request object that caused the ResponseValidationError
:param validation_exc: ResponseValidationError containing validation errors
"""
return mapper.info_op(url)
_ = request
_ = validation_exc
return InvalidResponse(validation_exc)


@api.get(
"/verify_user",
dependencies=[Depends(mapper.user_security)],
response_model=VerifyUser,
responses={**responses, 200: {"model": VerifyUser}},
)
@mapper.inject_token
@mapper.authorised_user_required
async def verify_user(
request: Request,
username: str = Query(
...,
description="username to compare to local username",
),
header: str = Header(
...,
alias="Authorization",
description="OIDC Access Token or valid one-time token",
),
): # pylint: disable=unused-argument
"""Verify that the authenticated user has a local account with the given **username**.
@api.exception_handler(RequestValidationError)
async def request_validation_exception_handler(
request: Request, validation_exc: RequestValidationError
):
"""Replacement callback for handling RequestValidationError exceptions.
Requires the user to be authorised on the service.
\f
:param username: username to compare to local username
:param request: request object that caused the RequestValidationError
:param validation_exc: RequestValidationError containing validation errors
"""
return mapper.verify_user(request, username)


@api.get("/privacy", response_class=HTMLResponse)
async def privacy():
return mapper.get_privacy_policy()


# Logo for redoc (currently disabled).
# This must be at the end after all the routes have been set!
# api.openapi()["info"]["x-logo"] = {
# "url": "https://motley-cue.readthedocs.io/en/latest/_static/logos/motley-cue.png"
# }
_ = request
return MissingParameter(validation_exc)
File renamed without changes.
Loading

1 comment on commit ea74e0a

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage for this commit

87.61%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
motley_cue
   __init__.py100%100%100%100%
   __main__.py0%100%100%0%11–12, 3, 6, 8
   _version.py100%100%100%100%
   api.py81.40%100%100%81.40%44–45, 50, 53–54, 59, 65–66
   dependencies.py93.75%100%100%93.75%28, 44
   models.py100%100%100%100%
   static.py33.33%100%100%33.33%38–39, 41–44
motley_cue/apis
   __init__.py100%100%100%100%
   utils.py92.86%100%100%92.86%13
motley_cue/apis/v1
   __init__.py100%100%100%100%
   admin.py100%100%100%100%
   api.py100%100%100%100%
   root.py96.30%100%100%96.30%138
   user.py100%100%100%100%
motley_cue/mapper
   __init__.py59.57%100%100%59.57%105, 112, 119, 127, 134–137, 143–144, 150–151, 157–159, 167, 173, 180–181, 190–191, 195, 198–204, 29–30, 32–33, 36, 38, 56, 93, 99
   authorisation.py91.59%100%100%91.59%138–140, 146, 204, 40, 53, 66, 99
   config.py83.76%100%100%83.76%113, 116–120, 130–138, 153–157, 161–162, 201–204, 213–215, 238, 268, 271–273, 275, 279, 356, 99
   exceptions.py96.88%100%100%96.88%30
   local_user_management.py90.11%100%100%90.11%160–161, 163–167, 74–75
   token_manager.py95.31%100%100%95.31%164, 201, 243, 278, 377, 379, 383, 397, 450–451, 63–64, 68

Please sign in to comment.