diff --git a/.dockerignore b/.dockerignore index b0cde71..f4be6c6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,5 @@ * -!src -!conf +!app !requirements.* -!.flaskenv +!config.ini +!logging.yaml \ No newline at end of file diff --git a/.flaskenv b/.flaskenv deleted file mode 100644 index 912fe18..0000000 --- a/.flaskenv +++ /dev/null @@ -1,3 +0,0 @@ -FLASK_APP=src/app:create_app() -FLASK_ENV=develop -FLASK_DEBUG=0 diff --git a/.gitignore b/.gitignore index cbb3138..7530f86 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ env/ .vscode .idea +postgres \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 08fa5b0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -language: python -python: 3.6.9 - -env: - DOCKER_COMPOSE_VERSION: 1.25.4 -services: - - docker - -jobs: - include: - - stage: Testing - install: pip install -r src/requirements.txt - script: pytest src - - - stage: container creation and publishing - install: skip - script: travis/containerCreation.sh um-service-template - - - stage: smoke and acceptance test - install: skip # without this there's a `git clone` executed! - script: travis/acceptanceTest.sh um-service-template 8080 7000 # Service name + external port + internal port for docker - - - stage: release - if: branch = master AND NOT type IN (pull_request) - install: skip - script: travis/release.sh um-service-template - -import: - - docs/.travis.yml - -#notifications: -# slack: eoepca:Msk9hjQKAbwSYcVWiepenPim -# email: -# recipients: -# - a.person@acme.com -# - a.n.other@acme.com -# on_success: never # default: change -# on_failure: never # default: always diff --git a/Dockerfile b/Dockerfile index bde3033..738f77c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,7 @@ -FROM python:alpine -RUN apk add --no-cache git -RUN mkdir /app -WORKDIR /app -COPY . . -ENV FLASK_APP "src/app:create_app()" -ENV FLASK_ENV local -ENV FLASK_DEBUG 1 -RUN pip install -r requirements.txt -EXPOSE 5566 -CMD [ "python", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5566"] \ No newline at end of file +FROM python:3.12 +WORKDIR /code +COPY ./requirements.txt /code/requirements.txt +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +COPY ./config.ini /code/config.ini +COPY ./app /code/app +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8080"] \ No newline at end of file diff --git a/README.md b/README.md index f8cc1f5..e0df669 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,3 @@ - - - [![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] @@ -12,26 +5,20 @@ [![MIT License][license-shield]][license-url] ![Build][build-shield] -

- - Logo -

Identity API

- Flask application to enable a REST API server to manage Keycloak through Keycloak Admin API (https://www.keycloak.org/docs-api/21.0.1/rest-api/index.html) and Protection API (https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_protection_api). + FastAPI application exposing a Restful API to manage Keycloak through Keycloak Admin API (https://www.keycloak.org/docs-api/22.0.1/rest-api/index.html) and Protection API (https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_protection_api).
Explore the docs »
- View Demo · Report Bug · Request Feature -

## Table of Contents @@ -42,36 +29,28 @@ - [Getting Started](#getting-started) - [Prerequisites](#prerequisites) - [Installation](#installation) - - [Testing](#testing) - [Documentation](#documentation) - [Usage](#usage) - - [Running the template service](#running-the-template-service) - - [Upgrading Gradle Wrapper](#upgrading-gradle-wrapper) - [Roadmap](#roadmap) - [Contributing](#contributing) - [License](#license) - [Contact](#contact) - [Acknowledgements](#acknowledgements) - ## About The Project -Flask application to enable a REST API server to manage Keycloak through Keycloak Admin API (https://www.keycloak.org/docs-api/21.0.1/rest-api/index.html) and Protection API (https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_protection_api). +FastAPI application exposing a Restful API to manage Keycloak through Keycloak Admin +API (https://www.keycloak.org/docs-api/21.0.1/rest-api/index.html) and Protection +API (https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_protection_api). -Includes three main paths: -- **Resources** - CRUD operations to manage resources -- **Policies** - CRUD operations to manage policies -- **Permissions** - CRUD operations to manage permissions +Swagger docs are available at /docs. +Redoc docs are available at /redoc. ### Built With -- [Python](https://www.python.org//) -- [PyTest](https://docs.pytest.org) -- [YAML](https://yaml.org/) -- [Travis CI](https://travis-ci.com/) - - +- [Python](https://www.python.org) +- [FastAPI](https://fastapi.tiangolo.com) ## Getting Started @@ -79,75 +58,69 @@ To get a local copy up and running follow these simple steps. ### Prerequisites -This is an example of how to list things you need to use the software and how to install them. - -- [Docker](https://www.docker.com/) -- [Python](https://www.python.org//) +- [Docker](https://www.docker.com) +or +- [Docker compose](https://docs.docker.com/compose) +or +- [Python](https://www.python.org) ### Installation -1. Get into EOEPCA's development environment - -```sh -vagrant ssh -``` - -3. Clone the repo +1. Clone the repo ```sh git clone https://github.com/EOEPCA/um-identity-api ``` -4. Change local directory +2. Change local directory ```sh cd um-identity-api ``` -5. Execute +3. Execute - 5.1 Run locally with Python + 3.1 Run with docker compose + ```sh + docker compose up -d --build + ``` + 3.2 Run with Python ```sh pip install -r requirements.txt - python -m "flask" run --host=0.0.0.0 --port=5566 + uvicorn app.main:app ``` - 5.2 Run locally with Docker + 3.3 Run with Docker ```sh - docker build . --progress=plain -t um-identity-api:develop - docker run --rm -dp 5566:5566 --name um-identity-api um-identity-api:develop + docker build . --progress=plain -t um-identity-api:local + docker run --rm -dp 8080:8080 --name um-identity-api um-identity-api:local ``` - 5.3 Run develop branch with Docker + 3.4 Run develop branch with Docker ```sh - docker run --rm -dp 5566:5566 --name um-identity-api ghcr.io/eoepca/um-identity-api:develop + docker run --rm -dp 8080:8080 --name um-identity-api ghcr.io/eoepca/um-identity-api:develop ``` - 5.4 Run master branch with Docker + 3.5 Run master branch with Docker ```sh - docker run --rm -dp 5566:5566 --name um-identity-api ghcr.io/eoepca/um-identity-api:production + docker run --rm -dp 8080:8080 --name um-identity-api ghcr.io/eoepca/um-identity-api:production ``` ## Documentation The component documentation can be found at https://eoepca.github.io/um-identity-api/. - ## Usage -Use this space to show useful examples of how a project can be used. Additional screenshots, code examples and demos work well in this space. You may also link to more resources. - -_For more examples, please refer to the [Documentation](https://example.com)_ - - +Check Redoc page to try out the API, available at http://localhost:8080/redoc ## Roadmap -See the [open issues](https://github.com/EOEPCA/um-identity-api/issues) for a list of proposed features (and known issues). - - +See the [open issues](https://github.com/EOEPCA/um-identity-api/issues) for a list of proposed features (and known +issues). ## Contributing -Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any +contributions you make are **greatly appreciated**. 1. Fork the Project 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) @@ -155,7 +128,6 @@ Contributions are what make the open source community such an amazing place to b 4. Push to the Branch (`git push origin feature/AmazingFeature`) 5. Open a Pull Request - ## License @@ -169,17 +141,27 @@ Project Link: [https://github.com/EOEPCA/um-identity-api](https://github.com/EOE ## Acknowledgements -- README.md is based on [this template](https://github.com/othneildrew/Best-README-Template) by [Othneil Drew](https://github.com/othneildrew). +- README.md is based on [this template](https://github.com/othneildrew/Best-README-Template) + by [Othneil Drew](https://github.com/othneildrew). +[contributors-shield]: https://img.shields.io/github/contributors/EOEPCA/um-identity-api.svg?style=flat-square -[contributors-shield]: https://img.shields.io/github/contributors/EOEPCA/um-identity-apisvg?style=flat-square [contributors-url]: https://github.com/EOEPCA/um-identity-api/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/EOEPCA/um-identity-apisvg?style=flat-square + +[forks-shield]: https://img.shields.io/github/forks/EOEPCA/um-identity-api.svg?style=flat-square + [forks-url]: https://github.com/EOEPCA/um-identity-api/network/members -[stars-shield]: https://img.shields.io/github/stars/EOEPCA/um-identity-apisvg?style=flat-square + +[stars-shield]: https://img.shields.io/github/stars/EOEPCA/um-identity-api.svg?style=flat-square + [stars-url]: https://github.com/EOEPCA/um-identity-api/stargazers -[issues-shield]: https://img.shields.io/github/issues/EOEPCA/um-identity-apisvg?style=flat-square + +[issues-shield]: https://img.shields.io/github/issues/EOEPCA/um-identity-api.svg?style=flat-square + [issues-url]: https://github.com/EOEPCA/um-identity-api/issues -[license-shield]: https://img.shields.io/github/license/EOEPCA/um-identity-apisvg?style=flat-square + +[license-shield]: https://img.shields.io/github/license/EOEPCA/um-identity-api.svg?style=flat-square + [license-url]: https://github.com/EOEPCA/um-identity-api/blob/master/LICENSE -[build-shield]: https://www.travis-ci.com/EOEPCA/um-identity-apisvg?branch=master \ No newline at end of file + +[build-shield]: https://www.travis-ci.com/EOEPCA/um-identity-api.svg?branch=master \ No newline at end of file diff --git a/app/__init __.py b/app/__init __.py new file mode 100644 index 0000000..e69de29 diff --git a/app/configuration.py b/app/configuration.py new file mode 100644 index 0000000..1beff0e --- /dev/null +++ b/app/configuration.py @@ -0,0 +1,8 @@ +import os +from typing import Mapping + +from identityutils.configuration import load_configuration + +config: Mapping[str, str] = ( + load_configuration(os.path.join(os.path.dirname(__file__), "../config.ini")) +) \ No newline at end of file diff --git a/app/error_handling.py b/app/error_handling.py new file mode 100644 index 0000000..7465a29 --- /dev/null +++ b/app/error_handling.py @@ -0,0 +1,80 @@ +import json +import traceback +from typing import Sequence, Any + +from fastapi import Request, status, FastAPI +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from keycloak import KeycloakPostError, KeycloakGetError, KeycloakPutError, KeycloakDeleteError + + +def exception_handler(app: FastAPI) -> None: + @app.middleware("http") + async def keycloak_error_handling(request: Request, call_next): + try: + return await call_next(request) + except (KeycloakGetError, KeycloakPostError, KeycloakPutError, KeycloakDeleteError) as e: + print(traceback.format_exc()) + return JSONResponse(status_code=e.response_code, content=jsonable_encoder(json.loads(e.error_message))) + + @app.exception_handler(500) + async def internal_exception_handler(): + return JSONResponse(status_code=500, content=jsonable_encoder({"error": "Internal Server Error"})) + + @app.exception_handler(400) + async def bad_request_handler(): + return JSONResponse(status_code=400, content=jsonable_encoder({"error": "Bad request"})) + + @app.exception_handler(RequestValidationError) + async def request_validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse: + errors = jsonable_encoder(exc.errors()) + status_code = ( + status.HTTP_400_BAD_REQUEST + if __is_bad_request(errors) + else status.HTTP_422_UNPROCESSABLE_ENTITY + ) + return JSONResponse( + status_code=status_code, + content={"error": errors}, + ) + + +def __is_bad_request(errors: Sequence[Any]) -> bool: + """Check if the given error indicates a malformed request.""" + if not len(errors) == 1: + return False + + error_item = errors[0] + + if not isinstance(error_item, dict): + return False + + if not isinstance(error_item.get("loc"), list): + return False + + loc = error_item["loc"] + + if not 1 <= len(loc) <= 2: + return False + + loc_item1 = loc[0] + + if loc_item1 != "body": + return False + + loc_item2 = loc[1] if len(loc) > 1 else None + + if loc_item2: + return False + + if not isinstance(error_item.get("msg"), str): + return False + + msg = error_item["msg"] + + return ( + msg == "field required" + or msg == "value is not a valid dict" + or msg.startswith("Expecting value:") + ) \ No newline at end of file diff --git a/app/keycloak_client.py b/app/keycloak_client.py new file mode 100644 index 0000000..05585ee --- /dev/null +++ b/app/keycloak_client.py @@ -0,0 +1,29 @@ +from identityutils.keycloak_client import KeycloakClient +from keycloak import KeycloakConnectionError +from retry.api import retry_call +from urllib3.exceptions import NewConnectionError + +from app.configuration import config +from app.log import logger + + +def __create_keycloak_client(): + auth_server_url = config.get("Keycloak", "auth_server_url") + realm = config.get("Keycloak", "realm") + logger.info("Starting Keycloak client for: " + auth_server_url + "/realms/" + realm) + return KeycloakClient( + server_url=auth_server_url, + realm=realm, + username=config.get("Keycloak", "admin_username"), + password=config.get("Keycloak", "admin_password") + ) + + +keycloak = retry_call( + __create_keycloak_client, + exceptions=(KeycloakConnectionError, NewConnectionError), + delay=0.5, + backoff=1.2, + jitter=(1, 2), + logger=logger +) \ No newline at end of file diff --git a/app/log.py b/app/log.py new file mode 100644 index 0000000..a619015 --- /dev/null +++ b/app/log.py @@ -0,0 +1,30 @@ +import logging + +from app.configuration import config + + +def get_logging_level(): + level = config.get("App", "logging_level") + if not level: + level = 'info' + level = level.lower() + if level == 'critical' or level == 'critical': + return logging.CRITICAL + if level == 'error': + return logging.ERROR + if level == 'warning' or level == 'warn': + return logging.WARNING + if level == 'info': + return logging.INFO + if level == 'debug': + return logging.DEBUG + if level == 'notset': + return logging.NOTSET + + +logger = logging.getLogger('um-identity-api') +logging_level = get_logging_level() +logger.setLevel(logging_level) +fh = logging.FileHandler('um-identity-api.log') +fh.setLevel(logging_level) +logger.addHandler(fh) \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..84d8ff4 --- /dev/null +++ b/app/main.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import RedirectResponse + +from app.configuration import config +from app.error_handling import exception_handler +from app.routers import clients, health, policies, resources, clients_permissions, clients_resources, clients_policies + + +app = FastAPI( + title=config.get("Swagger", "swagger_title"), + description=config.get("Swagger", "swagger_description"), + version=config.get("Swagger", "swagger_version"), +) +app.add_middleware( + CORSMiddleware, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + allow_origins=["*"], +) +exception_handler(app) +app.include_router(clients.router) +app.include_router(clients_permissions.router) +app.include_router(clients_policies.router) +app.include_router(clients_resources.router) +app.include_router(policies.router) +app.include_router(resources.router) +app.include_router(health.router) + + +@app.get("/", include_in_schema=False) +async def docs_redirect(): + return RedirectResponse(url='/docs') + + +def main() -> None: + uvicorn.run("main:app", host="0.0.0.0") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/app/models/__init __.py b/app/models/__init __.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 0000000..9433421 --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, ConfigDict + + +class APIBaseModel(BaseModel): + model_config = ConfigDict(extra='forbid', use_enum_values=True) + + def model_dump(self, exclude_none=True, **kwargs): + return super().model_dump(exclude_none=exclude_none, **kwargs) \ No newline at end of file diff --git a/app/models/clients.py b/app/models/clients.py new file mode 100644 index 0000000..f10f06f --- /dev/null +++ b/app/models/clients.py @@ -0,0 +1,33 @@ +from typing import List, Optional + +from pydantic import Field + +from app.models.base import APIBaseModel +from app.models.resources import Resource + + +class Client(APIBaseModel): + clientId: str = Field(description="Client id") + name: Optional[str] = Field(None, description="Client name") + description: Optional[str] = Field(None, description="Client description") + secret: Optional[str] = Field(None, description="Client secret") + rootUrl: Optional[str] = Field(None, description="Client root URL") + adminUrl: Optional[str] = Field(None, description="Client admin URL") + baseUrl: Optional[str] = Field(None, description="Client base URL") + redirectUris: Optional[List[str]] = Field(['*'], description="Client Redirect URIs") + webOrigins: Optional[List[str]] = Field(['*'], description="Client Web origins") + protocol: Optional[str] = Field('openid-connect', description="Client protocol: openid-connect / SAML") + defaultRoles: Optional[List[str]] = Field(None, description="Client Default roles") + bearerOnly: Optional[bool] = Field(None, description="Enable/Disable Bearer only") + consentRequired: Optional[bool] = Field(None, description="Enable/Disable Consent required") + publicClient: Optional[bool] = Field(False, description="Disable/Enable authentication to the client") + authorizationServicesEnabled: Optional[bool] = Field(True, description="Enable Authorization Services") + serviceAccountsEnabled: Optional[bool] = Field(True, + description="Either or not to create a Service Account for the client") + standardFlowEnabled: Optional[bool] = Field(True, description="Enable/Disable Standard Flow") + implicitFlowEnabled: Optional[bool] = Field(None, description="Client name") + directAccessGrantsEnabled: Optional[bool] = Field(None, description="Enable/Disable Direct Access Grants Flow") + oauth2DeviceAuthorizationGrantEnabled: Optional[bool] = Field(None, + description="Enable/Disable OAuth2 Device Authorization Grant Flow") + directGrantsOnly: Optional[bool] = Field(None, description="Enable/Disable Direct Grants Flow") + resources: Optional[List[Resource]] = Field([], description="List of resources to be added to the client") \ No newline at end of file diff --git a/app/models/permissions.py b/app/models/permissions.py new file mode 100644 index 0000000..af8f78f --- /dev/null +++ b/app/models/permissions.py @@ -0,0 +1,211 @@ +from enum import Enum +from typing import List, Optional + +from pydantic import PositiveInt, Field + +from app.models.base import APIBaseModel +from app.models.policies import Logic, PolicyType + + +class DecisionStrategy(Enum): + AFFIRMATIVE = 'AFFIRMATIVE' + UNANIMOUS = 'UNANIMOUS' + CONSENSUS = 'CONSENSUS' + + +class ClientPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Client policy name") + clients: List[str] = Field(description="Client policy clients") + description: Optional[str] = Field(description="Client policy description") + + +class AggregatedPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Aggregated Policy name") + policies: List[str] = Field(description="Aggregated Policy policies") + description: Optional[str] = Field(description="Aggregated Policy description") + + +class ClientScope(APIBaseModel): + id: str = Field(description="Client scope id") + + +class ScopePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Scope policy name") + clientScopes: List[ClientScope] = Field(description="Scope policy client scopes") + description: Optional[str] = Field(description="Scope policy description") + + +class Group(APIBaseModel): + id: str = Field(description="Group id") + path: str = Field(description="Group path") + + +class GroupPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Group policy name") + groups: List[Group] = Field(description="Group policy groups") + groupsClaim: Optional[str] = Field(description="Group policy groups claim") + description: Optional[str] = Field(description="Group policy description") + + +class RegexPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Regex policy name") + pattern: str = Field(description="Regex policy regex pattern") + targetClaim: Optional[str] = Field(description="Regex policy target claim") + description: Optional[str] = Field(description="Regex policy description") + + +class Role(APIBaseModel): + id: str = Field(description="Role id") + + +class RolePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Role policy name") + roles: List[Role] = Field(description="Role policy roles") + description: Optional[str] = Field(description="Role policy description") + + +class RelativeTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Relative time policy name") + notAfter: str = Field(description="Relative time policy end date") + notBefore: str = Field(description="Relative time policy start date") + description: Optional[str] = Field(description="Relative time policy description") + + +class DayMonthTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Day month time policy name") + dayMonth: PositiveInt = Field(description="Day month time policy day month start") + dayMonthEnd: PositiveInt = Field(description="Day month time policy day month end") + description: Optional[str] = Field(description="Day month time policy description") + + +class MonthTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Month time policy name") + month: PositiveInt = Field(description="Month time policy month start") + monthEnd: PositiveInt = Field(description="Month time policy month end") + description: Optional[str] = Field(description="Month time policy description") + + +class YearTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Year time policy name") + year: PositiveInt = Field(description="Year time policy year start") + yearEnd: PositiveInt = Field(description="Year time policy year end") + description: Optional[str] = Field(description="Year time policy description") + + +class HourTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Hour time policy name") + hour: PositiveInt = Field(description="Hour time policy hour start") + hourEnd: PositiveInt = Field(description="Hour time policy hour end") + description: Optional[str] = Field(description="Hour time policy description") + + +class MinuteTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Minute time policy name") + minute: PositiveInt = Field(description="Minute time policy minute start") + minuteEnd: PositiveInt = Field(description="Minute time policy minute end") + description: Optional[str] = Field(description="Minute time policy description") + + +class UserPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="User policy name") + users: List[str] = Field(description="User policy users list") + + +class ModifyClientPermission(ClientPermission): + type: PolicyType = Field(PolicyType.CLIENT.value, description="Policy type") + + +class ModifyAggregatedPermission(AggregatedPermission): + type: PolicyType = Field(PolicyType.AGGREGATE.value, description="Policy type") + + +class ModifyScopePermission(ScopePermission): + type: PolicyType = Field(PolicyType.SCOPE.value, description="Policy type") + + +class ModifyGroupPermission(GroupPermission): + type: PolicyType = Field(PolicyType.GROUP.value, description="Policy type") + + +class ModifyRegexPermission(RegexPermission): + type: PolicyType = Field(PolicyType.REGEX.value, description="Policy type") + + +class ModifyRolePermission(RolePermission): + type: PolicyType = Field(PolicyType.ROLE.value, description="Policy type") + + +class ModifyRelativeTimePermission(RelativeTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyDayMonthTimePermission(DayMonthTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyMonthTimePermission(MonthTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyYearTimePermission(YearTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyHourTimePermission(HourTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyMinuteTimePermission(MinuteTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyUserPermission(UserPermission): + type: PolicyType = Field(PolicyType.USER.value, description="Policy type") + + +class ResourceBasedPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Resource based permission name") + resources: List[str] = Field(description="Resource based permission resources") + policies: List[str] = Field(description="Resource based permission policies") \ No newline at end of file diff --git a/app/models/policies.py b/app/models/policies.py new file mode 100644 index 0000000..d46adf1 --- /dev/null +++ b/app/models/policies.py @@ -0,0 +1,40 @@ +from enum import Enum +from typing import List, Optional + +from pydantic import PositiveInt, Field + +from app.models.base import APIBaseModel + + +class PolicyType(Enum): + ROLE = 'role' + USER = 'user' + CLIENT = 'client' + AGGREGATE = 'aggregate' + SCOPE = 'scope' + GROUP = 'group' + REGEX = 'regex' + TIME = 'time' + + +class Logic(Enum): + POSITIVE = 'POSITIVE' + NEGATIVE = 'NEGATIVE' + + +class UserPolicy(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + users: List[str] = Field(None, description="List of usernames") + + +class RolePolicy(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + roles: List[str] = Field(None, description="List of roles") + + +class SearchPolicies(APIBaseModel): + resource: str = '' + name: str = '' + uri: str = '' + first: PositiveInt = 0 + maximum: int = -1 \ No newline at end of file diff --git a/app/models/resources.py b/app/models/resources.py new file mode 100644 index 0000000..75c7340 --- /dev/null +++ b/app/models/resources.py @@ -0,0 +1,23 @@ +from typing import List, Optional, Any + +from pydantic import Field + +from app.models.base import APIBaseModel +from app.models.permissions import DecisionStrategy, UserPermission, RolePermission + + +class ResourcePermission(APIBaseModel): + user: List[str] | List[UserPermission] = Field([], description="User based permission") + role: List[str] | List[RolePermission] = Field([], description="Role based permission") + authenticated: bool = Field(False, description="Authenticated only permission") + + +class Resource(APIBaseModel): + name: str = Field(description="Resource name") + uris: List[str] = Field(description="Resource URIs") + attributes: Optional[Any] = Field({}, description="Resource attributes") + scopes: Optional[List[str]] = Field(["access"], description="Resource scopes") + ownerManagedAccess: Optional[bool] = Field(False, description="Enable/Disable management by the resource owner") + permissions: Optional[ResourcePermission] = Field(None, description="Resource permissions") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") \ No newline at end of file diff --git a/app/routers/__init __.py b/app/routers/__init __.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/clients.py b/app/routers/clients.py new file mode 100644 index 0000000..ffc8ec8 --- /dev/null +++ b/app/routers/clients.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter + +from app.keycloak_client import keycloak +from app.models.clients import Client +from app.routers.clients_resources import register_resources + +router = APIRouter( + prefix="/clients", + tags=["Clients"] +) + + +@router.post("") +def create_client(client: Client): + resources = client.resources + client_dict = client.model_dump() + del client_dict['resources'] + response_client = keycloak.create_client(client_dict) + response = { + "client": response_client + } + if resources: + response_resources = register_resources(client.clientId, resources) + response["resources"] = response_resources + return response \ No newline at end of file diff --git a/app/routers/clients_permissions.py b/app/routers/clients_permissions.py new file mode 100644 index 0000000..ee26ae4 --- /dev/null +++ b/app/routers/clients_permissions.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter + +from app.keycloak_client import keycloak +from app.models.permissions import ResourceBasedPermission + +router = APIRouter( + prefix="/{client_id}/permissions", + tags=["Clients Permissions"], +) + + +@router.get("") +def get_client_authz_permissions(client_id: str): + return keycloak.get_client_authz_permissions(client_id) + + +@router.get("/management") +def get_client_management_permissions(client_id: str): + return keycloak.get_client_management_permissions(client_id) + + +@router.get("/resources") +def get_client_resource_permissions(client_id: str): + return keycloak.get_client_resource_permissions(client_id) + + +@router.post("/resources") +def create_client_authz_resource_based_permission(client_id: str, resource_based_permission: ResourceBasedPermission): + resource_based_permission['type'] = 'resource' + return keycloak.create_client_authz_resource_based_permission(client_id, resource_based_permission) \ No newline at end of file diff --git a/app/routers/clients_policies.py b/app/routers/clients_policies.py new file mode 100644 index 0000000..275a06f --- /dev/null +++ b/app/routers/clients_policies.py @@ -0,0 +1,85 @@ +from fastapi import APIRouter + +from app.keycloak_client import keycloak +from app.models.permissions import ClientPermission, AggregatedPermission, \ + ScopePermission, GroupPermission, RegexPermission, RolePermission, RelativeTimePermission, YearTimePermission, \ + HourTimePermission, \ + DayMonthTimePermission, MonthTimePermission, MinuteTimePermission, UserPermission, ModifyClientPermission, \ + ModifyRegexPermission, \ + ModifyMonthTimePermission, ModifyUserPermission, ModifyAggregatedPermission, ModifyRolePermission, \ + ModifyYearTimePermission, \ + ModifyRelativeTimePermission, ModifyScopePermission, ModifyHourTimePermission, ModifyDayMonthTimePermission, \ + ModifyMinuteTimePermission + +router = APIRouter( + prefix="/{client_id}/policies", + tags=["Clients Policies"], +) + + +@router.get("") +def get_client_authz_policies(client_id: str): + return keycloak.get_client_authz_policies(client_id) + + +@router.post("/client") +def create_client_policy(client_id: str, client_policy: ClientPermission): + client_policy["type"] = "client" + return keycloak.register_client_policy(client_policy, client_id) + + +@router.post("/aggregated") +def create_aggregated_policy(client_id: str, aggregated_policy: AggregatedPermission): + aggregated_policy["type"] = "aggregated" + return keycloak.register_aggregated_policy(aggregated_policy, client_id) + + +@router.post("/scope") +def create_client_scope_policy(client_id: str, scope_policy: ScopePermission): + scope_policy["type"] = "scope" + return keycloak.register_client_scope_policy(scope_policy, client_id) + + +@router.post("/group") +def create_group_policy(client_id: str, group_policy: GroupPermission): + group_policy["type"] = "group" + return keycloak.register_group_policy(group_policy, client_id) + + +@router.post("/regex") +def create_regex_policy(client_id: str, regex_policy: RegexPermission): + regex_policy["type"] = "regex" + return keycloak.register_regex_policy(regex_policy, client_id) + + +@router.post("/role") +def create_role_policy(client_id: str, role_policy: RolePermission): + role_policy["type"] = "role" + return keycloak.register_role_policy(role_policy, client_id) + + +@router.post("/time") +def create_time_policy(client_id: str, + time_policy: RelativeTimePermission | DayMonthTimePermission | MonthTimePermission | + YearTimePermission | HourTimePermission | MinuteTimePermission): + time_policy["type"] = "time" + return keycloak.register_time_policy(time_policy, client_id) + + +@router.post("/user") +def create_user_policy(client_id: str, user_policy: UserPermission): + return keycloak.register_user_policy(user_policy, client_id) + + +@router.put("/{policy_id}") +def update_policy(client_id: str, policy_id: str, + policy: ModifyClientPermission | ModifyAggregatedPermission | ModifyScopePermission | + ModifyRegexPermission | ModifyRolePermission | ModifyRelativeTimePermission | ModifyDayMonthTimePermission | + ModifyMonthTimePermission | ModifyYearTimePermission | ModifyHourTimePermission | ModifyMinuteTimePermission | + ModifyUserPermission): + return keycloak.update_policy(client_id, policy_id, policy.model_dump()) + + +@router.delete("/{policy_id}") +def delete_policy(client_id: str, policy_id: str): + return keycloak.delete_policy(policy_id, client_id) \ No newline at end of file diff --git a/app/routers/clients_resources.py b/app/routers/clients_resources.py new file mode 100644 index 0000000..618c839 --- /dev/null +++ b/app/routers/clients_resources.py @@ -0,0 +1,83 @@ +from typing import List + +from fastapi import APIRouter + +from app.keycloak_client import keycloak +from app.models.policies import PolicyType +from app.models.resources import Resource + +router = APIRouter( + prefix="/{client_id}/resources", + tags=["Clients Resources"], +) + + +@router.post("") +def register_resources(client_id: str, resources: List[Resource]): + response_list = [] + for resource in resources: + resource_name = resource.name.replace(" ", "_") + res = { + "name": resource_name, + "uris": resource.uris, + "scopes": resource.scopes, + } + response_resource = keycloak.register_resource(res, client_id) + response_list.append(response_resource) + permissions = resource.permissions + policy_list = [] + if permissions.role: + policy = { + "name": f'{resource_name}_role_policy', + "roles": [{"id": p} for p in permissions.role] + } + policy_response = keycloak.register_role_policy(policy, client_id) + policy_list.append(policy_response["name"]) + if permissions.user: + policy = { + "name": f'{resource_name}_user_policy', + "users": permissions.user + } + policy_response = keycloak.register_user_policy(policy, client_id) + policy_list.append(policy_response["name"]) + permission_payload = { + "type": "resource", + "name": f'{resource_name}_permission', + "decisionStrategy": resource.decisionStrategy, + "resources": [ + resource_name + ], + "policies": policy_list + } + keycloak.create_client_authz_resource_based_permission(client_id, permission_payload) + return response_list + + +@router.delete("/{resource_name}/all") +def delete_resource_and_policies(client_id: str, resource_name: str): + # delete policies + client_policies = keycloak.get_client_authz_policies(client_id) + for policy in client_policies: + for policy_type in [e.value for e in PolicyType]: + if policy['name'] == f'{resource_name}_{policy_type}_policy': + keycloak.delete_policy(policy['id'], client_id) + # delete permissions + permissions = keycloak.get_client_resource_permissions(client_id) + for permission in permissions: + if permission['name'] == f'{resource_name}_permission': + keycloak.delete_resource_permissions(client_id, permission['id']) + # delete resources + resources = keycloak.get_resources(client_id) + for resource in resources: + if resource['name'] == resource_name: + return keycloak.delete_resource(resource['_id'], client_id) + + +@router.put("/{resource_id}") +def update_resource(client_id: str, resource_id: str, resource: Resource): + return keycloak.update_resource(resource_id, resource, client_id) + + +@router.delete("/{resource_id}") +def delete_resource(client_id: str, resource_id: str): + return keycloak.delete_resource(resource_id, client_id) \ No newline at end of file diff --git a/app/routers/health.py b/app/routers/health.py new file mode 100644 index 0000000..d7912f6 --- /dev/null +++ b/app/routers/health.py @@ -0,0 +1,38 @@ +from fastapi import status, APIRouter + +from app.models.base import APIBaseModel + +router = APIRouter( + prefix="/health", + tags=["Health checks"] +) + + +class HealthCheck(APIBaseModel): + """Response model to validate and return when performing a health check.""" + status: str = "OK" + + +@router.get( + "/liveness", + summary="Perform a liveness Health Check", + response_description="Return HTTP Status Code 200 (OK)", + status_code=status.HTTP_200_OK +) +@router.get( + "/readiness", + summary="Perform a readiness Health Check", + response_description="Return HTTP Status Code 200 (OK)", + status_code=status.HTTP_200_OK +) +def get_health() -> HealthCheck: + """ + ## Perform a Health Check + Endpoint to perform a healthcheck on. This endpoint can primarily be used Docker + to ensure a robust container orchestration and management is in place. Other + services which rely on proper functioning of the API service will not deploy if this + endpoint returns any other HTTP status code except 200 (OK). + Returns: + HealthCheck: Returns a JSON response with the health status + """ + return HealthCheck(status="OK") \ No newline at end of file diff --git a/app/routers/policies.py b/app/routers/policies.py new file mode 100644 index 0000000..f8c7f57 --- /dev/null +++ b/app/routers/policies.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter + +from app.keycloak_client import keycloak +from app.models.policies import SearchPolicies + +router = APIRouter( + prefix="/policies", + tags=["Policies"], +) + + +@router.get("") +def search_policies(search_params: SearchPolicies): + return keycloak.get_policies( + search_params.resource, + search_params.name, + search_params.scope, + search_params.first, + search_params.maximum + ) \ No newline at end of file diff --git a/app/routers/resources.py b/app/routers/resources.py new file mode 100644 index 0000000..e20bd81 --- /dev/null +++ b/app/routers/resources.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +from app.keycloak_client import keycloak + +router = APIRouter( + prefix="/resources", + tags=["Resouces"], +) + + +@router.get("") +def get_resources(): + return keycloak.get_resources() + + +@router.get("/resources/{resource_id}") +def get_resource(resource_id: str): + return keycloak.get_resource(resource_id) \ No newline at end of file diff --git a/conf/config.ini b/conf/config.ini deleted file mode 100644 index cbb9eea..0000000 --- a/conf/config.ini +++ /dev/null @@ -1,9 +0,0 @@ -[Keycloak] -auth_server_url = http://localhost:8080/ -admin_username = admin -admin_password = CHANGE ME -realm = master -[Swagger] -swagger_url = /swagger-ui -swagger_api_url = /swagger-ui-api -swagger_app_name = Identity API \ No newline at end of file diff --git a/conf/logging.yaml b/conf/logging.yaml deleted file mode 100644 index 57a2fe9..0000000 --- a/conf/logging.yaml +++ /dev/null @@ -1,29 +0,0 @@ -version: 1 -disable_existing_loggers: true - -formatters: - verbose: - format: '%(asctime)s:%(levelname)s:%(message)s' - datefmt: '%Y-%m-%dT%H:%M:%S%z' - -handlers: - console: - class: logging.StreamHandler - level: INFO - formatter: verbose - stream: ext://sys.stdout - - log: - class: logging.handlers.RotatingFileHandler - filename: ../../logs/identity-api.log - formatter: verbose - level: DEBUG - maxBytes: 1073741824 ## 1 GB log file size before rotation - backupCount: 10 ## Saves 10 most recent log files - -loggers: - IDENTITY_API: - level: DEBUG - handlers: [ console, log ] - qualname: IDENTITY_API - propagate: false diff --git a/conf/swagger.json b/conf/swagger.json deleted file mode 100644 index 8a3e183..0000000 --- a/conf/swagger.json +++ /dev/null @@ -1,255 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "Policy Enforcement Point Interfaces", - "description": "This OpenAPI Document describes the endpoints exposed by Policy Enforcement Point Building Block deployments.

Using this API will allow to register resources that can be protected using both the Login Service and the Policy Decision Point and access them through the Policy Enforcement Endpoint.

As an example this documentation uses \"proxy\" as the configured base URL for Policy Enforcement, but this can be manipulated through configuration parameters." - }, - "tags": [ - { - "name": "Resources", - "description": "Operations to create, modify or delete resources" - } - ], - "paths": { - "/resources": { - "parameters": [ - { - "in": "header", - "name": "Authorization", - "description": "JWT or Bearer Token", - "schema": { - "type": "string" - } - } - ], - "get": { - "tags": [ - "Resources" - ], - "summary": "List all owned resources", - "description": "This operation lists all resources filtered by ownership ID. Ownership ID is extracted from the OpenID Connect Token", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/resource" - } - } - } - } - } - } - }, - "post": { - "tags": [ - "Resources" - ], - "summary": "Creates a new Resource reference in the Platform", - "description": "This operation generates a new resource reference object that can be protected. Ownership ID is set to the unique ID of the End-User", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/new_resource" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/resource" - } - } - } - }, - "401": { - "description": "UNAUTHORIZED" - }, - "404": { - "description": "NOT FOUND" - } - } - } - }, - "/resources/{resource_id}": { - "parameters": [ - { - "in": "path", - "name": "resource_id", - "description": "Unique Resource ID", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "header", - "name": "Authorization", - "description": "JWT or Bearer Token", - "schema": { - "type": "string" - } - } - ], - "get": { - "tags": [ - "Resources" - ], - "summary": "Retrieve a specific owned resource", - "description": "This operation retrieves information about an owned resource.", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/resource" - } - } - } - }, - "404": { - "description": "NOT FOUND" - } - } - }, - "put": { - "tags": [ - "Resources" - ], - "summary": "Updates an existing Resource reference in the Platform", - "description": "This operation updates an existing 'owned' resource reference. ", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/resource" - } - } - } - }, - "responses": { - "200": { - "description": "OK" - }, - "401": { - "description": "UNAUTHORIZED" - }, - "404": { - "description": "NOT FOUND" - } - } - }, - "delete": { - "tags": [ - "Resources" - ], - "summary": "Deletes an owned Resource Reference from the Platform", - "description": "This operation removes an existing Resource reference owned by the user.", - "responses": { - "200": { - "description": "OK" - }, - "401": { - "description": "UNAUTHORIZED" - }, - "404": { - "description": "NOT FOUND" - } - } - } - } - }, - "components": { - "responses": { - "UMAUnauthorized": { - "description": "Unauthorized access request.", - "headers": { - "WWW-Authenticate": { - "schema": { - "type": "string" - }, - "description": "'UMA_realm=\"example\",as_uri=\"https://as.example.com\",ticket=\"016f84e8-f9b9-11e0-bd6f-0021cc6004de\"'" - } - } - } - }, - "schemas": { - "new_resource": { - "type": "object", - "properties": { - "name": { - "description": "Human readable name for the resource", - "type": "string", - "example": "My Beautiful Resource" - }, - "icon_uri": { - "description": "Protected uri of the resource.\n", - "type": "string", - "example": "/wps3/processes/" - }, - "scopes": { - "description": "List of scopes associated with the resource", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "public", - "myOtherAttr" - ] - } - } - }, - "resource": { - "type": "object", - "properties": { - "ownership_id": { - "description": "UUID of the Owner End-User", - "type": "string", - "format": "uuid", - "example": "d290f1ee-6c54-4b01-90e6-288571188183" - }, - "id": { - "description": "UUID of the resource", - "type": "string", - "format": "uuid", - "example": "d290f1ee-6c54-4b01-90e6-d701748f0851" - }, - "name": { - "description": "Human readable name for the resource", - "type": "string", - "example": "My Beautiful Resource" - }, - "icon_uri": { - "description": "Protected uri of the resource.\n", - "type": "string", - "example": "/wps3/processes/" - }, - "scopes": { - "description": "List of scopes associated with the resource", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "public", - "myOtherAttr" - ] - } - } - } - } - } -} diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..09469c7 --- /dev/null +++ b/config.ini @@ -0,0 +1,11 @@ +[App] +logging_level = info +[Keycloak] +auth_server_url = http://localhost +admin_username = admin +admin_password = admin +realm = master +[Swagger] +swagger_title = Identity API Documentation +swagger_description = API endpoints +swagger_version = v1.0.0 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7d406f7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +version: "3.5" +services: + identity-api: + build: + context: . + dockerfile: Dockerfile + image: um-identity-api:local + container_name: um-identity-api + environment: + - AUTH_SERVER_URL=http://keycloak:8080 + ports: + - '8080:8080' + keycloak: + image: quay.io/keycloak/keycloak:22.0.5 + container_name: keycloak + ports: + - "80:8080" + environment: + - KEYCLOAK_LOGLEVEL=DEBUG + - WILDFLY_LOGLEVEL=DEBUG + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin + - KC_PROXY=edge + - KC_LOGLEVEL=WARN + - PROXY_ADDRESS_FORWARDING=true + - KC_HOSTNAME_STRICT=false + - KC_DB=postgres + - KC_DB_URL_HOST=postgres + - KC_DB_PASSWORD=123456 + - KC_DB_USERNAME=keycloak + - KC_DB_URL_PORT=5432 + entrypoint: /opt/keycloak/bin/kc.sh start + restart: on-failure + postgres: + image: postgres:16.0 + container_name: postgres + volumes: + - ./postgres/data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=keycloak + - POSTGRES_USER=keycloak + - POSTGRES_PASSWORD=123456 + - PGPASSWORD=123 + - PGDATA=/var/lib/postgresql/data/keycloak + restart: on-failure \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 1ad7ea0..0000000 --- a/docs/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ - -output/ -repos/ diff --git a/docs/.travis.yml b/docs/.travis.yml deleted file mode 100644 index 1da5117..0000000 --- a/docs/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -jobs: - include: - - stage: generate docs - if: branch = master AND NOT type IN (pull_request) - - # Assume that 'docker' service is stated at the Travis global level. It seems that re-stating it here confuses Travis. - # services: - # - docker - - before_install: - - docker pull asciidoctor/docker-asciidoctor - - script: - - ./docs/bin/generate-docs.sh - - after_error: - - docker logs asciidoc-to-html - - docker logs asciidoc-to-pdf - - after_failure: - - docker logs asciidoc-to-html - - docker logs asciidoc-to-pdf - - after_success: - - ./docs/bin/publish-docs.sh diff --git a/docs/01.introduction/00.introduction.adoc b/docs/01.introduction/00.introduction.adoc deleted file mode 100644 index 98a7802..0000000 --- a/docs/01.introduction/00.introduction.adoc +++ /dev/null @@ -1,20 +0,0 @@ - -= Introduction - -== Purpose and Scope - -This document presents the {component-name} Design for the Common Architecture. - -== Structure of the Document - -Section 2 - <>:: -Provides an over of the {component-name} component, within the context of the wider Common Architecture design. - -Section 3 - <>:: -Provides the design of the {component-name} component. - -include::03.reference-docs.adoc[leveloffset=+1] - -include::04.terminology.adoc[leveloffset=+1] - -include::05.glossary.adoc[leveloffset=+1] diff --git a/docs/01.introduction/03.reference-docs.adoc b/docs/01.introduction/03.reference-docs.adoc deleted file mode 100644 index eae80fb..0000000 --- a/docs/01.introduction/03.reference-docs.adoc +++ /dev/null @@ -1,162 +0,0 @@ - -= Reference Documents - -The following is a list of Reference Documents with a direct bearing on the content of this document. - -[cols="2,7a,2a"] -|=== -| Reference | Document Details | Version - -| [[EOEPCA-UC]][EOEPCA-UC] -| EOEPCA - Use Case Analysis + -EOEPCA.TN.005 + -https://eoepca.github.io/use-case-analysis -| Issue 1.0, + -02/08/2019 - -| [[EP-FM]][EP-FM] -| Exploitation Platform - Functional Model, + -ESA-EOPSDP-TN-17-050 -| Issue 1.0, + -30/11/2017 - -| [[TEP-OA]][TEP-OA] -| Thematic Exploitation Platform Open Architecture, + -EMSS-EOPS-TN-17-002 -| Issue 1, + -12/12/2017 - -| [[WPS-T]][WPS-T] -| OGC Testbed-14: WPS-T Engineering Report, + -OGC 18-036r1, + -http://docs.opengeospatial.org/per/18-036r1.html -| 18-036r1, + -07/02/2019 - -| [[WPS-REST-JSON]][WPS-REST-JSON] -| OGC WPS 2.0 REST/JSON Binding Extension, Draft, + -OGC 18-062, + -https://raw.githubusercontent.com/opengeospatial/wps-rest-binding/develop/docs/18-062.pdf -| 1.0-draft - -| [[CWL]][CWL] -| Common Workflow Language Specifications, + -https://www.commonwl.org/v1.0/ -| v1.0.2 - -| [[TB13-AP]][TB13-AP] -| OGC Testbed-13, EP Application Package Engineering Report, + -OGC 17-023, + -http://docs.opengeospatial.org/per/17-023.html -| 17-023, + -30/01/2018 - -| [[TB13-ADES]][TB13-ADES] -| OGC Testbed-13, Application Deployment and Execution Service Engineering Report, + -OGC 17-024, + -http://docs.opengeospatial.org/per/17-024.html -| 17-024, + -11/01/2018 - -| [[TB14-AP]][TB14-AP] -| OGC Testbed-14, Application Package Engineering Report, + -OGC 18-049r1, + -http://docs.opengeospatial.org/per/18-049r1.html -| 18-049r1, + -07/02/2019 - -| [[TB14-ADES]][TB14-ADES] -| OGC Testbed-14, ADES & EMS Results and Best Practices Engineering Report, + -OGC 18-050r1, http://docs.opengeospatial.org/per/18-050r1.html -| 18-050r1, + -08/02/2019 - -| [[OS-GEO-TIME]][OS-GEO-TIME] -| OpenSearch GEO: OpenSearch Geo and Time Extensions, + -OGC 10-032r8, + -http://www.opengeospatial.org/standards/opensearchgeo -| 10-032r8, + -14/04/2014 - -| [[OS-EO]][OS-EO] -| OpenSearch EO: OGC OpenSearch Extension for Earth Observation, + -OGC 13-026r9, + -http://docs.opengeospatial.org/is/13-026r8/13-026r8.html -| 13-026r9, + -16/12/2016 - -| [[GEOJSON-LD]][GEOJSON-LD] -| OGC EO Dataset Metadata GeoJSON(-LD) Encoding Standard, + -OGC 17-003r1/17-084 -| 17-003r1/17-084 - -| [[GEOJSON-LD-RESP]][GEOJSON-LD-RESP] -| OGC OpenSearch-EO GeoJSON(-LD) Response Encoding Standard, + -OGC 17-047 -| 17-047 - -| [[PCI-DSS]][PCI-DSS] -| The Payment Card Industry Data Security Standard, + -https://www.pcisecuritystandards.org/document_library?category=pcidss&document=pci_dss -| v3.2.1 - -| [[CEOS-OS-BP]][CEOS-OS-BP] -| CEOS OpenSearch Best Practise, + -http://ceos.org/ourwork/workinggroups/wgiss/access/opensearch/ -| v1.2, + -13/06/2017 - -| [[OIDC]][OIDC] -| OpenID Connect Core 1.0, + -https://openid.net/specs/openid-connect-core-1_0.html -| v1.0, + -08/11/2014 - -| [[OGC-CSW]][OGC-CSW] -| OGC Catalogue Services 3.0 Specification - HTTP Protocol Binding (Catalogue Services for the Web), + -OGC 12-176r7, + -http://docs.opengeospatial.org/is/12-176r7/12-176r7.html -| v3.0, + -10/06/2016 - -| [[OGC-WMS]][OGC-WMS] -| OGC Web Map Server Implementation Specification, + -OGC 06-042, + -http://portal.opengeospatial.org/files/?artifact_id=14416 -| v1.3.0, + -05/03/2006 - -| [[OGC-WMTS]][OGC-WMTS] -| OGC Web Map Tile Service Implementation Standard, + -OGC 07-057r7, + -http://portal.opengeospatial.org/files/?artifact_id=35326 -| v1.0.0, + -06/04/2010 - -| [[OGC-WFS]][OGC-WFS] -| OGC Web Feature Service 2.0 Interface Standard – With Corrigendum, + -OGC 09-025r2, + -http://docs.opengeospatial.org/is/09-025r2/09-025r2.html -| v2.0.2, + -10/07/2014 - -| [[OGC-WCS]][OGC-WCS] -| OGC Web Coverage Service (WCS) 2.1 Interface Standard - Core, + -OGC 17-089r1, + -http://docs.opengeospatial.org/is/17-089r1/17-089r1.html -| v2.1, + -16/08/2018 - -| [[OGC-WCPS]][OGC-WCPS] -| Web Coverage Processing Service (WCPS) Language Interface Standard, + -OGC 08-068r2, + -http://portal.opengeospatial.org/files/?artifact_id=32319 -| v1.0.0, + -25/03/2009 - -| [[AWS-S3]][AWS-S3] -| Amazon Simple Storage Service REST API, + -https://docs.aws.amazon.com/AmazonS3/latest/API -| API Version 2006-03-01 - -|=== diff --git a/docs/01.introduction/04.terminology.adoc b/docs/01.introduction/04.terminology.adoc deleted file mode 100644 index 7d2d906..0000000 --- a/docs/01.introduction/04.terminology.adoc +++ /dev/null @@ -1,187 +0,0 @@ - -= Terminology - -The following terms are used in the Master System Design. - -[cols="1,3"] -|=== -| Term | Meaning - -| Admin -| User with administrative capability on the EP - -| Algorithm -| A self-contained set of operations to be performed, typically to achieve a desired data manipulation. The algorithm must be implemented (codified) for deployment and execution on the platform. - -| Analysis Result -| The _Products_ produced as output of an _Interactive Application_ analysis session. - -| Analytics -| A set of activities aimed to discover, interpret and communicate meaningful patters within the data. Analytics considered here are performed manually (or in a semi-automatic way) on-line with the aid of _Interactive Applications_. - -| Application Artefact -| The 'software' component that provides the execution unit of the _Application Package_. - -| Application Deployment and Execution Service (ADES) -| WPS-T (REST/JSON) service that incorporates the Docker execution engine, and is responsible for the execution of the processing service (as a WPS request) within the ‘target’ Exploitation Platform. - -| Application Descriptor -| A file that provides the metadata part of the _Application Package_. Provides all the metadata required to accommodate the processor within the WPS service and make it available for execution. - -| Application Package -| A platform independent and self-contained representation of a software item, providing executable, metadata and dependencies such that it can be deployed to and executed within an Exploitation Platform. Comprises the _Application Descriptor_ and the _Application Artefact_. - -| Bulk Processing -| Execution of a _Processing Service_ on large amounts of data specified by AOI and TOI. - -| Code -| The codification of an algorithm performed with a given programming language - compiled to Software or directly executed (interpretted) within the platform. - -| Compute Platform -| The Platform on which execution occurs (this may differ from the Host or Home platform where federated processing is happening) - -| Consumer -| User accessing existing services/products within the EP. Consumers may be scientific/research or commercial, and may or may not be experts of the domain - -| Data Access Library -| An abstraction of the interface to the data layer of the resource tier. The library provides bindings for common languages (including python, Javascript) and presents a common object model to the code. - -| Development -| The act of building new products/services/applications to be exposed within the platform and made available for users to conduct exploitation activities. Development may be performed inside or outside of the platform. If performed outside, an integration activity will be required to accommodate the developed service so that it is exposed within the platform. - -| Discovery -| User finds products/services of interest to them based upon search criteria. - -| Execution -| The act to start a _Processing Service_ or an _Interactive Application_. - -| Execution Management Service (EMS) -| The EMS is responsible for the orchestration of workflows, including the possibility of steps running on other (remote) platforms, and the on-demand deployment of processors to local/remote ADES as required. - -| Expert -| User developing and integrating added-value to the EP (Scientific Researcher or Service Developer) - -| Exploitation Tier -| The Exploitation Tier represents the end-users who exploit the services of the platform to perform analysis, or using high-level applications built-in on top of the platform’s services - -| External Application -| An application or script that is developed and executed outside of the Exploitation Platform, but is able to use the data/services of the EP via a programmatic interface (API). - -| Guest -| An unregistered User or an unauthenticated Consumer with limited access to the EP's services - -| Home Platform -| The Platform on which a User is based or from which an action was initiated by a User - -| Host Platform -| The Platform through which a Resource has been published - -| Identity Provider (IdP) -| The source for validating user identity in a federated identity system, (user authentication as a service). - -| Interactive Application -| A stand-alone application provided within the exploitation platform for on-line hosted processing. Provides an interactive interface through which the user is able to conduct their analysis of the data, producing _Analysis Results_ as output. Interactive Applications include at least the following types: console application, web application (rich browser interface), remote desktop to a hosted VM. - -| Interactive Console Application -| A simple _Interactive Application_ for analysis in which a console interface to a platform-hosted terminal is provided to the user. The console interface can be provided through the user's browser session or through a remote SSH connection. - -| Interactive Remote Desktop -| An Interactive Application for analysis provided as a remote desktop session to an OS-session (or directly to a 'native' application) on the exploitation platform. The user will have access to a number of applications within the hosted OS. The remote desktop session is provided through the user’s web browser. - -| Interactive Web Application -| An Interactive Application for analysis provided as a rich user interface through the user's web browser. - -| Key-Value Pair -| A key-value pair (KVP) is an abstract data type that includes a group of key identifiers and a set of associated values. Key-value pairs are frequently used in lookup tables, hash tables and configuration files. - -| Kubernetes (K8s) -| Container orchestration system for automating application deployment, scaling and management. - -| Login Service -| An encapsulation of Authenticated Login provision within the Exploitation Platform context. The Login Service is an OpenID Connect Provider that is used purely for authentication. It acts as a Relying Party in flows with external IdPs to obtain access to the user's identity. - -| EO Network of Resources -| The coordinated collection of European EO resources (platforms, data sources, etc.). - -| Object Store -| A computer data storage architecture that manages data as objects. Each object typically includes the data itself, a variable amount of metadata, and a globally unique identifier. - -| On-demand Processing Service -| A _Processing Service_ whose execution is initiated directly by the user on an ad-hoc basis. - -| Platform (EP) -| An on-line collection of products, services and tools for exploitation of EO data - -| Platform Tier -| The Platform Tier represents the Exploitation Platform and the services it offers to end-users - -| Processing -| A set of pre-defined activities that interact to achieve a result. For the exploitation platform, comprises on-line processing to derive data products from input data, conducted by a hosted processing service execution. - -| Processing Result -| The _Products_ produced as output of a _Processing Service_ execution. - -| Processing Service -| A non-interactive data processing that has a well-defined set of input data types, input parameterisation, producing _Processing Results_ with a well-defined output data type. - -| Products -| EO data (commercial and non-commercial) and Value-added products and made available through the EP. _It is assumed that the Hosting Environment for the EP makes available an existing supply of EO Data_ - -| Resource -| A entity, such as a Product, Processing Service or Interactive Application, which is of interest to a user, is indexed in a catalogue and can be returned as a single meaningful search result - -| Resource Tier -| The Resource Tier represents the hosting infrastructure and provides the EO data, storage and compute upon which the exploitation platform is deployed - -| Reusable Research Object -| An encapsulation of some research/analysis that describes all aspects required to reproduce the analysis, including data used, processing performed etc. - -| Scientific Researcher -| Expert user with the objective to perform scientific research. Having minimal IT knowledge with no desire to acquire it, they want the effort for the translation of their algorithm into a service/product to be minimised by the platform. - -| Service Developer -| Expert user with the objective to provide a performing, stable and reliable service/product. Having deeper IT knowledge or a willingness to acquire it, they require deeper access to the platform IT functionalities for optimisation of their algorithm. - -| Software -| The compilation of code into a binary program to be executed within the platform on-line computing environment. - -| Systematic Processing Service -| A _Processing Service_ whose execution is initiated automatically (on behalf of a user), either according to a schedule (routine) or triggered by an event (e.g. arrival of new data). - -| Terms & Conditions (T&Cs) -| The obligations that the user agrees to abide by in regard of usage of products/services of the platform. T&Cs are set by the provider of each product/service. - -| Transactional Web Processing Service (WPS-T) -| Transactional extension to WPS that allows adhoc deployment / undeployment of user-provided processors. - -| User -| An individual using the EP, of any type (Admin/Consumer/Expert/Guest) - -| Value-added products -| Products generated from processing services of the EP (or external processing) and made available through the EP. This includes products uploaded to the EP by users and published for collaborative consumption - -| Visualisation -| To obtain a visual representation of any data/products held within the platform - presented to the user within their web browser session. - -| Web Coverage Service (WCS) -| OGC standard that provides an open specification for sharing raster datasets on the web. - -| Web Coverage Processing Service (WCPS) -| OGC standard that defines a protocol-independent language for the extraction, processing, and analysis of multi-dimentional coverages representing sensor, image, or statistics data. - -| Web Feature Service (WFS) -| OGC standard that makes geographic feature data (vector geospatial datasets) available on the web. - -| Web Map Service (WMS) -| OGC standard that provides a simple HTTP interface for requesting geo-registered map images from one or more distributed geospatial databases. - -| Web Map Tile Service (WMTS) -| OGC standard that provides a simple HTTP interface for requesting map tiles of spatially referenced data using the images with predefined content, extent, and resolution. - -| Web Processing Services (WPS) -| OGC standard that defines how a client can request the execution of a process, and how the output from the process is handled. - -| Workspace -| A user-scoped 'container' in the EP, in which each user maintains their own links to resources (products and services) that have been collected by a user during their usage of the EP. The workspace acts as the hub for a user's exploitation activities within the EP - -|=== diff --git a/docs/01.introduction/05.glossary.adoc b/docs/01.introduction/05.glossary.adoc deleted file mode 100644 index 18031f4..0000000 --- a/docs/01.introduction/05.glossary.adoc +++ /dev/null @@ -1,49 +0,0 @@ - -= Glossary - -The following acronyms and abbreviations have been used in this report. - -[cols="1,6"] -|=== -| Term | Definition - -| AAI | Authentication & Authorization Infrastructure -| ABAC | Attribute Based Access Control -| ADES | Application Deployment and Execution Service -| ALFA | Abbreviated Language For Authorization -| AOI | Area of Interest -| API | Application Programming Interface -| CMS | Content Management System -| CWL | Common Workflow Language -| DAL | Data Access Library -| EMS | Execution Management Service -| EO | Earth Observation -| EP | Exploitation Platform -| FUSE | Filesystem in Userspace -| GeoXACML | Geo-specific extension to the XACML Policy Language -| IAM | Identity and Access Management -| IdP | Identity Provider -| JSON | JavaScript Object Notation -| K8s | Kubernetes -| KVP | Key-value Pair -| M2M | Machine-to-machine -| OGC | Open Geospatial Consortium -| PDE | Processor Development Environment -| PDP | Policy Decision Point -| PEP | Policy Enforcement Point -| PIP | Policy Information Point -| RBAC | Role Based Access Control -| REST | Representational State Transfer -| SSH | Secure Shell -| TOI | Time of Interest -| UMA | User-Managed Access -| VNC | Virtual Network Computing -| WCS | Web Coverage Service -| WCPS | Web Coverage Processing Service -| WFS | Web Feature Service -| WMS | Web Map Service -| WMTS | Web Map Tile Service -| WPS | Web Processing Service -| WPS-T | Transactional Web Processing Service -| XACML | eXtensible Access Control Markup Language -|=== diff --git a/docs/02.overview/00.overview.adoc b/docs/02.overview/00.overview.adoc deleted file mode 100644 index e7845bf..0000000 --- a/docs/02.overview/00.overview.adoc +++ /dev/null @@ -1,4 +0,0 @@ -[[mainOverview]] -= Overview - -TBD diff --git a/docs/03.design/00.design.adoc b/docs/03.design/00.design.adoc deleted file mode 100644 index a5dbe96..0000000 --- a/docs/03.design/00.design.adoc +++ /dev/null @@ -1,4 +0,0 @@ -[[mainDesign]] -= Design - -TBD diff --git a/docs/README.adoc b/docs/README.adoc deleted file mode 100644 index 9a653cb..0000000 --- a/docs/README.adoc +++ /dev/null @@ -1,32 +0,0 @@ -= Documentation -:component-name: