From 17cc2ddbde8d6a091bd57a370b9123e57bcf2af0 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 3 Jan 2025 22:47:23 +0000 Subject: [PATCH 01/10] Create a separate staff schema based on email domain and add support for impersonation header Signed-off-by: Federico Busetti <729029+febus982@users.noreply.github.com> --- .idea/dataSources.xml | 2 +- auth_volumes/kratos/identity.schema.json | 1 - auth_volumes/kratos/kratos.yml | 4 +- auth_volumes/kratos/staff.schema.json | 49 ++++++++++++++++++++++++ auth_volumes/oathkeeper/oathkeeper.yml | 5 +++ 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 auth_volumes/kratos/staff.schema.json diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index d52b407b..db9e40be 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -20,7 +20,7 @@ sqlite.xerial true org.sqlite.JDBC - jdbc:sqlite:$PROJECT_DIR$/auth/kratos/db.sqlite + jdbc:sqlite:$PROJECT_DIR$/auth_volumes/kratos/db.sqlite diff --git a/auth_volumes/kratos/identity.schema.json b/auth_volumes/kratos/identity.schema.json index 1a137875..d71cc3f1 100644 --- a/auth_volumes/kratos/identity.schema.json +++ b/auth_volumes/kratos/identity.schema.json @@ -1,5 +1,4 @@ { - "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Person", "type": "object", diff --git a/auth_volumes/kratos/kratos.yml b/auth_volumes/kratos/kratos.yml index ad9d1f04..22c7f5f2 100644 --- a/auth_volumes/kratos/kratos.yml +++ b/auth_volumes/kratos/kratos.yml @@ -107,8 +107,10 @@ hashers: cost: 8 identity: - default_schema_id: default + default_schema_id: staff schemas: + - id: staff + url: file:///etc/config/kratos/staff.schema.json - id: default url: file:///etc/config/kratos/identity.schema.json diff --git a/auth_volumes/kratos/staff.schema.json b/auth_volumes/kratos/staff.schema.json new file mode 100644 index 00000000..f050843d --- /dev/null +++ b/auth_volumes/kratos/staff.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Staff", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "pattern": "^.*@staffdomain\\.com$", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "name": { + "type": "object", + "properties": { + "first": { + "title": "First Name", + "type": "string" + }, + "last": { + "title": "Last Name", + "type": "string" + } + } + } + }, + "required": [ + "email" + ], + "additionalProperties": false + } + } +} diff --git a/auth_volumes/oathkeeper/oathkeeper.yml b/auth_volumes/oathkeeper/oathkeeper.yml index 26137b63..c94a87da 100644 --- a/auth_volumes/oathkeeper/oathkeeper.yml +++ b/auth_volumes/oathkeeper/oathkeeper.yml @@ -96,5 +96,10 @@ mutators: jwks_url: file:///etc/config/oathkeeper/id_token.jwks.json claims: | { + {{ if eq .Extra.identity.schema_id "staff" }} + {{ if .MatchContext.Header.Get "x-impersonate" }} + "impersonate": {{ .MatchContext.Header.Get "x-impersonate" | toJson }}, + {{ end }} + {{ end }} "session": {{ .Extra | toJson }} } From bd494da76c882d12b59edb3d219d646069d6ead2 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 3 Jan 2025 23:36:09 +0000 Subject: [PATCH 02/10] Add example web_hook configuration after registration Signed-off-by: Federico Busetti <729029+febus982@users.noreply.github.com> --- auth_volumes/kratos/kratos.yml | 17 ++++++++++++++++- auth_volumes/kratos/user_registered.jsonnet | 7 +++++++ src/http_app/routes/__init__.py | 3 ++- src/http_app/routes/user_registered_hook.py | 11 +++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 auth_volumes/kratos/user_registered.jsonnet create mode 100644 src/http_app/routes/user_registered_hook.py diff --git a/auth_volumes/kratos/kratos.yml b/auth_volumes/kratos/kratos.yml index 22c7f5f2..55671edd 100644 --- a/auth_volumes/kratos/kratos.yml +++ b/auth_volumes/kratos/kratos.yml @@ -85,6 +85,21 @@ selfservice: password: hooks: - hook: session + - hook: web_hook + config: + url: http://dev:8000/user_registered/ + method: "POST" +# headers: { } + # https://www.ory.sh/docs/guides/integrate-with-ory-cloud-through-webhooks#jsonnet-templating + body: file:///etc/config/kratos/user_registered.jsonnet + can_interrupt: false + emit_analytics_event: false +# auth: +# type: api_key +# config: +# name: "" +# value: "" +# in: header # - hook: show_verification_ui log: @@ -107,7 +122,7 @@ hashers: cost: 8 identity: - default_schema_id: staff + default_schema_id: default schemas: - id: staff url: file:///etc/config/kratos/staff.schema.json diff --git a/auth_volumes/kratos/user_registered.jsonnet b/auth_volumes/kratos/user_registered.jsonnet new file mode 100644 index 00000000..461429fa --- /dev/null +++ b/auth_volumes/kratos/user_registered.jsonnet @@ -0,0 +1,7 @@ +function(ctx) { + user_id: ctx.identity.id, + traits: { + email: ctx.identity.traits.email, + name: ctx.identity.traits.name, + } +} diff --git a/src/http_app/routes/__init__.py b/src/http_app/routes/__init__.py index 0ca50d64..f914c039 100644 --- a/src/http_app/routes/__init__.py +++ b/src/http_app/routes/__init__.py @@ -1,6 +1,6 @@ from fastapi import FastAPI -from http_app.routes import api, events, graphql, hello, ping +from http_app.routes import api, events, graphql, hello, ping, user_registered_hook def init_routes(app: FastAPI) -> None: @@ -8,4 +8,5 @@ def init_routes(app: FastAPI) -> None: app.include_router(ping.router) app.include_router(hello.router) app.include_router(events.router) + app.include_router(user_registered_hook.router) app.include_router(graphql.router, prefix="/graphql") diff --git a/src/http_app/routes/user_registered_hook.py b/src/http_app/routes/user_registered_hook.py new file mode 100644 index 00000000..16f32f2a --- /dev/null +++ b/src/http_app/routes/user_registered_hook.py @@ -0,0 +1,11 @@ +import logging + +from fastapi import APIRouter, Request + +router = APIRouter(prefix="/user_registered") + + +@router.post("/") +async def user_registered(request: Request): + logging.info("User registered", extra={"body": await request.json()}) + return {"user_registered": "OK"} From 80c9f14066253173603f531cd1d69eb036020cf1 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 3 Jan 2025 23:57:22 +0000 Subject: [PATCH 03/10] Remove separate staff schema Signed-off-by: Federico Busetti <729029+febus982@users.noreply.github.com> --- auth_volumes/kratos/kratos.yml | 2 -- auth_volumes/kratos/staff.schema.json | 49 --------------------------- 2 files changed, 51 deletions(-) delete mode 100644 auth_volumes/kratos/staff.schema.json diff --git a/auth_volumes/kratos/kratos.yml b/auth_volumes/kratos/kratos.yml index 55671edd..387e49e9 100644 --- a/auth_volumes/kratos/kratos.yml +++ b/auth_volumes/kratos/kratos.yml @@ -124,8 +124,6 @@ hashers: identity: default_schema_id: default schemas: - - id: staff - url: file:///etc/config/kratos/staff.schema.json - id: default url: file:///etc/config/kratos/identity.schema.json diff --git a/auth_volumes/kratos/staff.schema.json b/auth_volumes/kratos/staff.schema.json deleted file mode 100644 index f050843d..00000000 --- a/auth_volumes/kratos/staff.schema.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Staff", - "type": "object", - "properties": { - "traits": { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email", - "title": "E-Mail", - "minLength": 3, - "pattern": "^.*@staffdomain\\.com$", - "ory.sh/kratos": { - "credentials": { - "password": { - "identifier": true - } - }, - "verification": { - "via": "email" - }, - "recovery": { - "via": "email" - } - } - }, - "name": { - "type": "object", - "properties": { - "first": { - "title": "First Name", - "type": "string" - }, - "last": { - "title": "Last Name", - "type": "string" - } - } - } - }, - "required": [ - "email" - ], - "additionalProperties": false - } - } -} From b3aeb44de341cec57a48e3ac5bd7df1bf62bdaa5 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 3 Jan 2025 23:59:57 +0000 Subject: [PATCH 04/10] Remove checks related to identity schema Signed-off-by: Federico Busetti <729029+febus982@users.noreply.github.com> --- auth_volumes/oathkeeper/oathkeeper.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/auth_volumes/oathkeeper/oathkeeper.yml b/auth_volumes/oathkeeper/oathkeeper.yml index c94a87da..20666392 100644 --- a/auth_volumes/oathkeeper/oathkeeper.yml +++ b/auth_volumes/oathkeeper/oathkeeper.yml @@ -96,10 +96,8 @@ mutators: jwks_url: file:///etc/config/oathkeeper/id_token.jwks.json claims: | { - {{ if eq .Extra.identity.schema_id "staff" }} {{ if .MatchContext.Header.Get "x-impersonate" }} "impersonate": {{ .MatchContext.Header.Get "x-impersonate" | toJson }}, {{ end }} - {{ end }} "session": {{ .Extra | toJson }} } From bd181c97e0db2af06313a963bac855fbaa390fa2 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:03:28 +0000 Subject: [PATCH 05/10] Add comment Signed-off-by: Federico Busetti <729029+febus982@users.noreply.github.com> --- src/http_app/routes/user_registered_hook.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/http_app/routes/user_registered_hook.py b/src/http_app/routes/user_registered_hook.py index 16f32f2a..99624f3f 100644 --- a/src/http_app/routes/user_registered_hook.py +++ b/src/http_app/routes/user_registered_hook.py @@ -7,5 +7,6 @@ @router.post("/") async def user_registered(request: Request): + # Here we could check the email and add staff metadata to the identity logging.info("User registered", extra={"body": await request.json()}) return {"user_registered": "OK"} From e1d4919f23478434d33bf3add50a4b112052abd0 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:06:58 +0000 Subject: [PATCH 06/10] Disable coverage on test endpoint Signed-off-by: Federico Busetti <729029+febus982@users.noreply.github.com> --- src/http_app/routes/user_registered_hook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http_app/routes/user_registered_hook.py b/src/http_app/routes/user_registered_hook.py index 99624f3f..7914599e 100644 --- a/src/http_app/routes/user_registered_hook.py +++ b/src/http_app/routes/user_registered_hook.py @@ -6,7 +6,7 @@ @router.post("/") -async def user_registered(request: Request): +async def user_registered(request: Request): # pragma: no cover # Here we could check the email and add staff metadata to the identity logging.info("User registered", extra={"body": await request.json()}) return {"user_registered": "OK"} From 2af831fe685da8e87a0041dde9c3f061d03440be Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sat, 4 Jan 2025 09:53:26 +0000 Subject: [PATCH 07/10] Proxy all API endpoints to the dev container Signed-off-by: Federico Busetti <729029+febus982@users.noreply.github.com> --- auth_volumes/oathkeeper/access-rules.yml | 5 +++-- docs/zero_trust.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/auth_volumes/oathkeeper/access-rules.yml b/auth_volumes/oathkeeper/access-rules.yml index 34748da7..049b7155 100644 --- a/auth_volumes/oathkeeper/access-rules.yml +++ b/auth_volumes/oathkeeper/access-rules.yml @@ -73,13 +73,14 @@ config: to: http://127.0.0.1:8080/login -# Dev container access to protected /hello endpoint +# Dev container access to protected /api/* endpoints, to the dev container - id: "http_app:protected" upstream: preserve_host: true url: "http://dev:8000" + strip_path: /api match: - url: "http://127.0.0.1:8080/hello<{,/,/**}>" + url: "http://127.0.0.1:8080/<{,api/,api/**,openapi.json}>" methods: - GET authenticators: diff --git a/docs/zero_trust.md b/docs/zero_trust.md index f7decaa9..ad57a1d8 100644 --- a/docs/zero_trust.md +++ b/docs/zero_trust.md @@ -50,7 +50,7 @@ subgraph dn["Internal Docker Network (intranet)"] OO-->|"Proxies /auth/login, /auth/registration, /dashboard, ... to"|SA SA-->|Talks to|OK OO-->|Validates auth sessions using|OK - OO-->|"Proxies /hello to"|DEV + OO-->|"Proxies /api/* requests (authenticated only)"|DEV OK[Ory Kratos] OO["Reverse Proxy (Ory Oathkeeper)"] SA["SecureApp (Ory Kratos SelfService UI Node Example)"] From 5b106e0401a32c06b796c7850e9f6ed788216e6e Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:50:42 +0000 Subject: [PATCH 08/10] Fix double oathkeeper rule on / Signed-off-by: Federico Busetti <729029+febus982@users.noreply.github.com> --- auth_volumes/oathkeeper/access-rules.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_volumes/oathkeeper/access-rules.yml b/auth_volumes/oathkeeper/access-rules.yml index 049b7155..b5f4ef39 100644 --- a/auth_volumes/oathkeeper/access-rules.yml +++ b/auth_volumes/oathkeeper/access-rules.yml @@ -80,7 +80,7 @@ url: "http://dev:8000" strip_path: /api match: - url: "http://127.0.0.1:8080/<{,api/,api/**,openapi.json}>" + url: "http://127.0.0.1:8080/<{api/,api/**,openapi.json}>" methods: - GET authenticators: From 8901b0fc5fdf84ca37255922c1ca268766a84e77 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:52:07 +0000 Subject: [PATCH 09/10] Make the registration webhook be a blocker for undesired emails Signed-off-by: Federico Busetti <729029+febus982@users.noreply.github.com> --- auth_volumes/kratos/kratos.yml | 16 ++---- auth_volumes/kratos/user_registered.jsonnet | 3 - src/http_app/routes/user_registered_hook.py | 62 +++++++++++++++++++-- 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/auth_volumes/kratos/kratos.yml b/auth_volumes/kratos/kratos.yml index 387e49e9..61099bc6 100644 --- a/auth_volumes/kratos/kratos.yml +++ b/auth_volumes/kratos/kratos.yml @@ -81,26 +81,18 @@ selfservice: registration: lifespan: 10m ui_url: http://127.0.0.1:8080/registration + after: password: hooks: - - hook: session - hook: web_hook config: url: http://dev:8000/user_registered/ method: "POST" -# headers: { } - # https://www.ory.sh/docs/guides/integrate-with-ory-cloud-through-webhooks#jsonnet-templating body: file:///etc/config/kratos/user_registered.jsonnet - can_interrupt: false - emit_analytics_event: false -# auth: -# type: api_key -# config: -# name: "" -# value: "" -# in: header -# - hook: show_verification_ui + can_interrupt: true + emit_analytics_event: true + - hook: session log: level: info diff --git a/auth_volumes/kratos/user_registered.jsonnet b/auth_volumes/kratos/user_registered.jsonnet index 461429fa..8751729f 100644 --- a/auth_volumes/kratos/user_registered.jsonnet +++ b/auth_volumes/kratos/user_registered.jsonnet @@ -1,7 +1,4 @@ function(ctx) { user_id: ctx.identity.id, - traits: { email: ctx.identity.traits.email, - name: ctx.identity.traits.name, - } } diff --git a/src/http_app/routes/user_registered_hook.py b/src/http_app/routes/user_registered_hook.py index 7914599e..3299918e 100644 --- a/src/http_app/routes/user_registered_hook.py +++ b/src/http_app/routes/user_registered_hook.py @@ -1,12 +1,64 @@ import logging -from fastapi import APIRouter, Request +from fastapi import APIRouter, status +from fastapi.responses import JSONResponse, Response +from pydantic import BaseModel router = APIRouter(prefix="/user_registered") +class UserRegisteredWebhook(BaseModel): + user_id: str + email: str + + @router.post("/") -async def user_registered(request: Request): # pragma: no cover - # Here we could check the email and add staff metadata to the identity - logging.info("User registered", extra={"body": await request.json()}) - return {"user_registered": "OK"} +async def user_registered(user: UserRegisteredWebhook): # pragma: no cover + """ + Handles the user registration webhook. + + This function is triggered when a user registration webhook is received. + It logs the event details, evaluates the email validity, and returns an + appropriate HTTP response based on the validation. If the user's email + is invalid, it returns an error response along with a structured error + message. Otherwise, it confirms successful processing with no additional + content. + + Args: + user (UserRegisteredWebhook): The webhook payload received when a user + registers, containing user details such as email and traits. + + Returns: + Response: An HTTP response with a 403 Forbidden status and structured + error message if the user email is invalid. + Otherwise, an HTTP 204 No Content response to confirm successful + processing. + """ + logging.info("User registered", extra={"user": user.model_dump()}) + + error_message = { + "messages": [ + { + "instance_ptr": "#/traits/email", + "messages": [ + { + "id": 123, # Error id to be evaluated in frontend + "text": "You are not allowed to register.", + "type": "error", + "context": { # Additional context we can send to the Frontend + "value": "short value", + "any": "additional information", + }, + } + ], + } + ] + } + + if user.email == "invalid@test.com": + return JSONResponse( + error_message, + status.HTTP_403_FORBIDDEN, + ) + else: + return Response(status_code=status.HTTP_204_NO_CONTENT) From 7f92cb5e793f9f461abee78ab5806a8cb5d95381 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Tue, 7 Jan 2025 10:43:42 +0000 Subject: [PATCH 10/10] PyCharm profiles --- .idea/.gitignore | 8 ------- .idea/bootstrap-python-fastapi.iml | 13 ++++++------ .idea/inspectionProfiles/Project_Default.xml | 21 ------------------- .../inspectionProfiles/profiles_settings.xml | 6 ++++++ .idea/misc.xml | 11 ++-------- .idea/runConfigurations/Tests.xml | 2 +- .idea/vcs.xml | 2 +- 7 files changed, 17 insertions(+), 46 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b81..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/bootstrap-python-fastapi.iml b/.idea/bootstrap-python-fastapi.iml index 4e1b42f2..6a0f2b0a 100644 --- a/.idea/bootstrap-python-fastapi.iml +++ b/.idea/bootstrap-python-fastapi.iml @@ -4,15 +4,16 @@ + - + - - - + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 0c3737ba..00000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index bb0acce9..a72c6a13 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,14 +1,7 @@ - - - - - - - + \ No newline at end of file diff --git a/.idea/runConfigurations/Tests.xml b/.idea/runConfigurations/Tests.xml index a41a4bbc..92e69331 100644 --- a/.idea/runConfigurations/Tests.xml +++ b/.idea/runConfigurations/Tests.xml @@ -14,7 +14,7 @@