From d37b4dfa6a7e39c6372743f6aad317638c8bb97e Mon Sep 17 00:00:00 2001 From: Evilham Date: Sun, 11 Dec 2022 19:02:27 +0100 Subject: [PATCH] [dd-sso] Add API documentation The API spec file can be generated with: python -m admin.views.test.test_ApiViews --generate-spec From the admin development environment. A simple testing ground that serves the Swagger UI can also be started with: python -m admin.views.test.test_ApiViews --- dd-sso/admin/Pipfile | 1 + dd-sso/admin/Pipfile.lock | 73 ++- dd-sso/admin/docker/requirements.pip3 | 1 + dd-sso/admin/src/admin/views/ApiViews.py | 71 ++- .../src/admin/views/api_docs/group_delete.yml | 18 + .../src/admin/views/api_docs/group_get.yml | 20 + .../src/admin/views/api_docs/group_new.yml | 30 + .../admin/src/admin/views/api_docs/groups.yml | 34 ++ .../src/admin/views/api_docs/role_users.yml | 31 + .../admin/src/admin/views/api_docs/roles.yml | 31 + .../src/admin/views/api_docs/user_delete.yml | 18 + .../src/admin/views/api_docs/user_get.yml | 20 + .../src/admin/views/api_docs/user_new.yml | 63 ++ .../src/admin/views/api_docs/user_put.yml | 49 ++ .../admin/src/admin/views/api_docs/users.yml | 58 ++ .../src/admin/views/api_docs/users_filter.yml | 21 + .../src/admin/views/test/test_ApiViews.py | 24 + docs/customising.ca.md | 5 + docs/ddapi.json | 554 ++++++++++++++++++ docs/integrations.ca.md | 63 ++ mkdocs.yml | 1 + 21 files changed, 1174 insertions(+), 12 deletions(-) create mode 100644 dd-sso/admin/src/admin/views/api_docs/group_delete.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/group_get.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/group_new.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/groups.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/role_users.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/roles.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/user_delete.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/user_get.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/user_new.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/user_put.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/users.yml create mode 100644 dd-sso/admin/src/admin/views/api_docs/users_filter.yml create mode 100644 docs/ddapi.json create mode 100644 docs/integrations.ca.md diff --git a/dd-sso/admin/Pipfile b/dd-sso/admin/Pipfile index 9618867..4432f2b 100644 --- a/dd-sso/admin/Pipfile +++ b/dd-sso/admin/Pipfile @@ -21,6 +21,7 @@ requests = "*" python-keycloak = "*" attrs = "*" cryptography = "*" +flasgger = "*" [dev-packages] mypy = "*" diff --git a/dd-sso/admin/Pipfile.lock b/dd-sso/admin/Pipfile.lock index 6d392e9..ebce04a 100644 --- a/dd-sso/admin/Pipfile.lock +++ b/dd-sso/admin/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "cd4a56afb09ac033e44f2b8c075a8103f5ee27b31b766508441e34539f654ea1" + "sha256": "7ce3de9caf3a9fcc47859dc03ad9e09db96185bd6be89480c7264ce71f6e80ca" }, "pipfile-spec": 6, "requires": { @@ -185,7 +185,7 @@ "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" ], - "markers": "python_version >= '3.6' and python_version < '4'", + "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==2.2.1" }, "ecdsa": { @@ -204,6 +204,14 @@ "index": "pypi", "version": "==0.33.2" }, + "flasgger": { + "hashes": [ + "sha256:0603941cf4003626b4ee551ca87331f1d17b8eecce500ccf1a1f1d3a332fc94a", + "sha256:6ebea406b5beecd77e8da42550f380d4d05a6107bc90b69ce9e77aee7612e2d0" + ], + "index": "pypi", + "version": "==0.9.5" + }, "flask": { "hashes": [ "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b", @@ -310,6 +318,14 @@ "markers": "python_version < '3.10'", "version": "==5.1.0" }, + "importlib-resources": { + "hashes": [ + "sha256:32bb095bda29741f6ef0e5278c42df98d135391bee5f932841efc0041f748dc3", + "sha256:c09b067d82e72c66f4f8eb12332f5efbebc9b007c0b6c40818108c9870adc363" + ], + "markers": "python_version < '3.9'", + "version": "==5.10.1" + }, "itsdangerous": { "hashes": [ "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", @@ -326,6 +342,14 @@ "markers": "python_version >= '3.7'", "version": "==3.1.2" }, + "jsonschema": { + "hashes": [ + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + ], + "markers": "python_version >= '3.7'", + "version": "==4.17.3" + }, "markupsafe": { "hashes": [ "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", @@ -380,6 +404,13 @@ "index": "pypi", "version": "==7.1.12" }, + "mistune": { + "hashes": [ + "sha256:182cc5ee6f8ed1b807de6b7bb50155df7b66495412836b9a74c8fbdfc75fe36d", + "sha256:9ee0a66053e2267aba772c71e06891fa8f1af6d4b01d5e84e267b4570d4d9808" + ], + "version": "==2.0.4" + }, "mysql-connector-python": { "hashes": [ "sha256:02526f16eacc3961ff681c5c8455d2306a9b45124f2f012ca75a1eac9ceb5165", @@ -479,6 +510,14 @@ "index": "pypi", "version": "==9.3.0" }, + "pkgutil-resolve-name": { + "hashes": [ + "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174", + "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e" + ], + "markers": "python_version < '3.9'", + "version": "==1.3.10" + }, "protobuf": { "hashes": [ "sha256:06059eb6953ff01e56a25cd02cca1a9649a75a7e65397b5b9b4e929ed71d10cf", @@ -553,6 +592,34 @@ ], "version": "==2.21" }, + "pyrsistent": { + "hashes": [ + "sha256:055ab45d5911d7cae397dc418808d8802fb95262751872c841c170b0dbf51eed", + "sha256:111156137b2e71f3a9936baf27cb322e8024dac3dc54ec7fb9f0bcf3249e68bb", + "sha256:187d5730b0507d9285a96fca9716310d572e5464cadd19f22b63a6976254d77a", + "sha256:21455e2b16000440e896ab99e8304617151981ed40c29e9507ef1c2e4314ee95", + "sha256:2aede922a488861de0ad00c7630a6e2d57e8023e4be72d9d7147a9fcd2d30712", + "sha256:3ba4134a3ff0fc7ad225b6b457d1309f4698108fb6b35532d015dca8f5abed73", + "sha256:456cb30ca8bff00596519f2c53e42c245c09e1a4543945703acd4312949bfd41", + "sha256:71d332b0320642b3261e9fee47ab9e65872c2bd90260e5d225dabeed93cbd42b", + "sha256:879b4c2f4d41585c42df4d7654ddffff1239dc4065bc88b745f0341828b83e78", + "sha256:9cd3e9978d12b5d99cbdc727a3022da0430ad007dacf33d0bf554b96427f33ab", + "sha256:a178209e2df710e3f142cbd05313ba0c5ebed0a55d78d9945ac7a4e09d923308", + "sha256:b39725209e06759217d1ac5fcdb510e98670af9e37223985f330b611f62e7425", + "sha256:bfa0351be89c9fcbcb8c9879b826f4353be10f58f8a677efab0c017bf7137ec2", + "sha256:bfd880614c6237243ff53a0539f1cb26987a6dc8ac6e66e0c5a40617296a045e", + "sha256:c43bec251bbd10e3cb58ced80609c5c1eb238da9ca78b964aea410fb820d00d6", + "sha256:d690b18ac4b3e3cab73b0b7aa7dbe65978a172ff94970ff98d82f2031f8971c2", + "sha256:d6982b5a0237e1b7d876b60265564648a69b14017f3b5f908c5be2de3f9abb7a", + "sha256:dec3eac7549869365fe263831f576c8457f6c833937c68542d08fde73457d291", + "sha256:e371b844cec09d8dc424d940e54bba8f67a03ebea20ff7b7b0d56f526c71d584", + "sha256:e5d8f84d81e3729c3b506657dddfe46e8ba9c330bf1858ee33108f8bb2adb38a", + "sha256:ea6b79a02a28550c98b6ca9c35b9f492beaa54d7c5c9e9949555893c8a9234d0", + "sha256:f1258f4e6c42ad0b20f9cfcc3ada5bd6b83374516cd01c0960e3cb75fdca6770" + ], + "markers": "python_version >= '3.7'", + "version": "==0.19.2" + }, "python-engineio": { "hashes": [ "sha256:7454314a529bba20e745928601ffeaf101c1b5aca9a6c4e48ad397803d10ea0c", @@ -651,7 +718,7 @@ "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21" ], - "markers": "python_version >= '3.6' and python_version < '4'", + "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==4.9" }, "schema": { diff --git a/dd-sso/admin/docker/requirements.pip3 b/dd-sso/admin/docker/requirements.pip3 index 30e62ed..deaf8bd 100644 --- a/dd-sso/admin/docker/requirements.pip3 +++ b/dd-sso/admin/docker/requirements.pip3 @@ -23,6 +23,7 @@ Flask==2.1.3 Flask-Login==0.6.2 eventlet==0.33.1 Flask-SocketIO==5.2.0 +flasgger==0.9.5 bcrypt==3.2.2 # diceware can't be upgraded without issues diceware==0.9.6 diff --git a/dd-sso/admin/src/admin/views/ApiViews.py b/dd-sso/admin/src/admin/views/ApiViews.py index d9ebd1c..71378c4 100644 --- a/dd-sso/admin/src/admin/views/ApiViews.py +++ b/dd-sso/admin/src/admin/views/ApiViews.py @@ -21,10 +21,13 @@ import copy import json import logging as log +import os import traceback from operator import itemgetter from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional +from flasgger import Swagger +from flasgger.utils import swag_from from flask import request if TYPE_CHECKING: @@ -66,8 +69,34 @@ ERR_412 = ( def setup_api_views(app: "AdminFlaskApp") -> None: + swagger = Swagger( + app, + template={ + "info": { + "title": "DD API", + "description": "DD API for external integrations", + "version": "2022.11.0", + "termsOfService": "", + }, + "externalDocs": { + "description": "Online Documentation", + "url": "https://dd.digitalitzacio-democratica.xnet-x.net/docs/", + }, + "securityDefinitions": { + "dd_jwt": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "JWS token using API_SECRET (e.g. 'bearer X.Y')", + } + }, + "security": {"dd_jwt": {"$ref": "#/securityDefinitions/dd_jwt"}}, + "swagger_ui": bool(os.environ.get("SWAGGER_UI", "")), + }, + ) # LISTS - @app.json_route("/ddapi/users", methods=["GET"]) + @app.json_route("/ddapi/users", methods=["GET"], endpoint="api_users") + @swag_from("api_docs/users.yml", endpoint="api_users") @has_token def ddapi_users() -> OptionalJsonResponse: try: @@ -84,7 +113,10 @@ def setup_api_views(app: "AdminFlaskApp") -> None: return ERR_500 return None - @app.json_route("/ddapi/users/filter", methods=["POST"]) + @app.json_route( + "/ddapi/users/filter", methods=["POST"], endpoint="api_users_filter" + ) + @swag_from("api_docs/users_filter.yml", endpoint="api_users_filter") @has_token def ddapi_users_search() -> OptionalJsonResponse: try: @@ -110,7 +142,8 @@ def setup_api_views(app: "AdminFlaskApp") -> None: return ERR_500 return None - @app.json_route("/ddapi/groups", methods=["GET"]) + @app.json_route("/ddapi/groups", methods=["GET"], endpoint="api_groups") + @swag_from("api_docs/groups.yml", endpoint="api_groups") @has_token def ddapi_groups() -> OptionalJsonResponse: try: @@ -127,7 +160,8 @@ def setup_api_views(app: "AdminFlaskApp") -> None: return ERR_500 return None - @app.json_route("/ddapi/roles", methods=["GET"]) + @app.json_route("/ddapi/roles", methods=["GET"], endpoint="api_roles") + @swag_from("api_docs/roles.yml", endpoint="api_roles") @has_token def ddapi_roles() -> OptionalJsonResponse: try: @@ -141,7 +175,8 @@ def setup_api_views(app: "AdminFlaskApp") -> None: return ERR_500 return None - @app.json_route("/ddapi/role/users", methods=["POST"]) + @app.json_route("/ddapi/role/users", methods=["POST"], endpoint="api_role_users") + @swag_from("api_docs/role_users.yml", endpoint="api_role_users") @has_token def ddapi_role_users() -> OptionalJsonResponse: try: @@ -174,8 +209,16 @@ def setup_api_views(app: "AdminFlaskApp") -> None: return None # INDIVIDUAL ACTIONS - @app.json_route("/ddapi/user", methods=["POST"]) - @app.json_route("/ddapi/user/", methods=["PUT", "GET", "DELETE"]) + @app.json_route("/ddapi/user", methods=["POST"], endpoint="api_user_new") + @app.json_route( + "/ddapi/user/", + methods=["PUT", "GET", "DELETE"], + endpoint="api_user_ddid", + ) + @swag_from("api_docs/user_new.yml", endpoint="api_user_new") + @swag_from("api_docs/user_get.yml", endpoint="api_user_ddid", methods=["GET"]) + @swag_from("api_docs/user_put.yml", endpoint="api_user_ddid", methods=["PUT"]) + @swag_from("api_docs/user_delete.yml", endpoint="api_user_ddid", methods=["DELETE"]) @has_token def ddapi_user(user_ddid: Optional[str] = None) -> OptionalJsonResponse: try: @@ -284,8 +327,18 @@ def setup_api_views(app: "AdminFlaskApp") -> None: log.error(traceback.format_exc()) return ERR_500 - @app.json_route("/ddapi/group", methods=["POST"]) - @app.json_route("/ddapi/group/", methods=["GET", "POST", "DELETE"]) + @app.json_route("/ddapi/group", methods=["POST"], endpoint="api_group_new") + @app.json_route( + "/ddapi/group/", + methods=["GET", "POST", "DELETE"], + endpoint="api_group_group_id", + ) + @swag_from("api_docs/group_new.yml", endpoint="api_group_new") + @swag_from("api_docs/group_get.yml", endpoint="api_group_group_id", methods=["GET"]) + # @swag_from('api_docs/group_put.yml', endpoint='api_group_group_id', methods=["PUT"]) + @swag_from( + "api_docs/group_delete.yml", endpoint="api_group_group_id", methods=["DELETE"] + ) # @app.json_route("/api/group/", methods=["PUT", "GET", "DELETE"]) @has_token def ddapi_group(group_id: Optional[str] = None) -> OptionalJsonResponse: diff --git a/dd-sso/admin/src/admin/views/api_docs/group_delete.yml b/dd-sso/admin/src/admin/views/api_docs/group_delete.yml new file mode 100644 index 0000000..56e2ce4 --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/group_delete.yml @@ -0,0 +1,18 @@ +Delete a registered group in DD +--- +consumes: + - application/json +parameters: + - in: url + name: group_id + description: | + The group to delete from DD + schema: + type: string +responses: + 200: + schema: + type: object + 404: + description: | + The group does not exist diff --git a/dd-sso/admin/src/admin/views/api_docs/group_get.yml b/dd-sso/admin/src/admin/views/api_docs/group_get.yml new file mode 100644 index 0000000..7c41a8a --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/group_get.yml @@ -0,0 +1,20 @@ +Get a registered group in DD +--- +consumes: + - application/json +parameters: + - in: url + name: group_id + description: | + The group to retrieve from DD + schema: + type: string +responses: + 200: + description: | + The group as it exists on DD + schema: + $ref: '#/definitions/Group' + 404: + description: | + The group does not exist diff --git a/dd-sso/admin/src/admin/views/api_docs/group_new.yml b/dd-sso/admin/src/admin/views/api_docs/group_new.yml new file mode 100644 index 0000000..a9e85b4 --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/group_new.yml @@ -0,0 +1,30 @@ +Register a new group in DD +--- +consumes: + - application/json +parameters: + - in: body + name: group + description: | + The group to be registered on DD. + schema: + type: object + properties: + name: + required: True + type: string + description: + required: False + type: string + parent: + required: False + type: string +responses: + 200: + description: | + The keycloak_id of the newly registered group + schema: + $ref: '#/definitions/KeycloakId' + 409: + description: | + The group already exists diff --git a/dd-sso/admin/src/admin/views/api_docs/groups.yml b/dd-sso/admin/src/admin/views/api_docs/groups.yml new file mode 100644 index 0000000..4fd76a2 --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/groups.yml @@ -0,0 +1,34 @@ +List all registered groups on DD. +--- +definitions: + Group: + type: object + properties: + keycloak_id: + type: string + format: uuid + id: + type: string + name: + type: string + path: + type: string + description: + type: string + Groups: + type: array + items: + $ref: '#/definitions/Group' +responses: + 200: + description: The list of groups registered on DD + schema: + $ref: '#/definitions/Groups' + examples: | + [{ + "keycloak_id": "f6ec2bda-bec9-415f-bcb7-f5ae644bfec5", + "id": "ID", + "name": "NAME", + "path": "PATH", + "description": "DESCRIPITON", + }] diff --git a/dd-sso/admin/src/admin/views/api_docs/role_users.yml b/dd-sso/admin/src/admin/views/api_docs/role_users.yml new file mode 100644 index 0000000..01bbc65 --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/role_users.yml @@ -0,0 +1,31 @@ +List registered users on DD with a the given role. +--- +consumes: + - application/json +parameters: + - in: body + name: role + description: | + The role to search users registered on DD. + One of 'id', 'name' and 'keycloak_id' must be provided. + This is also the order in which the parameters are checked, + in case multiple are provided. + schema: + type: object + properties: + id: + type: string + required: False + name: + type: string + required: False + keycloak_id: + type: string + format: uuid + required: False +responses: + 200: + description: | + The list of users registered on DD with the filter applied. + schema: + $ref: '#/definitions/Users' diff --git a/dd-sso/admin/src/admin/views/api_docs/roles.yml b/dd-sso/admin/src/admin/views/api_docs/roles.yml new file mode 100644 index 0000000..0d66abd --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/roles.yml @@ -0,0 +1,31 @@ +List all roles configured on DD. +--- +definitions: + Role: + type: object + properties: + keycloak_id: + type: string + format: uuid + id: + type: string + name: + type: string + description: + type: string + Roles: + type: array + items: + $ref: '#/definitions/Role' +responses: + 200: + description: The list of roles configured on DD + schema: + $ref: '#/definitions/Roles' + examples: | + [{ + "keycloak_id": "f6ec2bda-bec9-415f-bcb7-f5ae644bfec5", + "id": "ID", + "name": "NAME", + "description": "DESCRIPITON", + }] diff --git a/dd-sso/admin/src/admin/views/api_docs/user_delete.yml b/dd-sso/admin/src/admin/views/api_docs/user_delete.yml new file mode 100644 index 0000000..0b3eeb9 --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/user_delete.yml @@ -0,0 +1,18 @@ +Delete a registered user in DD +--- +consumes: + - application/json +parameters: + - in: url + name: user_ddid + description: | + The user to delete from DD + schema: + type: string +responses: + 200: + schema: + type: object + 404: + description: | + The user does not exist diff --git a/dd-sso/admin/src/admin/views/api_docs/user_get.yml b/dd-sso/admin/src/admin/views/api_docs/user_get.yml new file mode 100644 index 0000000..3e131d7 --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/user_get.yml @@ -0,0 +1,20 @@ +Get a registered user in DD +--- +consumes: + - application/json +parameters: + - in: url + name: user_ddid + description: | + The user to retrieve from DD + schema: + type: string +responses: + 200: + description: | + The user as it exists on DD + schema: + $ref: '#/definitions/User' + 404: + description: | + The user does not exist diff --git a/dd-sso/admin/src/admin/views/api_docs/user_new.yml b/dd-sso/admin/src/admin/views/api_docs/user_new.yml new file mode 100644 index 0000000..9cf8a64 --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/user_new.yml @@ -0,0 +1,63 @@ +Register a new user in DD +--- +definitions: + KeycloakId: + type: object + properties: + keycloak_id: + required: True + type: string +consumes: + - application/json +parameters: + - in: body + name: user + description: | + The user to be registered on DD. + schema: + type: object + properties: + username: + required: True + type: string + first: + required: True + type: string + last: + required: True + type: string + email: + required: True + type: string + format: email + password: + required: True + type: string + format: email + password_temporary: + required: False + type: bool + quota: + required: True + type: string + enabled: + required: True + type: bool + role: + required: True + groups: + required: True + type: array + items: + type: string +responses: + 200: + description: | + The keycloak_id of the newly registered user + schema: + $ref: '#/definitions/KeycloakId' + examples: | + { "keycloak_id": "f6ec2bda-bec9-415f-bcb7-f5ae644bfec5" } + 409: + description: | + The user already exists diff --git a/dd-sso/admin/src/admin/views/api_docs/user_put.yml b/dd-sso/admin/src/admin/views/api_docs/user_put.yml new file mode 100644 index 0000000..3906bfd --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/user_put.yml @@ -0,0 +1,49 @@ +Modify a user in DD +--- +consumes: + - application/json +parameters: + - in: body + name: user + description: | + The user to be modified on DD. + schema: + type: object + properties: + first: + required: False + type: string + last: + required: False + type: string + email: + required: False + type: string + format: email + password: + required: False + type: string + format: email + password_temporary: + required: False + type: bool + quota: + required: False + type: string + enabled: + required: False + type: bool + role: + required: False + groups: + required: False + type: array + items: + type: string +responses: + 200: + schema: + type: object + 404: + description: | + The user does not exist diff --git a/dd-sso/admin/src/admin/views/api_docs/users.yml b/dd-sso/admin/src/admin/views/api_docs/users.yml new file mode 100644 index 0000000..0f95a56 --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/users.yml @@ -0,0 +1,58 @@ +List all registered users on DD. +--- +definitions: + User: + type: object + properties: + keycloak_id: + type: string + format: uuid + id: + type: string + username: + type: string + enabled: + type: boolean + first: + type: string + last: + type: string + role: + type: string + email: + type: string + fomat: email + groups: + type: array + items: + type: string + quota: + type: string + quota_used_bytes: + type: string + Users: + type: array + items: + $ref: '#/definitions/User' +responses: + 200: + description: The list of users registered on DD + schema: + $ref: '#/definitions/Users' + examples: | + [{ + "keycloak_id": "a773d249-a113-4542-8101-3a50f4cd28c2", + "id": "ID", + "username": "ID", + "enabled": true, + "first": "NAME", + "last": "LASTNAME", + "role": "student", + "email": "ID@DOMAIN", + "groups": [ + "GRUP", + "student" + ], + "quota": "500 MB", + "quota_used_bytes": "0 MB" + }] diff --git a/dd-sso/admin/src/admin/views/api_docs/users_filter.yml b/dd-sso/admin/src/admin/views/api_docs/users_filter.yml new file mode 100644 index 0000000..0331d85 --- /dev/null +++ b/dd-sso/admin/src/admin/views/api_docs/users_filter.yml @@ -0,0 +1,21 @@ +List registered users on DD with a filter applied. +--- +consumes: + - application/json +parameters: + - in: body + name: filter + description: The filter to apply to users registered on DD + schema: + type: object + required: + - filter + properties: + text: + type: string +responses: + 200: + description: | + The list of users registered on DD with the filter applied. + schema: + $ref: '#/definitions/Users' diff --git a/dd-sso/admin/src/admin/views/test/test_ApiViews.py b/dd-sso/admin/src/admin/views/test/test_ApiViews.py index 8b98d76..80b1941 100644 --- a/dd-sso/admin/src/admin/views/test/test_ApiViews.py +++ b/dd-sso/admin/src/admin/views/test/test_ApiViews.py @@ -553,3 +553,27 @@ class ApiViewsTests(flask_unittest.ClientTestCase): # rv = self._r(client, "/ddapi/role/users", method="POST") # print(rv) # print(rv.json) + + +if __name__ == "__main__": + import sys + + if "DOMAIN" not in os.environ: + os.environ["DOMAIN"] = "localhost" + os.environ["SWAGGER_UI"] = "TRUE" + app = _testApp() + if "--generate-spec" in sys.argv: + with app.app_context(): + ep = app.swag.config["specs"][0]["endpoint"] + spec = app.swag.get_apispecs(ep) + import json + + print(json.dumps(spec, indent=4)) + else: + # Start a simple testing server + app.socketio.run( + app, + host=os.environ.get("ADMIN_LISTEN_ADDESS", "::"), + port=int(os.environ.get("ADMIN_LISTEN_PORT", "9000")), + debug=True, + ) diff --git a/docs/customising.ca.md b/docs/customising.ca.md index 407f365..f32dd28 100644 --- a/docs/customising.ca.md +++ b/docs/customising.ca.md @@ -77,3 +77,8 @@ Un cop fet això, a la interfície d'administració de Keycloak haurem de triar > **Nota:** el directori dd-custom no s'actualitzarà mai, és responsabilitat > vostra revisar els canvis al tema `dd` i al directori `dd-custom.sample` > per tal de mantenir la compatibilitat amb els vostres canvis. + +## Integració amb altres eines + +És possible integrar el DD amb altres eines, vegeu la secció +d'[integracions](integrations.ca.md). diff --git a/docs/ddapi.json b/docs/ddapi.json new file mode 100644 index 0000000..db0d3d2 --- /dev/null +++ b/docs/ddapi.json @@ -0,0 +1,554 @@ +{ + "info": { + "title": "DD API", + "description": "DD API for external integrations", + "version": "2022.11.0", + "termsOfService": "" + }, + "paths": { + "/ddapi/users": { + "get": { + "summary": "List all registered users on DD.", + "responses": { + "200": { + "description": "The list of users registered on DD", + "schema": { + "$ref": "#/definitions/Users" + }, + "examples": "[{\n \"keycloak_id\": \"a773d249-a113-4542-8101-3a50f4cd28c2\",\n \"id\": \"ID\",\n \"username\": \"ID\",\n \"enabled\": true,\n \"first\": \"NAME\",\n \"last\": \"LASTNAME\",\n \"role\": \"student\",\n \"email\": \"ID@DOMAIN\",\n \"groups\": [\n \"GRUP\",\n \"student\"\n ],\n \"quota\": \"500 MB\",\n \"quota_used_bytes\": \"0 MB\"\n}]\n" + } + } + } + }, + "/ddapi/users/filter": { + "post": { + "summary": "List registered users on DD with a filter applied.", + "responses": { + "200": { + "description": "The list of users registered on DD with the filter applied.\n", + "schema": { + "$ref": "#/definitions/Users" + } + } + }, + "parameters": [ + { + "in": "body", + "name": "filter", + "description": "The filter to apply to users registered on DD", + "schema": { + "type": "object", + "required": [ + "filter" + ], + "properties": { + "text": { + "type": "string" + } + } + } + } + ], + "consumes": [ + "application/json" + ] + } + }, + "/ddapi/groups": { + "get": { + "summary": "List all registered groups on DD.", + "responses": { + "200": { + "description": "The list of groups registered on DD", + "schema": { + "$ref": "#/definitions/Groups" + }, + "examples": "[{\n \"keycloak_id\": \"f6ec2bda-bec9-415f-bcb7-f5ae644bfec5\",\n \"id\": \"ID\",\n \"name\": \"NAME\",\n \"path\": \"PATH\",\n \"description\": \"DESCRIPITON\",\n}]\n" + } + } + } + }, + "/ddapi/roles": { + "get": { + "summary": "List all roles configured on DD.", + "responses": { + "200": { + "description": "The list of roles configured on DD", + "schema": { + "$ref": "#/definitions/Roles" + }, + "examples": "[{\n \"keycloak_id\": \"f6ec2bda-bec9-415f-bcb7-f5ae644bfec5\",\n \"id\": \"ID\",\n \"name\": \"NAME\",\n \"description\": \"DESCRIPITON\",\n}]\n" + } + } + } + }, + "/ddapi/role/users": { + "post": { + "summary": "List registered users on DD with a the given role.", + "responses": { + "200": { + "description": "The list of users registered on DD with the filter applied.\n", + "schema": { + "$ref": "#/definitions/Users" + } + } + }, + "parameters": [ + { + "in": "body", + "name": "role", + "description": "The role to search users registered on DD.\nOne of 'id', 'name' and 'keycloak_id' must be provided.\nThis is also the order in which the parameters are checked,\nin case multiple are provided.\n", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "keycloak_id": { + "type": "string", + "format": "uuid", + "required": false + } + } + } + } + ], + "consumes": [ + "application/json" + ] + } + }, + "/ddapi/user/{user_ddid}": { + "get": { + "summary": "Get a registered user in DD", + "responses": { + "200": { + "description": "The user as it exists on DD\n", + "schema": { + "$ref": "#/definitions/User" + } + }, + "404": { + "description": "The user does not exist\n" + } + }, + "parameters": [ + { + "in": "url", + "name": "user_ddid", + "description": "The user to retrieve from DD\n", + "schema": { + "type": "string" + } + } + ], + "consumes": [ + "application/json" + ] + }, + "delete": { + "summary": "Delete a registered user in DD", + "responses": { + "200": { + "schema": { + "type": "object" + } + }, + "404": { + "description": "The user does not exist\n" + } + }, + "parameters": [ + { + "in": "url", + "name": "user_ddid", + "description": "The user to delete from DD\n", + "schema": { + "type": "string" + } + } + ], + "consumes": [ + "application/json" + ] + }, + "put": { + "summary": "Modify a user in DD", + "responses": { + "200": { + "schema": { + "type": "object" + } + }, + "404": { + "description": "The user does not exist\n" + } + }, + "parameters": [ + { + "in": "body", + "name": "user", + "description": "The user to be modified on DD.\n", + "schema": { + "type": "object", + "properties": { + "first": { + "required": false, + "type": "string" + }, + "last": { + "required": false, + "type": "string" + }, + "email": { + "required": false, + "type": "string", + "format": "email" + }, + "password": { + "required": false, + "type": "string", + "format": "email" + }, + "password_temporary": { + "required": false, + "type": "bool" + }, + "quota": { + "required": false, + "type": "string" + }, + "enabled": { + "required": false, + "type": "bool" + }, + "role": { + "required": false + }, + "groups": { + "required": false, + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + ], + "consumes": [ + "application/json" + ] + } + }, + "/ddapi/user": { + "post": { + "summary": "Register a new user in DD", + "responses": { + "200": { + "description": "The keycloak_id of the newly registered user\n", + "schema": { + "$ref": "#/definitions/KeycloakId" + }, + "examples": "{ \"keycloak_id\": \"f6ec2bda-bec9-415f-bcb7-f5ae644bfec5\" }\n" + }, + "409": { + "description": "The user already exists\n" + } + }, + "parameters": [ + { + "in": "body", + "name": "user", + "description": "The user to be registered on DD.\n", + "schema": { + "type": "object", + "properties": { + "username": { + "required": true, + "type": "string" + }, + "first": { + "required": true, + "type": "string" + }, + "last": { + "required": true, + "type": "string" + }, + "email": { + "required": true, + "type": "string", + "format": "email" + }, + "password": { + "required": true, + "type": "string", + "format": "email" + }, + "password_temporary": { + "required": false, + "type": "bool" + }, + "quota": { + "required": true, + "type": "string" + }, + "enabled": { + "required": true, + "type": "bool" + }, + "role": { + "required": true + }, + "groups": { + "required": true, + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + ], + "consumes": [ + "application/json" + ] + } + }, + "/ddapi/group/{group_id}": { + "get": { + "summary": "Get a registered group in DD", + "responses": { + "200": { + "description": "The group as it exists on DD\n", + "schema": { + "$ref": "#/definitions/Group" + } + }, + "404": { + "description": "The group does not exist\n" + } + }, + "parameters": [ + { + "in": "url", + "name": "group_id", + "description": "The group to retrieve from DD\n", + "schema": { + "type": "string" + } + } + ], + "consumes": [ + "application/json" + ] + }, + "delete": { + "summary": "Delete a registered group in DD", + "responses": { + "200": { + "schema": { + "type": "object" + } + }, + "404": { + "description": "The group does not exist\n" + } + }, + "parameters": [ + { + "in": "url", + "name": "group_id", + "description": "The group to delete from DD\n", + "schema": { + "type": "string" + } + } + ], + "consumes": [ + "application/json" + ] + } + }, + "/ddapi/group": { + "post": { + "summary": "Register a new group in DD", + "responses": { + "200": { + "description": "The keycloak_id of the newly registered group\n", + "schema": { + "$ref": "#/definitions/KeycloakId" + } + }, + "409": { + "description": "The group already exists\n" + } + }, + "parameters": [ + { + "in": "body", + "name": "group", + "description": "The group to be registered on DD.\n", + "schema": { + "type": "object", + "properties": { + "name": { + "required": true, + "type": "string" + }, + "description": { + "required": false, + "type": "string" + }, + "parent": { + "required": false, + "type": "string" + } + } + } + } + ], + "consumes": [ + "application/json" + ] + } + } + }, + "definitions": { + "User": { + "type": "object", + "properties": { + "keycloak_id": { + "type": "string", + "format": "uuid" + }, + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "first": { + "type": "string" + }, + "last": { + "type": "string" + }, + "role": { + "type": "string" + }, + "email": { + "type": "string", + "fomat": "email" + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + }, + "quota": { + "type": "string" + }, + "quota_used_bytes": { + "type": "string" + } + } + }, + "Users": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + }, + "Group": { + "type": "object", + "properties": { + "keycloak_id": { + "type": "string", + "format": "uuid" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "Groups": { + "type": "array", + "items": { + "$ref": "#/definitions/Group" + } + }, + "Role": { + "type": "object", + "properties": { + "keycloak_id": { + "type": "string", + "format": "uuid" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "Roles": { + "type": "array", + "items": { + "$ref": "#/definitions/Role" + } + }, + "KeycloakId": { + "type": "object", + "properties": { + "keycloak_id": { + "required": true, + "type": "string" + } + } + } + }, + "swagger": "2.0", + "externalDocs": { + "description": "Online Documentation", + "url": "https://dd.digitalitzacio-democratica.xnet-x.net/docs/" + }, + "securityDefinitions": { + "dd_jwt": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "JWS token using API_SECRET (e.g. 'bearer X.Y')" + } + }, + "security": { + "dd_jwt": { + "$ref": "#/securityDefinitions/dd_jwt" + } + }, + "swagger_ui": true +} diff --git a/docs/integrations.ca.md b/docs/integrations.ca.md new file mode 100644 index 0000000..ce685e6 --- /dev/null +++ b/docs/integrations.ca.md @@ -0,0 +1,63 @@ +# Integracions + +El DD es pot integrar amb altres sistemes a través de les seves APIs. + +## Autenticació + +Totes les peticions han d'estar autenticades amb un [Json Web Token (JWT)][jwt], +que estigui signat per l'`API_SECRET` (present al fitxer `dd.conf`). + +Aquesta autenticació es fa mitjançant la capcelera HTTP `Authentication`. + +
+Vegeu-ne els detalls +```sh +> curl -H "Authorization: bearer ${jwt}" https://admin.DOMAIN/ddapi/roles +[ + { + "keycloak_id": "9325ad99-7e04-4c31-9768-5512e1564160", + "id": "admin", + "name": "admin", + "description": "${role_admin}" + }, + { + "keycloak_id": "c6c8a73e-51fc-4716-831d-1dfc0e0b62b0", + "id": "manager", + "name": "manager", + "description": "Realm managers" + }, + { + "keycloak_id": "24d7977e-da83-4591-8e13-0fac3126afa1", + "id": "student", + "name": "student", + "description": "Realm students" + }, + { + "keycloak_id": "d6699c41-13d5-4623-bdca-e5f2775474ed", + "id": "teacher", + "name": "teacher", + "description": "Realm teachers" + } +] +``` + +On el JWT es pot generar, per exemple fent servir python-jose, de la +següent manera: + +```python +import os +from jose import jws +t = jws.sign({}, os.environ["API_SECRET"], algorithm="HS256") +print(t) +``` + +Altres llenguatges de programació i llibreries tindran una manera anàloga de +generar aquests tokens. +
+ +[jwt]: https://jwt.io/ +[jose]: https://python-jose.readthedocs.io/ + +## API + +!!swagger ddapi.json!! diff --git a/mkdocs.yml b/mkdocs.yml index 06c896a..f3b62ad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,6 +42,7 @@ markdown_extensions: plugins: - search #- enumerate-headings + - render_swagger - i18n: languages: ca: "Català"