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]
-
-
-
-
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:
-:component-github-name: template-svce
-
-The documentation is written as AsciiDoc and auto-generated by the Travis job.
-
-The output of the auto-generation is published via GitHub pages and so available at the URL https://eoepca.github.io/{component-github-name}.
-
-== Setup Pre-requisites
-
-This section documents the pre-requisites for the job.
-
-=== gh-pages branch
-
-The travis job pushes the generated doc files (html/images + pdf) to trhe 'gh-pages' branch of the repository. This should be created if it doesn't already exist.
-
-=== Travis settings
-
-The Travis settings for the project must include the following environment variables...
-
-GH_USER_NAME::
-the name used in the commit message
-
-GH_USER_EMAIL::
-the email address used in the commit message
-
-GH_REPOS_NAME::
-the name of the repos in GitHub, i.e. the last part of the GitHub URL https://github.com/EOEPCA/
-
-GH_TOKEN::
-the access token that allows Travis to push to the GitHub repos 'gh-pages' branch. This must be generated as a `Personal Access Token` within the settings of the `EOEPCA-CI` GitHub account (https://github.com/settings/tokens) and pasted in to set the environment variable in Travis. +
-*In doing so, ensure that the 'display value' option is disabled*.
diff --git a/docs/amendment-history.adoc b/docs/amendment-history.adoc
deleted file mode 100644
index c38f813..0000000
--- a/docs/amendment-history.adoc
+++ /dev/null
@@ -1,16 +0,0 @@
-
-'''
-
-AMENDMENT HISTORY::
-This document shall be amended by releasing a new edition of the document in its entirety. +
-The Amendment Record Sheet below records the history and issue status of this document.
-+
-.Amendment Record Sheet
-[cols="^1h,^2,<5"]
-|===
-| ISSUE | DATE | REASON
-
-| 0.1 | dd/mm/yyyy | Initial in-progress draft
-|===
-
-'''
diff --git a/docs/bin/generate-docs.sh b/docs/bin/generate-docs.sh
deleted file mode 100755
index 1df07ee..0000000
--- a/docs/bin/generate-docs.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env bash
-
-ORIG_DIR="$(pwd)"
-cd "$(dirname "$0")"
-BIN_DIR="$(pwd)"
-
-trap "cd '${ORIG_DIR}'" EXIT
-
-# Work in the docs/ directory
-cd "${BIN_DIR}/.."
-
-# Set context dependant on whether this script has been invoked by Travis or not
-if [ "${TRAVIS}" = "true" ]
-then
- echo "Running in Travis..."
-else
- # Running locally we want to remove the docker containers
- export DOCKER_RM="--rm"
-fi
-
-# Prepare output/ directory
-rm -rf output
-mkdir -p output
-cp -r images output
-cp -r stylesheets output
-
-# Docuemnt Generation - using asciidoctor docker image
-#
-# HTML version
-docker run ${DOCKER_RM} -v $PWD:/documents/ --name asciidoc-to-html asciidoctor/docker-asciidoctor asciidoctor -r asciidoctor-diagram -D /documents/output index.adoc
-# PDF version
-docker run ${DOCKER_RM} -v $PWD:/documents/ --name asciidoc-to-pdf asciidoctor/docker-asciidoctor asciidoctor-pdf -r asciidoctor-diagram -D /documents/output index.adoc
diff --git a/docs/bin/publish-docs.sh b/docs/bin/publish-docs.sh
deleted file mode 100755
index 15dbea3..0000000
--- a/docs/bin/publish-docs.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env bash
-
-ORIG_DIR="$(pwd)"
-cd "$(dirname "$0")"
-BIN_DIR="$(pwd)"
-
-trap "cd '${ORIG_DIR}'" EXIT
-
-# Work in the docs/ directory
-cd "${BIN_DIR}/.."
-
-# Check the output directory exists
-if [ ! -d "output" ]
-then
- echo "ERROR: output directory is missing, generate-docs must be run first" >&2
- exit 1
-fi
-
-# Deduce the name of the repository
-if [ -z "${GH_REPOS_NAME}" ]
-then
- export GH_REPOS_NAME="$(basename $(dirname $PWD))"
-fi
-
-# Clone the 'gh-pages' branch
-git clone --branch gh-pages --single-branch "https://${GH_TOKEN}@github.com/EOEPCA/${GH_REPOS_NAME}" repos
-
-# Move generated doc outputs to the repos
-cd repos
-rm -rf current
-mv ../output current
-mv current/index.pdf current/EOEPCA-${GH_REPOS_NAME}.pdf
-
-# Prepare the root landing page/README - but don't overwrite if they already exist
-if [ ! -e index.html ]; then cp ../gh-page-root.html index.html; fi
-if [ ! -e README.adoc ]; then cp ../gh-page-README.adoc README.adoc; fi
-
-# Config git profile for commits
-if [ -n "${GH_USER_NAME}" ]; then git config user.name "${GH_USER_NAME}"; fi
-if [ -n "${GH_USER_EMAIL}" ]; then git config user.email "${GH_USER_EMAIL}"; fi
-
-# Commit the newly generated docs, and push to upstream
-git add . ; git commit -m "Deploy to GitHub Pages @ $(date -u)"
-git push
diff --git a/docs/end-of-document.adoc b/docs/end-of-document.adoc
deleted file mode 100644
index 946622b..0000000
--- a/docs/end-of-document.adoc
+++ /dev/null
@@ -1,3 +0,0 @@
-
-[.large]
-<< End of Document >>
diff --git a/docs/gh-page-README.adoc b/docs/gh-page-README.adoc
deleted file mode 100644
index ccb521b..0000000
--- a/docs/gh-page-README.adoc
+++ /dev/null
@@ -1,15 +0,0 @@
-= EOEPCA - {component-name}
-:component-name:
-:component-github-name: template-svce
-
-EO Exploitation Platform Common Architecture (EOEPCA).
-
-This is the latest published version of the *{component-name} Component Design* document.
-
-The latest document https://eoepca.github.io/{component-github-name}/current/[can be viewed here]. It is published here for community review and feedback.
-
-It is also available in https://eoepca.github.io/{component-github-name}/current/EOEPCA-{component-github-name}.pdf[PDF form here].
-
-Older document releases https://eoepca.github.io/{component-github-name}[can be accessed from the landing page here].
-
-Comments are invited and should be made by raising an issue on the link:../../issues[Issues Page].
diff --git a/docs/gh-page-root.html b/docs/gh-page-root.html
deleted file mode 100644
index 66c0812..0000000
--- a/docs/gh-page-root.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
- Document
-
-
- template-svce documentation
- Releases...
-
-
-
diff --git a/docs/images/logo.png b/docs/images/logo.png
deleted file mode 100644
index 6e9ff5c..0000000
Binary files a/docs/images/logo.png and /dev/null differ
diff --git a/docs/index.adoc b/docs/index.adoc
deleted file mode 100644
index 5675198..0000000
--- a/docs/index.adoc
+++ /dev/null
@@ -1,59 +0,0 @@
-:doctype: book
-:author: TVUK System Team
-:email:
-:project: EOEPCA
-:project-name: EO Exploitation Platform Common Architecture
-:component-name:
-:component-github-name: template-svce
-:doc-title: {component-name} Design Document
-:doc-num: {project}.SDD.xxx
-:revnumber: 0.1
-:revdate: dd/mm/yyyy
-:revremark:
-:category: Design Document
-:copyrightYear: 2020
-// attributes
-:hardbreaks:
-:sectnums:
-:sectnumlevels: 5
-:toc: left
-:toclevels: 5
-:toc-title: {doc-title}
-//:toc-title:
-:description: {doc-title} for the Common Architecture
-:keywords: common architecture design
-:imagesdir: ./images
-:linkcss:
-:stylesdir: stylesheets
-:stylesheet: eoepca.css
-:icons: font
-:source-highlighter: coderay
-// pdf
-:pdf-stylesdir: resources/themes
-:pdf-style: eoepca
-:media: screen
-:title-logo-image: image::logo.png[top=5%, align=right, pdfwidth=6.5in]
-
-= {doc-title}: {doc-num}
-
-:leveloffset: +1
-
-include::preface.adoc[]
-
-<<<
-
-include::01.introduction/00.introduction.adoc[]
-
-<<<
-
-include::02.overview/00.overview.adoc[]
-
-<<<
-
-include::03.design/00.design.adoc[]
-
-'''
-
-include::end-of-document.adoc[]
-
-:leveloffset: -1
diff --git a/docs/preface.adoc b/docs/preface.adoc
deleted file mode 100644
index 0600e6e..0000000
--- a/docs/preface.adoc
+++ /dev/null
@@ -1,22 +0,0 @@
-
-{project-name}
-_{doc-title}_
-{doc-num}
-
-[cols="^1,^1"]
-|===
-| *COMMENTS and ISSUES* +
-If you would like to raise comments or issues on this document, please do so by raising an Issue at the following URL https://github.com/EOEPCA/{component-github-name}/issues.
-| *PDF* +
-This document is available in PDF format link:EOEPCA-{component-github-name}.pdf[here^].
-| *EUROPEAN SPACE AGENCY CONTRACT REPORT* +
-The work described in this report was done under ESA contract. Responsibility for the contents resides in the author or organisation that prepared it.
-| *TELESPAZIO VEGA UK Ltd* +
-350 Capability Green, Luton, Bedfordshire, LU1 3LU, United Kingdom. +
-Tel: +44 (0)1582 399000 +
-http://telespazio-vega.com/[www.telespazio-vega.com]
-|===
-
-include::amendment-history.adoc[]
-
-<<<
diff --git a/docs/resources/themes/eoepca-theme.yml b/docs/resources/themes/eoepca-theme.yml
deleted file mode 100644
index 9bb3156..0000000
--- a/docs/resources/themes/eoepca-theme.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-extends: origdefault
-base:
- font_color: 000000
-running_content:
- start_at: title
-footer:
- height: $base_line_height_length * 4
- columns: "<35% =30% >35%"
- # odd
- recto:
- left:
- content: |
- {project} +
- {doc-title}
- center:
- content: '{page-number}'
- right:
- content: |
- {doc-num} +
- Issue {revnumber}
- # even
- verso:
- left:
- content: $footer_recto_right_content
- center:
- content: $footer_recto_center_content
- right:
- content: $footer_recto_left_content
diff --git a/docs/resources/themes/origdefault-theme.yml b/docs/resources/themes/origdefault-theme.yml
deleted file mode 100644
index b762948..0000000
--- a/docs/resources/themes/origdefault-theme.yml
+++ /dev/null
@@ -1,274 +0,0 @@
-font:
- catalog:
- # Noto Serif supports Latin, Latin-1 Supplement, Latin Extended-A, Greek, Cyrillic, Vietnamese & an assortment of symbols
- Noto Serif:
- normal: notoserif-regular-subset.ttf
- bold: notoserif-bold-subset.ttf
- italic: notoserif-italic-subset.ttf
- bold_italic: notoserif-bold_italic-subset.ttf
- # M+ 1mn supports ASCII and the circled numbers used for conums
- M+ 1mn:
- normal: mplus1mn-regular-ascii-conums.ttf
- bold: mplus1mn-bold-ascii.ttf
- italic: mplus1mn-italic-ascii.ttf
- bold_italic: mplus1mn-bold_italic-ascii.ttf
- # M+ 1p supports Latin, Latin-1 Supplement, Latin Extended, Greek, Cyrillic, Vietnamese, Japanese & an assortment of symbols
- # It also provides arrows for ->, <-, => and <= replacements in case these glyphs are missing from font
- M+ 1p Fallback:
- normal: mplus1p-regular-fallback.ttf
- bold: mplus1p-regular-fallback.ttf
- italic: mplus1p-regular-fallback.ttf
- bold_italic: mplus1p-regular-fallback.ttf
- fallbacks:
- - M+ 1p Fallback
-page:
- background_color: ffffff
- layout: portrait
- margin: [0.5in, 0.67in, 0.67in, 0.67in]
- # margin_inner and margin_outer keys are used for recto/verso print margins when media=prepress
- margin_inner: 0.75in
- margin_outer: 0.59in
- size: A4
-base:
- align: justify
- # color as hex string (leading # is optional)
- font_color: 333333
- # color as RGB array
- #font_color: [51, 51, 51]
- # color as CMYK array (approximated)
- #font_color: [0, 0, 0, 0.92]
- #font_color: [0, 0, 0, 92%]
- font_family: Noto Serif
- # choose one of these font_size/line_height_length combinations
- #font_size: 14
- #line_height_length: 20
- #font_size: 11.25
- #line_height_length: 18
- #font_size: 11.2
- #line_height_length: 16
- font_size: 10.5
- #line_height_length: 15
- # correct line height for Noto Serif metrics
- line_height_length: 12
- #font_size: 11.25
- #line_height_length: 18
- line_height: $base_line_height_length / $base_font_size
- font_size_large: round($base_font_size * 1.25)
- font_size_small: round($base_font_size * 0.85)
- font_size_min: $base_font_size * 0.75
- font_style: normal
- border_color: eeeeee
- border_radius: 4
- border_width: 0.5
-# FIXME vertical_rhythm is weird; we should think in terms of ems
-#vertical_rhythm: $base_line_height_length * 2 / 3
-# correct line height for Noto Serif metrics (comes with built-in line height)
-vertical_rhythm: $base_line_height_length
-horizontal_rhythm: $base_line_height_length
-# QUESTION should vertical_spacing be block_spacing instead?
-vertical_spacing: $vertical_rhythm
-link:
- font_color: 428bca
-# literal is currently used for inline monospaced in prose and table cells
-literal:
- font_color: b12146
- font_family: M+ 1mn
-menu_caret_content: " \u203a "
-heading:
- align: left
- #font_color: 181818
- font_color: $base_font_color
- font_family: $base_font_family
- font_style: bold
- # h1 is used for part titles (book doctype) or the doctitle (article doctype)
- h1_font_size: floor($base_font_size * 2.6)
- # h2 is used for chapter titles (book doctype only)
- h2_font_size: floor($base_font_size * 2.15)
- h3_font_size: round($base_font_size * 1.7)
- h4_font_size: $base_font_size_large
- h5_font_size: $base_font_size
- h6_font_size: $base_font_size_small
- #line_height: 1.4
- # correct line height for Noto Serif metrics (comes with built-in line height)
- line_height: 1
- margin_top: $vertical_rhythm * 0.4
- margin_bottom: $vertical_rhythm * 0.9
-title_page:
- align: right
- logo:
- top: 10%
- title:
- top: 55%
- font_size: $heading_h1_font_size
- font_color: 999999
- line_height: 0.9
- subtitle:
- font_size: $heading_h3_font_size
- font_style: bold_italic
- line_height: 1
- authors:
- margin_top: $base_font_size * 1.25
- font_size: $base_font_size_large
- font_color: 181818
- revision:
- margin_top: $base_font_size * 1.25
-block:
- margin_top: 0
- margin_bottom: $vertical_rhythm
-caption:
- align: left
- font_size: $base_font_size * 0.95
- font_style: italic
- # FIXME perhaps set line_height instead of / in addition to margins?
- margin_inside: $vertical_rhythm / 3
- #margin_inside: $vertical_rhythm / 4
- margin_outside: 0
-lead:
- font_size: $base_font_size_large
- line_height: 1.4
-abstract:
- font_color: 5c6266
- font_size: $lead_font_size
- line_height: $lead_line_height
- font_style: italic
- first_line_font_style: bold
- title:
- align: center
- font_color: $heading_font_color
- font_family: $heading_font_family
- font_size: $heading_h4_font_size
- font_style: $heading_font_style
-admonition:
- column_rule_color: $base_border_color
- column_rule_width: $base_border_width
- padding: [0, $horizontal_rhythm, 0, $horizontal_rhythm]
- #icon:
- # tip:
- # name: far-lightbulb
- # stroke_color: 111111
- # size: 24
- label:
- text_transform: uppercase
- font_style: bold
-blockquote:
- font_color: $base_font_color
- font_size: $base_font_size_large
- border_color: $base_border_color
- border_width: 5
- # FIXME disable negative padding bottom once margin collapsing is implemented
- padding: [0, $horizontal_rhythm, $block_margin_bottom * -0.75, $horizontal_rhythm + $blockquote_border_width / 2]
- cite_font_size: $base_font_size_small
- cite_font_color: 999999
-# code is used for source blocks (perhaps change to source or listing?)
-code:
- font_color: $base_font_color
- font_family: $literal_font_family
- font_size: ceil($base_font_size)
- padding: $code_font_size
- line_height: 1.25
- # line_gap is an experimental property to control how a background color is applied to an inline block element
- line_gap: 3.8
- background_color: f5f5f5
- border_color: cccccc
- border_radius: $base_border_radius
- border_width: 0.75
-conum:
- font_family: M+ 1mn
- font_color: $literal_font_color
- font_size: $base_font_size
- line_height: 4 / 3
-example:
- border_color: $base_border_color
- border_radius: $base_border_radius
- border_width: 0.75
- background_color: ffffff
- # FIXME reenable padding bottom once margin collapsing is implemented
- padding: [$vertical_rhythm, $horizontal_rhythm, 0, $horizontal_rhythm]
-image:
- align: left
-prose:
- margin_top: $block_margin_top
- margin_bottom: $block_margin_bottom
-sidebar:
- background_color: eeeeee
- border_color: e1e1e1
- border_radius: $base_border_radius
- border_width: $base_border_width
- # FIXME reenable padding bottom once margin collapsing is implemented
- padding: [$vertical_rhythm, $vertical_rhythm * 1.25, 0, $vertical_rhythm * 1.25]
- title:
- align: center
- font_color: $heading_font_color
- font_family: $heading_font_family
- font_size: $heading_h4_font_size
- font_style: $heading_font_style
-thematic_break:
- border_color: $base_border_color
- border_style: solid
- border_width: $base_border_width
- margin_top: $vertical_rhythm * 0.5
- margin_bottom: $vertical_rhythm * 1.5
-description_list:
- term_font_style: bold
- term_spacing: $vertical_rhythm / 4
- description_indent: $horizontal_rhythm * 1.25
-outline_list:
- indent: $horizontal_rhythm * 1.5
- #marker_font_color: 404040
- # NOTE outline_list_item_spacing applies to list items that do not have complex content
- item_spacing: $vertical_rhythm / 2
-table:
- background_color: $page_background_color
- #head_background_color:
- #head_font_color: $base_font_color
- head_font_style: bold
- #body_background_color:
- body_stripe_background_color: f9f9f9
- foot_background_color: f0f0f0
- border_color: dddddd
- border_width: $base_border_width
- cell_padding: 3
-toc:
- indent: $horizontal_rhythm
- line_height: 1.4
- dot_leader:
- #content: ". "
- font_color: a9a9a9
- #levels: 2 3
-footnotes:
- font_size: round($base_font_size * 0.75)
- item_spacing: $outline_list_item_spacing / 2
-# NOTE in addition to footer, header is also supported
-footer:
- font_size: $base_font_size_small
- # NOTE if background_color is set, background and border will span width of page
- border_color: dddddd
- border_width: 0.25
- height: $base_line_height_length * 2.5
- line_height: 1
- padding: [$base_line_height_length / 2, 1, 0, 1]
- vertical_align: top
- #image_vertical_align: or
- # additional attributes for content:
- # * {page-count}
- # * {page-number}
- # * {document-title}
- # * {document-subtitle}
- # * {chapter-title}
- # * {section-title}
- # * {section-or-chapter-title}
- recto:
- #columns: "<50% =0% >50%"
- right:
- content: '{page-number}'
- #content: '{section-or-chapter-title} | {page-number}'
- #content: '{document-title} | {page-number}'
- #center:
- # content: '{page-number}'
- verso:
- #columns: $footer_recto_columns
- left:
- content: $footer_recto_right_content
- #content: '{page-number} | {chapter-title}'
- #center:
- # content: '{page-number}'
diff --git a/docs/stylesheets/asciidoctor.css b/docs/stylesheets/asciidoctor.css
deleted file mode 100644
index 37a53c3..0000000
--- a/docs/stylesheets/asciidoctor.css
+++ /dev/null
@@ -1,420 +0,0 @@
-/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */
-/* Uncomment @import statement below to use as custom stylesheet */
-@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700";
-article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}
-audio,canvas,video{display:inline-block}
-audio:not([controls]){display:none;height:0}
-script{display:none!important}
-html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
-a{background:transparent}
-a:focus{outline:thin dotted}
-a:active,a:hover{outline:0}
-h1{font-size:2em;margin:.67em 0}
-abbr[title]{border-bottom:1px dotted}
-b,strong{font-weight:bold}
-dfn{font-style:italic}
-hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}
-mark{background:#ff0;color:#000}
-code,kbd,pre,samp{font-family:monospace;font-size:1em}
-pre{white-space:pre-wrap}
-q{quotes:"\201C" "\201D" "\2018" "\2019"}
-small{font-size:80%}
-sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
-sup{top:-.5em}
-sub{bottom:-.25em}
-img{border:0}
-svg:not(:root){overflow:hidden}
-figure{margin:0}
-fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
-legend{border:0;padding:0}
-button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
-button,input{line-height:normal}
-button,select{text-transform:none}
-button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}
-button[disabled],html input[disabled]{cursor:default}
-input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}
-button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
-textarea{overflow:auto;vertical-align:top}
-table{border-collapse:collapse;border-spacing:0}
-*,*::before,*::after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}
-html,body{font-size:100%}
-body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto;tab-size:4;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}
-a:hover{cursor:pointer}
-img,object,embed{max-width:100%;height:auto}
-object,embed{height:100%}
-img{-ms-interpolation-mode:bicubic}
-.left{float:left!important}
-.right{float:right!important}
-.text-left{text-align:left!important}
-.text-right{text-align:right!important}
-.text-center{text-align:center!important}
-.text-justify{text-align:justify!important}
-.hide{display:none}
-img,object,svg{display:inline-block;vertical-align:middle}
-textarea{height:auto;min-height:50px}
-select{width:100%}
-.center{margin-left:auto;margin-right:auto}
-.stretch{width:100%}
-.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
-div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr}
-a{color:#2156a5;text-decoration:underline;line-height:inherit}
-a:hover,a:focus{color:#1d4b8f}
-a img{border:none}
-p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
-p aside{font-size:.875em;line-height:1.35;font-style:italic}
-h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
-h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
-h1{font-size:2.125em}
-h2{font-size:1.6875em}
-h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
-h4,h5{font-size:1.125em}
-h6{font-size:1em}
-hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0}
-em,i{font-style:italic;line-height:inherit}
-strong,b{font-weight:bold;line-height:inherit}
-small{font-size:60%;line-height:inherit}
-code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)}
-ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
-ul,ol{margin-left:1.5em}
-ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em}
-ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit}
-ul.square{list-style-type:square}
-ul.circle{list-style-type:circle}
-ul.disc{list-style-type:disc}
-ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
-dl dt{margin-bottom:.3125em;font-weight:bold}
-dl dd{margin-bottom:1.25em}
-abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help}
-abbr{text-transform:none}
-blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
-blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)}
-blockquote cite::before{content:"\2014 \0020"}
-blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)}
-blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
-@media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
-h1{font-size:2.75em}
-h2{font-size:2.3125em}
-h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
-h4{font-size:1.4375em}}
-table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede}
-table thead,table tfoot{background:#f7f8f7}
-table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
-table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
-table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7}
-table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6}
-h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
-h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
-.clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table}
-.clearfix::after,.float-group::after{clear:both}
-*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed;word-wrap:break-word}
-*:not(pre)>code.nobreak{word-wrap:normal}
-*:not(pre)>code.nowrap{white-space:nowrap}
-pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed}
-em em{font-style:normal}
-strong strong{font-weight:400}
-.keyseq{color:rgba(51,51,51,.8)}
-kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap}
-.keyseq kbd:first-child{margin-left:0}
-.keyseq kbd:last-child{margin-right:0}
-.menuseq,.menuref{color:#000}
-.menuseq b:not(.caret),.menuref{font-weight:inherit}
-.menuseq{word-spacing:-.02em}
-.menuseq b.caret{font-size:1.25em;line-height:.8}
-.menuseq i.caret{font-weight:bold;text-align:center;width:.45em}
-b.button::before,b.button::after{position:relative;top:-1px;font-weight:400}
-b.button::before{content:"[";padding:0 3px 0 2px}
-b.button::after{content:"]";padding:0 2px 0 3px}
-p a>code:hover{color:rgba(0,0,0,.9)}
-#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
-#header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table}
-#header::after,#content::after,#footnotes::after,#footer::after{clear:both}
-#content{margin-top:1.25em}
-#content::before{content:none}
-#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
-#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf}
-#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px}
-#header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap}
-#header .details span:first-child{margin-left:-.125em}
-#header .details span.email a{color:rgba(0,0,0,.85)}
-#header .details br{display:none}
-#header .details br+span::before{content:"\00a0\2013\00a0"}
-#header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
-#header .details br+span#revremark::before{content:"\00a0|\00a0"}
-#header #revnumber{text-transform:capitalize}
-#header #revnumber::after{content:"\00a0"}
-#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
-#toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em}
-#toc>ul{margin-left:.125em}
-#toc ul.sectlevel0>li>a{font-style:italic}
-#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
-#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
-#toc li{line-height:1.3334;margin-top:.3334em}
-#toc a{text-decoration:none}
-#toc a:active{text-decoration:underline}
-#toctitle{color:#7a2518;font-size:1.2em}
-@media screen and (min-width:768px){#toctitle{font-size:1.375em}
-body.toc2{padding-left:15em;padding-right:0}
-#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
-#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em}
-#toc.toc2>ul{font-size:.9em;margin-bottom:0}
-#toc.toc2 ul ul{margin-left:0;padding-left:1em}
-#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
-body.toc2.toc-right{padding-left:0;padding-right:15em}
-body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}}
-@media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
-#toc.toc2{width:20em}
-#toc.toc2 #toctitle{font-size:1.375em}
-#toc.toc2>ul{font-size:.95em}
-#toc.toc2 ul ul{padding-left:1.25em}
-body.toc2.toc-right{padding-left:0;padding-right:20em}}
-#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
-#content #toc>:first-child{margin-top:0}
-#content #toc>:last-child{margin-bottom:0}
-#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em}
-#footer-text{color:rgba(255,255,255,.8);line-height:1.44}
-#content{margin-bottom:.625em}
-.sect1{padding-bottom:.625em}
-@media screen and (min-width:768px){#content{margin-bottom:1.25em}
-.sect1{padding-bottom:1.25em}}
-.sect1:last-child{padding-bottom:0}
-.sect1+.sect1{border-top:1px solid #e7e7e9}
-#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
-#content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
-#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
-#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
-#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
-.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
-.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
-table.tableblock.fit-content>caption.title{white-space:nowrap;width:0}
-.paragraph.lead>p,#preamble>.sectionbody>[class="paragraph"]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)}
-table.tableblock #preamble>.sectionbody>[class="paragraph"]:first-of-type p{font-size:inherit}
-.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
-.admonitionblock>table td.icon{text-align:center;width:80px}
-.admonitionblock>table td.icon img{max-width:none}
-.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
-.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6)}
-.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
-.exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px}
-.exampleblock>.content>:first-child{margin-top:0}
-.exampleblock>.content>:last-child{margin-bottom:0}
-.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
-.sidebarblock>:first-child{margin-top:0}
-.sidebarblock>:last-child{margin-bottom:0}
-.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
-.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
-.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8}
-.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1}
-.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;overflow-x:auto;padding:1em;font-size:.8125em}
-@media screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}}
-@media screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}}
-.literalblock pre.nowrap,.literalblock pre.nowrap pre,.listingblock pre.nowrap,.listingblock pre.nowrap pre{white-space:pre;word-wrap:normal}
-.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)}
-.listingblock pre.highlightjs{padding:0}
-.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px}
-.listingblock pre.prettyprint{border-width:0}
-.listingblock>.content{position:relative}
-.listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999}
-.listingblock:hover code[data-lang]::before{display:block}
-.listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:#999}
-.listingblock.terminal pre .command:not([data-prompt])::before{content:"$"}
-table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none}
-table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0;line-height:1.45}
-table.pyhltable td.code{padding-left:.75em;padding-right:0}
-pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #dddddf}
-pre.pygments .lineno{display:inline-block;margin-right:.25em}
-table.pyhltable .linenodiv{background:none!important;padding-right:0!important}
-.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
-.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em}
-.quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
-.quoteblock blockquote{margin:0;padding:0;border:0}
-.quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
-.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
-.quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right}
-.verseblock{margin:0 1em 1.25em}
-.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
-.verseblock pre strong{font-weight:400}
-.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
-.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
-.quoteblock .attribution br,.verseblock .attribution br{display:none}
-.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)}
-.quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none}
-.quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0}
-.quoteblock.abstract{margin:0 1em 1.25em;display:block}
-.quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center}
-.quoteblock.excerpt,.quoteblock .quoteblock{margin:0 0 1.25em;padding:0 0 .25em 1em;border-left:.25em solid #dddddf}
-.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem}
-.quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;text-align:left;margin-right:0}
-table.tableblock{max-width:100%;border-collapse:separate}
-p.tableblock:last-child{margin-bottom:0}
-td.tableblock>.content{margin-bottom:-1.25em}
-table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
-table.grid-all>thead>tr>.tableblock,table.grid-all>tbody>tr>.tableblock{border-width:0 1px 1px 0}
-table.grid-all>tfoot>tr>.tableblock{border-width:1px 1px 0 0}
-table.grid-cols>*>tr>.tableblock{border-width:0 1px 0 0}
-table.grid-rows>thead>tr>.tableblock,table.grid-rows>tbody>tr>.tableblock{border-width:0 0 1px}
-table.grid-rows>tfoot>tr>.tableblock{border-width:1px 0 0}
-table.grid-all>*>tr>.tableblock:last-child,table.grid-cols>*>tr>.tableblock:last-child{border-right-width:0}
-table.grid-all>tbody>tr:last-child>.tableblock,table.grid-all>thead:last-child>tr>.tableblock,table.grid-rows>tbody>tr:last-child>.tableblock,table.grid-rows>thead:last-child>tr>.tableblock{border-bottom-width:0}
-table.frame-all{border-width:1px}
-table.frame-sides{border-width:0 1px}
-table.frame-topbot,table.frame-ends{border-width:1px 0}
-table.stripes-all tr,table.stripes-odd tr:nth-of-type(odd){background:#f8f8f7}
-table.stripes-none tr,table.stripes-odd tr:nth-of-type(even){background:none}
-th.halign-left,td.halign-left{text-align:left}
-th.halign-right,td.halign-right{text-align:right}
-th.halign-center,td.halign-center{text-align:center}
-th.valign-top,td.valign-top{vertical-align:top}
-th.valign-bottom,td.valign-bottom{vertical-align:bottom}
-th.valign-middle,td.valign-middle{vertical-align:middle}
-table thead th,table tfoot th{font-weight:bold}
-tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7}
-tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
-p.tableblock>code:only-child{background:none;padding:0}
-p.tableblock{font-size:1em}
-td>div.verse{white-space:pre}
-ol{margin-left:1.75em}
-ul li ol{margin-left:1.5em}
-dl dd{margin-left:1.125em}
-dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
-ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
-ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none}
-ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em}
-ul.unstyled,ol.unstyled{margin-left:0}
-ul.checklist{margin-left:.625em}
-ul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em}
-ul.checklist li>p:first-child>input[type="checkbox"]:first-child{margin-right:.25em}
-ul.inline{display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em}
-ul.inline>li{margin-left:1.25em}
-.unstyled dl dt{font-weight:400;font-style:normal}
-ol.arabic{list-style-type:decimal}
-ol.decimal{list-style-type:decimal-leading-zero}
-ol.loweralpha{list-style-type:lower-alpha}
-ol.upperalpha{list-style-type:upper-alpha}
-ol.lowerroman{list-style-type:lower-roman}
-ol.upperroman{list-style-type:upper-roman}
-ol.lowergreek{list-style-type:lower-greek}
-.hdlist>table,.colist>table{border:0;background:none}
-.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
-td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em}
-td.hdlist1{font-weight:bold;padding-bottom:1.25em}
-.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
-.colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top}
-.colist td:not([class]):first-child img{max-width:none}
-.colist td:not([class]):last-child{padding:.25em 0}
-.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd}
-.imageblock.left{margin:.25em .625em 1.25em 0}
-.imageblock.right{margin:.25em 0 1.25em .625em}
-.imageblock>.title{margin-bottom:0}
-.imageblock.thumb,.imageblock.th{border-width:6px}
-.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
-.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
-.image.left{margin-right:.625em}
-.image.right{margin-left:.625em}
-a.image{text-decoration:none;display:inline-block}
-a.image object{pointer-events:none}
-sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super}
-sup.footnote a,sup.footnoteref a{text-decoration:none}
-sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline}
-#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
-#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0}
-#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em}
-#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em}
-#footnotes .footnote:last-of-type{margin-bottom:0}
-#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
-.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0}
-.gist .file-data>table td.line-data{width:99%}
-div.unbreakable{page-break-inside:avoid}
-.big{font-size:larger}
-.small{font-size:smaller}
-.underline{text-decoration:underline}
-.overline{text-decoration:overline}
-.line-through{text-decoration:line-through}
-.aqua{color:#00bfbf}
-.aqua-background{background-color:#00fafa}
-.black{color:#000}
-.black-background{background-color:#000}
-.blue{color:#0000bf}
-.blue-background{background-color:#0000fa}
-.fuchsia{color:#bf00bf}
-.fuchsia-background{background-color:#fa00fa}
-.gray{color:#606060}
-.gray-background{background-color:#7d7d7d}
-.green{color:#006000}
-.green-background{background-color:#007d00}
-.lime{color:#00bf00}
-.lime-background{background-color:#00fa00}
-.maroon{color:#600000}
-.maroon-background{background-color:#7d0000}
-.navy{color:#000060}
-.navy-background{background-color:#00007d}
-.olive{color:#606000}
-.olive-background{background-color:#7d7d00}
-.purple{color:#600060}
-.purple-background{background-color:#7d007d}
-.red{color:#bf0000}
-.red-background{background-color:#fa0000}
-.silver{color:#909090}
-.silver-background{background-color:#bcbcbc}
-.teal{color:#006060}
-.teal-background{background-color:#007d7d}
-.white{color:#bfbfbf}
-.white-background{background-color:#fafafa}
-.yellow{color:#bfbf00}
-.yellow-background{background-color:#fafa00}
-span.icon>.fa{cursor:default}
-a span.icon>.fa{cursor:inherit}
-.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
-.admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c}
-.admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
-.admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900}
-.admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400}
-.admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000}
-.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
-.conum[data-value] *{color:#fff!important}
-.conum[data-value]+b{display:none}
-.conum[data-value]::after{content:attr(data-value)}
-pre .conum[data-value]{position:relative;top:-.125em}
-b.conum *{color:inherit!important}
-.conum:not([data-value]):empty{display:none}
-dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility}
-h1,h2,p,td.content,span.alt{letter-spacing:-.01em}
-p strong,td.content strong,div.footnote strong{letter-spacing:-.005em}
-p,blockquote,dt,td.content,span.alt{font-size:1.0625rem}
-p{margin-bottom:1.25rem}
-.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
-.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc}
-.print-only{display:none!important}
-@page{margin:1.25cm .75cm}
-@media print{*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}
-html{font-size:80%}
-a{color:inherit!important;text-decoration:underline!important}
-a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
-a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
-abbr[title]::after{content:" (" attr(title) ")"}
-pre,blockquote,tr,img,object,svg{page-break-inside:avoid}
-thead{display:table-header-group}
-svg{max-width:100%}
-p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
-h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
-#toc,.sidebarblock,.exampleblock>.content{background:none!important}
-#toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important}
-body.book #header{text-align:center}
-body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em}
-body.book #header .details{border:0!important;display:block;padding:0!important}
-body.book #header .details span:first-child{margin-left:0!important}
-body.book #header .details br{display:block}
-body.book #header .details br+span::before{content:none!important}
-body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
-body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
-.listingblock code[data-lang]::before{display:block}
-#footer{padding:0 .9375em}
-.hide-on-print{display:none!important}
-.print-only{display:block!important}
-.hide-for-print{display:none!important}
-.show-for-print{display:inherit!important}}
-@media print,amzn-kf8{#header>h1:first-child{margin-top:1.25rem}
-.sect1{padding:0!important}
-.sect1+.sect1{border:0}
-#footer{background:none}
-#footer-text{color:rgba(0,0,0,.6);font-size:.9em}}
-@media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}}
\ No newline at end of file
diff --git a/docs/stylesheets/eoepca.css b/docs/stylesheets/eoepca.css
deleted file mode 100644
index 46a3ba5..0000000
--- a/docs/stylesheets/eoepca.css
+++ /dev/null
@@ -1,25 +0,0 @@
-@import "asciidoctor.css";
-
-/* Centre align figure captions */
-.imageblock.text-center>.title {
- text-align: center;
-}
-
-/* Scale font size of CodeRay [source] elements, which seem a bit big */
-pre.CodeRay code {
- font-size: 0.9em;
-}
-
-/* Bold font for the numbers in an ordered list */
-div.strong > ol {
- font-weight: bold;
-}
-
-/*
- * EXAMPLE for custom code-block scaling...
- * Use an asciidoc source element like this '[source.src-scale05,python]' and then apply a custom style like below.
- * This basically adds the custom CSS class 'src-scale05' to the root of the DOM for the code block, which we can then exploit for styling.
- */
- .src-scale05 pre.CodeRay code {
- font-size: 0.5em;
-}
diff --git a/requirements.txt b/requirements.txt
index 464652f..c5373c4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,15 +1,9 @@
-Flask==3.0.0
-WellKnownHandler==0.2.0
-requests==2.31.0
-flask-swagger-ui==4.11.1
-python-keycloak==3.3.0
-mock==5.1.0
+fastapi==0.104.1
+uvicorn==0.24.0.post1
+python-keycloak==3.7.0
pyyaml==6.0.1
-elasticsearch==8.8.0
-lxml==4.9.3
configparser==6.0.0
-waitress==2.1.2
-python-dotenv==1.0.0
retry==0.9.2
-flask-healthz==1.0.0
-identityutils @ git+https://github.com/eoepca/um-identity-service@master
\ No newline at end of file
+urllib3==2.0.7
+pydantic==2.5.0
+identityutils @ git+https://github.com/eoepca/um-identity-service@v1.0.0
\ No newline at end of file
diff --git a/src/__init __.py b/src/__init __.py
deleted file mode 100644
index 819ccf0..0000000
--- a/src/__init __.py
+++ /dev/null
@@ -1 +0,0 @@
-from .app import create_app
diff --git a/src/app.py b/src/app.py
deleted file mode 100644
index 5978c57..0000000
--- a/src/app.py
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/usr/bin/env python3
-
-import json
-import logging
-import os
-from random import choice
-from string import ascii_lowercase
-
-from flask import Flask
-from flask_swagger_ui import get_swaggerui_blueprint
-from keycloak import KeycloakConnectionError
-from urllib3.exceptions import NewConnectionError
-
-import blueprints.permissions as permissions
-import blueprints.policies as policies
-import blueprints.resources as resources
-import identityutils.logger as logger
-from identityutils.configuration import load_configuration
-from identityutils.keycloak_client import KeycloakClient
-from retry.api import retry_call
-from flask_healthz import healthz
-
-logger.Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../conf/logging.yaml"))
-logger = logging.getLogger("IDENTITY_API")
-
-config_path = os.path.join(os.path.dirname(__file__), "../conf/config.ini")
-
-app = Flask(__name__)
-app.secret_key = ''.join(choice(ascii_lowercase) for _ in range(30)) # Random key
-app.config['HEALTHZ'] = {
- "live": lambda: None,
- "ready": lambda: None
-}
-
-def register_endpoints(config, keycloak):
- app.register_blueprint(resources.construct_blueprint(keycloak_client=keycloak))
- app.register_blueprint(policies.construct_blueprint(keycloak_client=keycloak))
- app.register_blueprint(permissions.construct_blueprint(keycloak_client=keycloak))
- app.register_blueprint(healthz, url_prefix="/health")
- swagger_spec_resources = json.load(open(os.path.join(os.path.dirname(__file__), "../conf/swagger.json")))
- swaggerui_resources_blueprint = get_swaggerui_blueprint(
- config.get('Swagger', 'swagger_url'),
- config.get('Swagger', 'swagger_api_url'),
- config={
- 'app_name': config.get('Swagger', 'swagger_app_name'),
- 'spec': swagger_spec_resources
- },
- )
- app.register_blueprint(swaggerui_resources_blueprint)
-
-
-def keycloak_client(config):
- auth_server_url = config.get("Keycloak", "auth_server_url")
- realm = config.get("Keycloak", "realm")
- logger.info("Starting Keycloak client for: " + str(auth_server_url) + " realm: " + str(realm))
- return KeycloakClient(server_url=auth_server_url,
- realm=realm,
- username=config.get("Keycloak", "admin_username"),
- password=config.get("Keycloak", "admin_password")
- )
-
-
-def create_app():
- """Create a Flask application using the app factory pattern."""
- config = load_configuration(config_path)
- keycloak = retry_call(keycloak_client, fargs=[config], exceptions=(KeycloakConnectionError, NewConnectionError),
- delay=0.5, backoff=1.2, jitter=(1, 2), logger=logger)
- register_endpoints(config, keycloak)
- return app
\ No newline at end of file
diff --git a/src/blueprints/permissions.py b/src/blueprints/permissions.py
deleted file mode 100644
index 446c8cf..0000000
--- a/src/blueprints/permissions.py
+++ /dev/null
@@ -1,89 +0,0 @@
-from flask import Blueprint, request
-from keycloak import KeycloakGetError, KeycloakPostError, KeycloakPutError
-
-
-def construct_blueprint(keycloak_client):
- keycloak_client = keycloak_client
- permissions = Blueprint('permissions', __name__)
-
- @permissions.route("//permissions", methods=["OPTIONS", "GET"])
- def get_client_authz_permissions(client_id: str):
- try:
- response = keycloak_client.get_client_authz_permissions(client_id)
- return response
- except KeycloakGetError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @permissions.route("//permissions/management", methods=["OPTIONS", "GET"])
- def get_client_management_permissions(client_id: str):
- try:
- response = keycloak_client.get_client_management_permissions(client_id)
- return response
- except KeycloakGetError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @permissions.route("//permissions/resources", methods=["OPTIONS", "GET"])
- def get_client_resource_permissions(client_id: str):
- try:
- response = keycloak_client.get_client_resource_permissions(client_id)
- return response
- except KeycloakGetError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- #@permissions.route("/client_authz_scope_permissions//", methods=["OPTIONS", "GET"])
- #def get_client_authz_scope_permissions(client_id: str, scope_id: str):
- # return keycloak_client.get_client_authz_scope_permissions(client_id, scope_id)
-
- #@permissions.route("/client_authz_scope_permissions/", methods=["OPTIONS", "POST"])
- #def create_client_authz_scope_based_permissions(client_id: str):
- # payload = request.get_json()
- # return keycloak_client.create_client_authz_scope_based_permission(client_id, payload)
-
- @permissions.route("//permissions/resources", methods=["OPTIONS", "POST"])
- def create_client_authz_resource_based_permission(client_id: str):
- payload = request.get_json()
- try:
- response = keycloak_client.create_client_authz_resource_based_permission(client_id, payload)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @permissions.route("//permissions/management", methods=["OPTIONS", "PUT"])
- def update_client_management_permissions(client_id: str):
- payload = request.get_json()
- try:
- response = keycloak_client.update_client_management_permissions(client_id, payload)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @permissions.route("//permissions/resources/", methods=["OPTIONS", "PUT"])
- def update_client_authz_resource_permission(client_id: str, permission_id):
- payload = request.get_json()
- try:
- response = keycloak_client.update_client_authz_resource_permission(client_id, payload, permission_id)
- return response
- except KeycloakPutError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- #@permissions.route("//permissions/scopes/", methods=["OPTIONS", "PUT"])
- #def update_client_authz_scope_permissions(client_id: str, scope_id):
- # payload = request.get_json()
- # return keycloak_client.update_client_authz_scope_permission(client_id, payload, scope_id)
-
- def custom_error(message, status_code):
- return message, status_code
-
- return permissions
diff --git a/src/blueprints/policies.py b/src/blueprints/policies.py
deleted file mode 100644
index 46a293d..0000000
--- a/src/blueprints/policies.py
+++ /dev/null
@@ -1,178 +0,0 @@
-from flask import Blueprint, request
-from keycloak import KeycloakDeleteError, KeycloakGetError, KeycloakPostError, KeycloakPutError
-
-
-def construct_blueprint(keycloak_client):
- keycloak_client = keycloak_client
- policies = Blueprint('policies', __name__)
-
- # -------- Always returns empty -------
- #@policies.route("/policies", methods=["OPTIONS", "GET"])
- #def get_policies():
- # resource = request.args.get('resource', "")
- # name = request.args.get('name', "")
- # scope = request.args.get('uri', "")
- # first = int(request.args.get('first', 0))
- # maximum = int(request.args.get('maximum', -1))
- # return keycloak_client.get_policies(resource, name, scope, first, maximum)
- # --------------- GET -----------------
-
- @policies.route("//policies", methods=["OPTIONS", "GET"])
- def get_client_authz_policies(client_id: str):
- try:
- response = keycloak_client.get_client_authz_policies(client_id)
- return response
- except KeycloakGetError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- # --------------- POST -----------------
-
- @policies.route("//policies/client", methods=["OPTIONS", "POST"])
- def create_client_policy(client_id: str):
- policy = request.get_json()
- try:
- response = keycloak_client.register_client_policy(policy, client_id)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
-
- @policies.route("//policies/aggregated", methods = ["POST"])
- def create_aggregated_policy(client_id: str):
- policy = request.get_json()
- try:
- response = keycloak_client.register_aggregated_policy(policy, client_id)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @policies.route("//policies/scope", methods = ["POST"])
- def create_client_scope_policy(client_id: str):
- policy = request.get_json()
- try:
- response = keycloak_client.register_client_scope_policy(policy, client_id)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @policies.route("//policies/group", methods = ["POST"])
- def create_group_policy(client_id: str):
- policy = request.get_json()
- try:
- response = keycloak_client.register_group_policy(policy, client_id)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @policies.route("//policies/regex", methods = ["POST"])
- def create_regex_policy(client_id: str):
- policy = request.get_json()
- try:
- response = keycloak_client.register_regex_policy(policy, client_id)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @policies.route("//policies/role", methods = ["POST"])
- def create_role_policy(client_id: str):
- policy = request.get_json()
- try:
- response = keycloak_client.register_role_policy(policy, client_id)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @policies.route("//policies/time", methods = ["POST"])
- def create_time_policy(client_id: str):
- # time can be one of:
- # "notAfter":"1970-01-01 00:00:00"
- # "notBefore":"1970-01-01 00:00:00"
- # "dayMonth":
- # "dayMonthEnd":
- # "month":
- # "monthEnd":
- # "year":
- # "yearEnd":
- # "hour":
- # "hourEnd":
- # "minute":
- # "minuteEnd":
- possible_times = [
- "notAfter",
- "notBefore",
- "dayMonth",
- "dayMonthEnd",
- "month",
- "monthEnd",
- "year",
- "yearEnd",
- "hour",
- "hourEnd",
- "minute",
- "minuteEnd"
- ]
- policy = request.get_json()
- try:
- response = keycloak_client.register_time_policy(policy, client_id)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @policies.route("//policies/user", methods = ["POST"])
- def create_user_policy(client_id: str):
- policy = request.get_json()
- try:
- response = keycloak_client.register_user_policy(policy, client_id)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
-
-
- # --------------- UPDATE -----------------
-
- @policies.route("//policies/", methods=["OPTIONS", "PUT"])
- def update_policy(client_id: str, policy_id: str):
- policy = request.get_json()
- try:
- response = keycloak_client.update_policy(policy_id, policy, client_id)
- return response
- except KeycloakPutError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- # --------------- DELETE -----------------
-
- @policies.route("//policies/", methods=["OPTIONS", "DELETE"])
- def delete_policy(client_id: str ,policy_id: str):
- try:
- response = keycloak_client.delete_policy(policy_id, client_id)
- return response
- except KeycloakDeleteError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- def custom_error(message, status_code):
- return message, status_code
-
- return policies
\ No newline at end of file
diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py
deleted file mode 100644
index 7060bd2..0000000
--- a/src/blueprints/resources.py
+++ /dev/null
@@ -1,352 +0,0 @@
-from flask import Blueprint, request
-from keycloak import KeycloakDeleteError, KeycloakGetError, KeycloakPostError, KeycloakPutError
-
-
-def construct_blueprint(keycloak_client):
- keycloak_client = keycloak_client
- resources = Blueprint('resources', __name__)
-
- @resources.route("//resources", methods=["OPTIONS", "GET"])
- def get_resources(client_id: str):
- try:
- response = keycloak_client.get_resources(client_id)
- return response
- except KeycloakGetError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @resources.route("/resources/", methods=["OPTIONS", "GET"])
- def get_resource(resource_id: str):
- try:
- response = keycloak_client.get_resource(resource_id)
- return response
- except KeycloakGetError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @resources.route("//resources", methods=["OPTIONS", "POST"])
- def register_resource(client_id: str ):
- resource = request.get_json()
- try:
- response = keycloak_client.register_resource(resource, client_id)
- return response
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
-
- @resources.route("//register-resources", methods=["OPTIONS", "POST"])
- def register_and_protect_resources(client_id: str, payload=None ):
- """payload = [{
- "resource":{
- "name": "resource1",
- "uris": ["/resource1/", "/resource2/"],
- 'attributes': {},
- 'scopes': ['view'],
- 'ownerManagedAccess': False,
- },
- "permissions": {
- "user": {
- "users":["user1","user2"],
- "logic":"NEGATIVE"
- },
- "role": {
- "roles":["role1","role2"],
- "logic":"POSITIVE"
- },
- },
- "decisionStrategy": "UNANIMOUS"
- }]"""
- if payload == None:
- payload = request.get_json()
- policy_list = []
-
- response_list = []
-
- for item in payload:
- # validate item fields
- error = _validate_register_resource(item)
- if error:
- return custom_error(error, 400)
-
- resource = item["resource"]
- policies = item["permissions"]
- decisionStrategy = item['decisionStrategy'] if 'decisionStrategy' in item else "UNANIMOUS"
- type = 'urn:' + client_id + ':resources:default'
- scopes = resource['scopes'] if 'scopes' in resource and resource['scopes'] != [] else ['access']
-
- try:
- # reconstruct resource object so it works when user sends unknown fields and to change field names to match what keycloak api expects
- resource["name"] = resource["name"].replace(" ", "_")
- response_resource = keycloak_client.register_resource( resource, client_id)
- for policy_type in policies:
- policy = {"name": resource["name"].replace(" ", "") + "_" + policy_type + "_policy"}
- if isinstance(policies[policy_type], list):
- match policy_type:
- case 'user':
- policy['users'] = policies[policy_type]
- case 'role':
- policy['roles'] = policies[policy_type]
- case 'aggregated':
- policy['policies'] = policies[policy_type]
- case 'group':
- policy['groups'] = policies[policy_type]
- else:
- for _key in policies[policy_type]:
- policy[_key] = policies[policy_type][_key]
- policy_list.append(policy["name"])
- response_policy = keycloak_client.register_general_policy(policy, client_id, policy_type)
-
- permission_payload = {
- "type": "resource",
- "name": resource["name"] + "_permission",
- "decisionStrategy": decisionStrategy,
- "resources": [
- resource["name"]
- ],
- "policies": policy_list
- }
-
- permission_response = keycloak_client.create_client_authz_resource_based_permission(client_id, permission_payload)
-
- response_list.append(response_resource)
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
- return response_list
-
-
- @resources.route("//delete-resources/", methods=["OPTIONS", "DELETE"])
- def delete_resource_and_policies(client_id: str, resource_name: str):
- try:
- client_policies = keycloak_client.get_client_authz_policies(client_id)
- policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated']
- for policy in client_policies:
- for policy_type in policy_types:
- if policy['name'] == resource_name + '_' + policy_type + '_policy':
- keycloak_client.delete_policy(policy['id'], client_id)
- permissions = keycloak_client.get_client_resource_permissions(client_id)
- for permission in permissions:
- if permission['name'] == resource_name +'permission':
- keycloak_client.delete_resource_permissions(client_id, permission['id'])
-
- _resources = keycloak_client.get_resources(client_id)
- for resource in _resources:
- if resource['name'] == resource_name:
- resource_delete_response = keycloak_client.delete_resource(resource['_id'], client_id)
- return resource_delete_response
- except KeycloakDeleteError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @resources.route("//resources/", methods=["OPTIONS", "PUT"])
- def update_resource(client_id: str, resource_id: str):
- resource = request.get_json()
- try:
- response = keycloak_client.update_resource(resource_id, resource, client_id)
- return response
- except KeycloakPutError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @resources.route("//resources/", methods=["OPTIONS", "DELETE"])
- def delete_resource(client_id: str, resource_id: str):
- try:
- response = keycloak_client.delete_resource(resource_id, client_id)
- return response
- except KeycloakDeleteError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- @resources.route("/create-client", methods=["POST"])
- def create_client():
- payload = request.get_json()
- helper_text = """ The following fields are allowed:
-clientId*: String
-name: String
-description: String
-rootUrl: String
-adminUrl: String
-baseUrl: String
-surrogateAuthRequired: Boolean
-enabled: Boolean
-alwaysDisplayInConsole: Boolean
-clientAuthenticatorType: String
-secret: String
-registrationAccessToken: String
-defaultRoles: List of [string]
-redirectUris: List of [string]
-webOrigins: List of [string]
-notBefore: Integer
-bearerOnly: Boolean
-consentRequired: Boolean
-standardFlowEnabled: Boolean
-implicitFlowEnabled: Boolean
-directAccessGrantsEnabled: Boolean
-serviceAccountsEnabled: Boolean
-oauth2DeviceAuthorizationGrantEnabled: Boolean
-authorizationServicesEnabled: Boolean
-directGrantsOnly: Boolean
-publicClient: Boolean
-frontchannelLogout: Boolean
-protocol: String
-attributes: Map of [string]
-authenticationFlowBindingOverrides: Map of [string]
-fullScopeAllowed: Boolean
-nodeReRegistrationTimeout: Integer
-registeredNodes: Map of [integer]
-protocolMappers: List of ProtocolMapperRepresentation
-clientTemplate: String
-useTemplateConfig: Boolean
-useTemplateScope: Boolean
-useTemplateMappers: Boolean
-defaultClientScopes: List of [string]
-ClientScopes: List of [string]
-authorizationSettings: ResourceServerRepresentation
-access: Map of [boolean]
-origin: String
-resources: List of[Resource Representation]"""
- if 'clientId' not in payload:
- return custom_error("The field 'client_id' is mandatory", 400)
- if 'redirectUris' not in payload:
- payload['redirectUris'] = ['*']
- if 'standardFlowEnabled' not in payload:
- payload['standardFlowEnabled'] = True
- if 'protocol' not in payload:
- payload['protocol'] = 'openid-connect'
- if 'resources' in payload:
- resources = payload['resources']
- del payload['resources']
- keycloak_client.create_client(payload)
- return register_and_protect_resources(payload['clientId'], resources)
- try:
- return keycloak_client.create_client(payload)
- except KeycloakPostError as error:
- return custom_error(error.error_message, error.response_code)
- except:
- return custom_error("Unknown server error", 500)
-
- def custom_error(message, status_code):
- return message, status_code
-
- def _validate_register_resource(item):
- payload_minimum_example = """
- payload example ->
- [{
- "resource":{
- "name": "resource1",
- "uris": ["/resource1/", "/resource2/"]
- },
- "permissions": {
- "user": ["user1","user2"],
- }
- }]
- """
- time_options = """time must be a dictionary with one of:
- "notAfter":"1970-01-01 00:00:00"
- "notBefore":"1970-01-01 00:00:00"
- "dayMonth":
- "dayMonthEnd":
- "month":
- "monthEnd":
- "year":
- "yearEnd":
- "hour":
- "hourEnd":
- "minute":
- "minuteEnd":"""
-
- policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated']
- resource_accepted_fields = ['name','uris','attributes', 'ownerManagedAccess', 'resource_scopes', 'type']
- policy_accepted_fields = ['logic', 'decisionStrategy', 'name', 'description', 'groupsClaim', 'targetClaim']
- time_accepted_fields = ["notAfter","notBefore","dayMonth","dayMonthEnd","month","monthEnd","year","yearEnd","hour","hourEnd","minute","minuteEnd"]
- if 'resource' not in item:
- return 'Resource field required. ' + payload_minimum_example
- if 'permissions' not in item or item['permissions'] == {}:
- return 'Permissions field required. ' + payload_minimum_example
- if 'name' not in item['resource']:
- return 'Resource name required. '+ payload_minimum_example
- if 'uris' not in item['resource']:
- return 'Resource uris required. '+ payload_minimum_example
- for resource_key in item['resource']:
- if resource_key in resource_accepted_fields:
- continue
- else:
- return 'There are fields not accepted in "resource"'
-
- for key in item['permissions']:
- if not isinstance(item['permissions'][key], list) and not isinstance(item['permissions'][key], dict):
- return "The value of {} ".format(key) + "must be a list of strings or a dictionary"
- if key not in policy_types:
- return 'Permissions type not found. Needs to be one of the following: ' + ', '.join(policy_types)
- if key == 'time':
- if not isinstance(item['permissions']['time'], dict):
- return time_options
- for time_key in item['permissions']['time']:
- if time_key in time_accepted_fields or time_key in policy_accepted_fields:
- continue
- else:
- return 'There are fields not accepted or ' + time_options
- if key == 'regex':
- if not isinstance(item['permissions'][key], dict):
- return 'Regex must be a dictionary like {"pattern":}'
- for regex_key in item['permissions'][key]:
- if regex_key == 'pattern' or regex_key in policy_accepted_fields:
- continue
- else:
- return 'The field "pattern" is not in the regex dictionary or there are fields not accepted'
- if key == 'user':
- if not isinstance(item['permissions'][key], list):
- for user_key in item['permissions'][key]:
- if user_key == 'users' or user_key in policy_accepted_fields:
- continue
- else:
- return 'The field "users" is not in the user dictionary or there are fields not accepted'
- if key == 'role':
- if not isinstance(item['permissions'][key], list):
- for role_key in item['permissions'][key]:
- if role_key == 'roles' or role_key in policy_accepted_fields:
- continue
- else:
- return 'The field "roles" is not in the role dictionary or there are fields not accepted'
- if key == 'group':
- if not isinstance(item['permissions'][key], list):
- for group_key in item['permissions'][key]:
- if group_key == 'groups' or group_key in policy_accepted_fields:
- continue
- else:
- return 'The field "groups" is not in the group dictionary or there are fields not accepted'
- if key == 'client-scope':
- if not isinstance(item['permissions'][key], list):
- for client_scope_key in item['permissions'][key]:
- if client_scope_key in policy_accepted_fields:
- continue
- else:
- return 'There are fields not accepted'
-
- if key == 'aggregated':
- if not isinstance(item['permissions'][key], list):
- for aggregated_key in item['permissions'][key]:
- if aggregated_key == 'policies' or aggregated_key in policy_accepted_fields:
- continue
- else:
- return 'The field "policies" is not in the aggregated dictionary or there are fields not accepted'
-
- if key == 'client':
- if not isinstance(item['permissions'][key], list):
- for client_key in item['permissions'][key]:
- if client_key in policy_accepted_fields:
- continue
- else:
- return 'There are fields not accepted'
-
- return None
-
- return resources
\ No newline at end of file
diff --git a/travis/acceptanceTest.sh b/travis/acceptanceTest.sh
deleted file mode 100755
index 5439bdd..0000000
--- a/travis/acceptanceTest.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env bash
-
-# fail fast settings from https://dougrichardson.org/2018/08/03/fail-fast-bash-scripting.html
-set -euov pipefail
-
-# Check presence of environment variables
-TRAVIS_BUILD_NUMBER="${TRAVIS_BUILD_NUMBER:-0}"
-
-buildTag=travis_$TRAVIS_BUILD_NUMBER # We use a temporary build number for tagging, since this is a transient artefact
-
-docker run --rm -d -p $2:$3 --name $1 eoepca/$1:${buildTag} # Runs container from EOEPCA repository
-
-sleep 15 # wait until the container is running
-
-# INSERT BELOW THE ACCEPTANCE TEST:
-#curl -s http://localhost:$2/search # trivial smoke test
diff --git a/travis/containerCreation.sh b/travis/containerCreation.sh
deleted file mode 100755
index cfd5f00..0000000
--- a/travis/containerCreation.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env bash
-
-# fail fast settings from https://dougrichardson.org/2018/08/03/fail-fast-bash-scripting.html
-set -euov pipefail
-
-
-# Check presence of environment variables
-TRAVIS_BUILD_NUMBER="${TRAVIS_BUILD_NUMBER:-0}"
-
-# Create a Docker image and tag it as 'travis_'
-buildTag=travis_$TRAVIS_BUILD_NUMBER # We use a temporary build number for tagging, since this is a transient artefact
-
-docker build -t eoepca/$1 .
-docker tag eoepca/$1 eoepca/$1:$buildTag
-
-echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
-
-docker push eoepca/$1:$buildTag # defaults to docker hub EOEPCA repo
-
diff --git a/travis/release.sh b/travis/release.sh
deleted file mode 100755
index 68ef4ae..0000000
--- a/travis/release.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env bash
-
-# fail fast settings from https://dougrichardson.org/2018/08/03/fail-fast-bash-scripting.html
-set -euov pipefail
-
-# Check presence of environment variables
-TRAVIS_BUILD_NUMBER="${TRAVIS_BUILD_NUMBER:-0}"
-buildTag=travis_$TRAVIS_BUILD_NUMBER # We use a temporary build number for tagging, since this is a transient artefact
-
-echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
-docker pull eoepca/$1:$buildTag # have to pull locally in order to tag as a release
-
-# Tag and push as a Release
-docker tag eoepca/$1:$buildTag eoepca/$1:release_$TRAVIS_BUILD_NUMBER
-docker push eoepca/$1:release_$TRAVIS_BUILD_NUMBER
-
-# Tag and push as `latest`
-docker tag eoepca/$1:$buildTag eoepca/$1:latest
-docker push eoepca/$1:latest