diff --git a/.env.template b/.env.template
new file mode 100644
index 000000000..1af83a01f
--- /dev/null
+++ b/.env.template
@@ -0,0 +1,12 @@
+
+PUBLIC_APP_HOST=workday.flock.local:8081
+PUBLIC_APP_URL=http://$PUBLIC_APP_HOST
+
+PUBLIC_ACCOUNTS_HOST=accounts.flock.local:8081
+PUBLIC_ACCOUNTS_URL=http://$PUBLIC_ACCOUNTS_HOST
+ACCOUNTS_SELFSERVICE_URL=$PUBLIC_ACCOUNTS_URL/ui/
+
+PUBLIC_KRATOS_URL=http://$PUBLIC_ACCOUNTS_HOST/api
+
+INTERNAL_KRATOS_URL=http://kratos:4433
+INTERNAL_ADMIN_KRATOS_URL=http://kratos:4434
diff --git a/.gitignore b/.gitignore
index 46418dc65..11d084932 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,4 @@ secrets.yml
node_modules
coverage
.env
+/docker/kratos/identities/existing_identities.json
diff --git a/docker-compose.yml b/docker-compose.yml
index 1a262d6fa..7e3512299 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,24 +1,25 @@
version: "3.9"
services:
- workday:
- image: gcr.io/flock-eco/flock-eco-workday-develop:snapshot
- environment:
- - GOOGLE_APPLICATION_CREDENTIALS=/tmp/keys/application_default_credentials.json
- volumes:
- - type: bind
- source: $HOME/.config/gcloud/application_default_credentials.json
- target: /tmp/keys/application_default_credentials.json
- read_only: true
- ports:
- - "8080:8080"
- networks:
- - intranet
+# workday:
+# image: gcr.io/flock-eco/flock-eco-workday-develop:snapshot
+# environment:
+# - GOOGLE_APPLICATION_CREDENTIALS=/tmp/keys/application_default_credentials.json
+# volumes:
+# - type: bind
+# source: $HOME/.config/gcloud/application_default_credentials.json
+# target: /tmp/keys/application_default_credentials.json
+# read_only: true
+# ports:
+# - "8080:8080"
+# networks:
+# - intranet
oathkeeper:
image: oryd/oathkeeper:v0.40.2
depends_on:
- kratos
- kratos-selfservice-ui-node
+ - kratos-setup-users
# - workday
ports:
- "8081:4455"
@@ -26,9 +27,10 @@ services:
command: serve proxy -c "/etc/config/oathkeeper/config.yaml"
environment:
- LOG_LEVEL=debug
- - ERRORS_HANDLERS_REDIRECT_CONFIG_TO=$SELFSERVICE_URL/login
- - SERVE_PROXY_CORS_ALLOWED_ORIGINS_0=$PUBLIC_INGRESS_URL
- - SERVE_PROXY_CORS_ALLOWED_ORIGINS_1=$SELFSERVICE_URL
+ - ERRORS_HANDLERS_REDIRECT_CONFIG_TO=$ACCOUNTS_SELFSERVICE_URL/login
+ - MUTATORS_ID_TOKEN_CONFIG_ISSUER_URL=$PUBLIC_ACCOUNTS_URL
+ - SERVE_PROXY_CORS_ALLOWED_ORIGINS_0=$PUBLIC_APP_URL
+ - SERVE_PROXY_CORS_ALLOWED_ORIGINS_1=$ACCOUNTS_SELFSERVICE_URL
- SERVE_PROXY_CORS_ALLOWED_ORIGINS_2=$PUBLIC_KRATOS_URL
restart: on-failure
networks:
@@ -63,19 +65,19 @@ services:
- DSN=postgres://kratos:secret@postgres-kratos:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4
#
- SERVE_PUBLIC_BASE_URL=$PUBLIC_KRATOS_URL
- - SERVE_ADMIN_BASE_URL=$ADMIN_KRATOS_URL_INTERNAL
- - SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=$PUBLIC_INGRESS_URL
- - SELFSERVICE_ALLOWED_RETURN_URLS_0=$PUBLIC_INGRESS_URL
- - SELFSERVICE_ALLOWED_RETURN_URLS_1=$SELFSERVICE_URL
+ - SERVE_ADMIN_BASE_URL=$INTERNAL_ADMIN_KRATOS_URL
+ - SELFSERVICE_DEFAULT_BROWSER_RETURN_URL=$PUBLIC_APP_URL
+ - SELFSERVICE_ALLOWED_RETURN_URLS_0=$PUBLIC_APP_URL
+ - SELFSERVICE_ALLOWED_RETURN_URLS_1=$ACCOUNTS_SELFSERVICE_URL
# - BASE_PATH=/kratos
- - SELFSERVICE_FLOWS_ERROR_UI_URL=$SELFSERVICE_URL/error
- - SELFSERVICE_FLOWS_SETTINGS_UI_URL=$SELFSERVICE_URL/settings
- - SELFSERVICE_FLOWS_RECOVERY_UI_URL=$SELFSERVICE_URL/recovery
- - SELFSERVICE_FLOWS_VERIFICATION_UI_URL=$SELFSERVICE_URL/verification
- - SELFSERVICE_FLOWS_LOGOUT_UI_URL=$SELFSERVICE_URL/logout
- - SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=$PUBLIC_INGRESS_URL
- - SELFSERVICE_FLOWS_LOGIN_UI_URL=$SELFSERVICE_URL/login
- - SELFSERVICE_FLOWS_REGISTRATION_UI_URL=$SELFSERVICE_URL/registration
+ - SELFSERVICE_FLOWS_ERROR_UI_URL=$ACCOUNTS_SELFSERVICE_URL/error
+ - SELFSERVICE_FLOWS_SETTINGS_UI_URL=$ACCOUNTS_SELFSERVICE_URL/settings
+ - SELFSERVICE_FLOWS_RECOVERY_UI_URL=$ACCOUNTS_SELFSERVICE_URL/recovery
+ - SELFSERVICE_FLOWS_VERIFICATION_UI_URL=$ACCOUNTS_SELFSERVICE_URL/verification
+ - SELFSERVICE_FLOWS_LOGOUT_UI_URL=$ACCOUNTS_SELFSERVICE_URL/logout
+ - SELFSERVICE_FLOWS_LOGOUT_AFTER_DEFAULT_BROWSER_RETURN_URL=$PUBLIC_APP_URL
+ - SELFSERVICE_FLOWS_LOGIN_UI_URL=$ACCOUNTS_SELFSERVICE_URL/login
+ - SELFSERVICE_FLOWS_REGISTRATION_UI_URL=$ACCOUNTS_SELFSERVICE_URL/registration
ports:
- '4433:4433'
- '4434:4434'
@@ -92,7 +94,7 @@ services:
kratos-selfservice-ui-node:
image: oryd/kratos-selfservice-ui-node:latest
environment:
- - KRATOS_PUBLIC_URL=$PUBLIC_KRATOS_URL_INTERNAL
+ - KRATOS_PUBLIC_URL=$INTERNAL_KRATOS_URL
- KRATOS_BROWSER_URL=$PUBLIC_KRATOS_URL
- BASE_PATH=/ui
networks:
@@ -135,7 +137,7 @@ services:
command: migrate sql -e --yes -c /etc/config/kratos/kratos.yml
kratos-setup-users:
- image: curlimages/curl
+ image: atlassian/default-image:4 # TODO let's go leaner
depends_on:
- kratos
networks:
@@ -144,7 +146,7 @@ services:
- type: bind
source: ./docker/kratos/identities
target: /tmp
- command: sh /tmp/import_identities.sh
+ command: /tmp/import_identities.sh
keto-migrate:
image: oryd/keto:v0.11.1-alpha.0
diff --git a/docker/keto/namespaces/permissions.ts b/docker/keto/namespaces/permissions.ts
index 8f1457dbb..5e875a45f 100644
--- a/docker/keto/namespaces/permissions.ts
+++ b/docker/keto/namespaces/permissions.ts
@@ -1,4 +1,4 @@
-import {Namespace, SubjectSet, Context} from "@ory/keto-namespace-types"
+import {Namespace, Context} from "@ory/keto-namespace-types"
class User implements Namespace {
diff --git a/docker/kratos/identities/import_identities.sh b/docker/kratos/identities/import_identities.sh
index b8da9937f..9ac79a03d 100644
--- a/docker/kratos/identities/import_identities.sh
+++ b/docker/kratos/identities/import_identities.sh
@@ -1,10 +1,14 @@
-#!/bin/bash
+#!/bin/sh
+
sleep 3
while read line; do
curl --request POST -vvv -sL \
+ --fail \
--header "Content-Type: application/json" \
--data-binary "@$line" http://kratos:4434/admin/identities
-done < <(find . -type f -name "*.json")
-
-
+done < <(find /tmp -type f -name "*.json")
+echo $PWD
+curl -vvv \
+ --header "Accept: application/json" \
+ http://kratos:4434/admin/identities | jq '[.[] | {"id": .id, "email": .traits.email} ] ' > /tmp/existing_identities.json
diff --git a/docker/oathkeeper/accounts-management-rules.yaml b/docker/oathkeeper/accounts-management-rules.yaml
index 5e5924373..b63ded6c4 100644
--- a/docker/oathkeeper/accounts-management-rules.yaml
+++ b/docker/oathkeeper/accounts-management-rules.yaml
@@ -1,3 +1,20 @@
+- id: oathkeeper-jwks
+ version: v0.40.2
+ upstream:
+ url: http://oathkeeper:4456
+ match:
+ url: http://accounts.flock.local:8081/.well-known/jwks.json
+ methods:
+ - GET
+ authenticators:
+ - handler: noop
+ authorizer:
+ handler: allow
+ mutators:
+ - handler: noop
+ errors:
+ - handler: redirect
+
- id: kratos-public-api
version: v0.40.2
upstream:
diff --git a/docker/oathkeeper/config.yaml b/docker/oathkeeper/config.yaml
index 66983d7d2..c1d0c6b34 100644
--- a/docker/oathkeeper/config.yaml
+++ b/docker/oathkeeper/config.yaml
@@ -57,6 +57,14 @@ errors:
verbose: true
mutators:
+ id_token:
+ # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false.
+ enabled: true
+ config:
+ issuer_url: --set-me-through-env-variable--
+ jwks_url: file:///etc/config/oathkeeper/jwks.json
+ ttl: 60s
+ claims: '{"aud": ["https://my-backend-service/some/endpoint"],"email": "{{ print .Extra.identity.traits.email }}"}'
header:
enabled: true
config:
diff --git a/docker/oathkeeper/jwks.json b/docker/oathkeeper/jwks.json
new file mode 100644
index 000000000..949be6087
--- /dev/null
+++ b/docker/oathkeeper/jwks.json
@@ -0,0 +1,18 @@
+{
+ "keys": [
+ {
+ "use": "sig",
+ "kty": "RSA",
+ "kid": "75ab2a62-5eeb-4362-8a23-6aa8e0622f4d",
+ "alg": "RS256",
+ "n": "u6cx6cF08Jqavfl5Mrh8_KBDXkBu-g6aY1SZsxy31ac_9iZgI3YgEkqkFqO4AvDG8fIydU8BINbbxtvDe6bgTXdagKAshsgqTHLRIrv0gPX7ZEeAU-dahP1dsWtmmgmNCzduPn8N8LBkQuj9DifGKihGr8E7LjcuGx6rvaxpsrl-bQZ5TxKZRq3CkB-vXJM8nXn48hlWwRZ8AcAUcazJFbao5xD0ejzgL1WDndXPix2u_Twl6zDZ8BlMmUKmPQUr9voWLA0CZbmpF3Temp6ENW20ngucrv1MsWE65MQnFqnt5bnlL30OJWdV3u280Dqchhr1b5CtNOdB0lzzGdmVuw",
+ "e": "AQAB",
+ "d": "qPy1_DXVI192_qGMvVpaY_3G7TfFOLax3cWv7BZujp7hAMAqT4Tu-Mny3thsu2ktH3Dmc7HD6FjU7k21ysLKt7hF7hNQRcg4H0Q-hbMRSpk53GJvdB-h6qVoHIAFk64KMBGo2-r7GMcgYwkB6h9zWz-N-HRg4QucH5wNOO_s9556GHe5tRVuz4YsaF0XdLXS5bPLgkAtVZhE_4KCaAaZJL4aV35zBc2HMKZ059L3H2RnPQtloEDUDbeDPPZKwUBCBhGNQvz6GkaYn-sdgIQnHv374RfXDHga2_qDKBbNIKno9F59tt5ytijrtqnJGovAMnmT4bAn7ExpUw0tMfC7gQ",
+ "p": "wVwk9tRW5w4PTyRuo5eYvmgRmfDUFWYreQCweHkKNT92EPUlJQevKcfvKc8DM2CH3T6o3KVvuNVq4JUsPAFh7MWW_V_H_HDZQ6Q0iAyb6mTrSdAjHrnTY9MHc-aDSKfuneXY_e3-ag-0gmMBng7Bu0PxomWZZHhWZ7aclAEitBs",
+ "q": "-HHEhBkjuycQg6ZdbOP4SFHQ_X6CQNXrHP8RWnYpnwFCV1vnccUxOHsaXZkJSVU1ibLD9eU_iTtR3F4zPFEEmIM5JVuBvLJcHqqBbEDYPabPoefAz3yqzUEw-yWG2O9JaD7u4zl-zr9R6ieMYV0IyGuGvRkPk6MnhJSsr6RcfuE",
+ "dp": "NT7pkurwL1pIzvNqYDQ7xJsl1a1iya3D5ONStSonrHgYTB0lqGfQTYIYEvxFll1LrJo3p-MKu-xRLR0G_FMpIylaJdW9XX-55I2QRbKrMMWvoTjmouxrEL8o-zqMBnLwG50SvwJNn-lJPOqEpIDNyoKwRJcTwX391TuNLJiPdOc",
+ "dq": "F_8VFhU_iEIbrDuTZoMWZhXQ88sWVaDT1rckO_KRzpPWjo2H60s6l9P8FfJEHVsnkqc7mjKMUnrySzCmDjtdEEYMbPlLrKglkaiyW3xf5oSIJYE29FN8Q9r6GifjwGxMUM9TT4ssHlgIV29-daEAyirolrcm9AGcPYgGrehYLsE",
+ "qi": "fjD-SuFpHe3xgk-1SDtjV19QtbFmchjoGuWA5A9SPiA-L3471uv9mI5zil-GS54URtuVBd6QZ404zBR7cuc9VnUHE5k_avKfQh0O4uIkOF7o7TMPf8xc3d4Uee2uKvJsASQw9rNcwtpqKSismxTNE86SoTa8zEYjYDdgYr81zTE"
+ }
+ ]
+}
diff --git a/docker/oathkeeper/rules.yaml b/docker/oathkeeper/rules.yaml
index d5c7f84c9..0c89de1c9 100644
--- a/docker/oathkeeper/rules.yaml
+++ b/docker/oathkeeper/rules.yaml
@@ -17,106 +17,12 @@
handler: allow
mutators:
- handler: header
- errors:
- - handler: redirect
-
-- id: echo-public
- version: v0.40.2
- match:
- url: http://localhost:8085/<.*>
- methods:
- - GET
- authenticators:
- - handler: anonymous
- authorizer:
- handler: allow
- mutators:
- - handler: header
- errors:
- - handler: redirect
-- id: echo-public
- version: v0.40.2
- match:
- url: https://<.*>.gitpod.io/echo/public/<.*>
- methods:
- - GET
- authenticators:
- - handler: anonymous
- authorizer:
- handler: allow
- mutators:
- - handler: header
- errors:
- - handler: redirect
-- id: echo-protected
- version: v0.40.2
- match:
- url: https://<.*>.gitpod.io/echo/protected/<.*>
- methods:
- - GET
- authenticators:
- - handler: cookie_session
- authorizer:
- handler: allow
- mutators:
- - handler: header
- errors:
- - handler: redirect
-- id: echo-elevated-authentication
- version: v0.40.2
- match:
- url: https://<.*>.gitpod.io/echo/elevated/<.*>
- methods:
- - GET
- authenticators:
- - handler: cookie_session
+ - handler: id_token
config:
- check_session_url: http://kratos-whoami-aal-aware:8080/sessions/whoami?aal=aal2
- authorizer:
- handler: remote_json
- config:
- remote: http://keto:4466/relation-tuples/check
- payload: '{ "namespace": "access", "object": "{{printIndex .MatchContext.RegexpCaptureGroups
- 1}}", "relation": "read", "subject_id": "{{print .Subject}}" }'
- mutators:
- - handler: header
- errors:
- - handler: redirect
- config:
- to: https://8085-flockcommun-identityacc-995ctfvg8og.ws-eu96b.gitpod.io/kratos/self-service/login/browser?refresh=true&aal=aal2
-- id: echo-keto-get-vehicles
- version: v0.40.2
- match:
- url: https://<.*>.gitpod.io/echo/vehicles/<.*>
- methods:
- - GET
- authenticators:
- - handler: cookie_session
- authorizer:
- handler: remote_json
- config:
- remote: http://keto:4466/relation-tuples/check
- payload: '{ "namespace": "access", "object": "{{printIndex .MatchContext.RegexpCaptureGroups
- 1}}", "relation": "read", "subject_id": "{{print .Subject}}" }'
- mutators:
- - handler: header
- errors:
- - handler: redirect
-- id: echo-keto-get-finances
- version: v0.40.2
- match:
- url: https://<.*>.gitpod.io/echo/finances/<.*>
- methods:
- - GET
- authenticators:
- - handler: cookie_session
- authorizer:
- handler: remote_json
- config:
- remote: http://keto:4466/relation-tuples/check
- payload: '{ "namespace": "access", "object": "{{printIndex .MatchContext.RegexpCaptureGroups
- 1}}", "relation": "read", "subject_id": "{{print .Subject}}" }'
- mutators:
- - handler: header
+ claims: |
+ {
+ "aud": ["http://workday.flock.local:8081"],
+ "email": "{{ print .Extra.identity.traits.email }}"
+ }
errors:
- handler: redirect
diff --git a/docs/Workday App permissions model.drawio.png b/docs/Workday App permissions model.drawio.png
new file mode 100644
index 000000000..a7bad596f
Binary files /dev/null and b/docs/Workday App permissions model.drawio.png differ
diff --git a/docs/identity-access-management.md b/docs/identity-access-management.md
new file mode 100644
index 000000000..460988e29
--- /dev/null
+++ b/docs/identity-access-management.md
@@ -0,0 +1,117 @@
+# Authentication and Authorization
+
+The intended target state for Workday is to extract all forms of Authentication and Authorization from the spring boot
+and or react application, but rather make use of an identity aware proxy (Ory's Oathkeeper) to make such decisions.
+
+This means that all incoming requests to the Spring Boot application are expected to be authenticated, and authorised,
+such that the application can focus on the functional logic solely.
+
+## Roadmap
+
+(🟡 being worked on, 🟢 done )
+
+1. 🟡 Setup authentication model in `flock-iam` project (external to Workday)
+2. 🟡 Introduce Oathkeeper proxy for Workday, to route all traffic through it before reaching the app itself
+3. 🟡 Authentication via Ory's Kratos
+4. 'Simpler' `GET` and `PUT` endpoints authorization
+5. `POST` / create endpoint
+6. List endpoints
+7. Removal of authorities from workday app itself
+
+## Understanding the Ory stack
+
+(probably best to move me to flock-iam related repo?)
+
+There are multiple components to the Ory stack
+
+- Kratos, ...
+- Oathkeeper, ...
+- Keto, ...
+
+Every http request to the Workday app is called via the Oathkeeper proxy (running on port `8081`)
+
+![Oathkeeper decision engine](./oathkeeper-decision-engine.png)
+
+Oathkeeper performs various checks:
+
+1. Is there a matching access rule? If no, handle error
+2. Is the user's request authenticated through a `cookie_sesion`? If no, handle error
+3. Is the user's request authorized? If no, handle error
+4. Mutate the request (e.g. to amend user information to the request)
+
+## Workday's permission model
+
+[Ory's keto](https://www.ory.sh/docs/keto/) make use of a permission model, to determine whether a given subject (a
+User) can access an object (e.g. a Workday, a Holiday, etc.). A permission model consists of two parts: a relation
+model, and the derived permissions from said these relations.
+
+Below you'll find the initial _relations model_ for Workday.
+![x](Workday%20App%20permissions%20model.drawio.png)
+
+Which can be read as
+
+- A _Person_ is the **owner** of a _Workday_
+- A _User_ is the **owner** of a _Person_
+- A _Flock_ is the **organisational unit** of a _Person_
+- A _Flock_ is the **organisational unit** of a _Flock_
+- A _User_ is the **manager** of a _Flock_
+- etc.
+
+> ℹ️
+> This model allows manager of (a) Flock. to have a relation with a specific Workday coupled to a person.
+>
+> E.g. _Workday-X_ is **owned** by _Person-Y_, which is part of **organisational unit** _Flock-Z_, which in turn is \*
+> \*managed\*\* by _User-manager-A_
+
+Besides the relations model, Workday also need permissions assigned. Ory's keto, provided an intuitive concept for
+permissions in the following form:
+
+> _subject_ can **permission** _object_? yes/no
+
+These are derived from the relations, and are essentially a graph traversal.
+
+In the permission model the following rules apply (amongst others):
+
+- An owner of a Person can view and edit all owned Workdays by that Person.
+- A manager of a Person can view and edit all owned Workdays by that Person.
+- A manager of a Flock is the manager of all Persons that have said Flock as their organizational unit.
+- A manager of a Flock is the manager of all Flocks that have said Flock as their organizational unit.
+
+for the implementation details see [permissions.ts](../docker/keto/namespaces/permissions.ts)
+
+## Developing Workday with Ory
+
+> ℹ️ Prerequisites:
+>
+> 1. In your `etc/hosts`, the domains `accounts.flock.local` and `workday.flock.local` should both resolve to 127.0.0.1
+>
+> ```
+> 127.0.0.1 accounts.flock.local
+> 127.0.0.1 workday.flock.local
+> ```
+>
+> 2. Run `docker compose up -d` to start all Ory related containers for identity and access management.
+> 3. Also, make sure that you're The workday application tries to connect to is expected to run on port `8080`, with
+
+ profile [`🔗develop`](src/main/resources/application-develop.properties)
+ and [`🔗develop-kratos`](src/main/resources/application-develop-kratos.properties) active (in that order!)
+
+By default, the `kratos-setup-users` container will try to create identities (= users) found in
+the [docker/kratos/identities](../docker/kratos/identities) folder. Workday can use the resulting kratosIds to bind them
+to the mocked users created on startup.
+
+> ⚠️ `kratosId`s
+>
+> Note that the `kratosId`s defined in [🔗`Users.kt`](src/develop/kotlin/community/flock/eco/workday/mocks/Users.kt) will
+> have different ids from the generated ones. These are substituted with the actual Kratos identities when Workday
+> creates
+> the users.
+
+### Generate a jwks.json
+
+(flock-iam material)
+To generate a `jwks.json` file:
+
+```bash
+docker run oryd/oathkeeper:v0.40.3 credentials generate --alg RS256 > jwks.json
+```
diff --git a/docs/oathkeeper-decision-engine.png b/docs/oathkeeper-decision-engine.png
new file mode 100644
index 000000000..8462e52dd
Binary files /dev/null and b/docs/oathkeeper-decision-engine.png differ
diff --git a/package-lock.json b/package-lock.json
index 769b82538..465785c8e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,8 +9,8 @@
"dependencies": {
"@babel/runtime": "^7.7.7",
"@date-io/dayjs": "^1.3.13",
- "@flock-community/flock-eco-core": "^2.7.8",
- "@flock-community/flock-eco-feature-user": "^2.7.8",
+ "@flock-community/flock-eco-core": "^2.7.9",
+ "@flock-community/flock-eco-feature-user": "^2.7.9",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
@@ -1893,19 +1893,19 @@
}
},
"node_modules/@flock-community/flock-eco-core": {
- "version": "2.7.8",
- "resolved": "https://flock.jfrog.io/artifactory/api/npm/flock-npm/@flock-community/flock-eco-core/-/@flock-community/flock-eco-core-2.7.8.tgz",
- "integrity": "sha512-K/VS2vwG7kQnFd9O+1hk/a50tqil+0C2Qu4LqEVhyEB4OFAs1ugQnhM8L/Fl1/XdjM1QPw6gSyeDWRbFodjebg==",
+ "version": "2.7.9",
+ "resolved": "https://flock.jfrog.io/artifactory/api/npm/flock-npm/@flock-community/flock-eco-core/-/@flock-community/flock-eco-core-2.7.9.tgz",
+ "integrity": "sha512-5XNqKPE5zNOwCSlWoKAN9AiIwF1OfjDVnG3uQXCEY/rWRLPOpWCI0nFvvMmCGKHna71cr+Hy25j8gZ5308tgNQ==",
"dependencies": {
"typescript": "4.5.5"
}
},
"node_modules/@flock-community/flock-eco-feature-user": {
- "version": "2.7.8",
- "resolved": "https://flock.jfrog.io/artifactory/api/npm/flock-npm/@flock-community/flock-eco-feature-user/-/@flock-community/flock-eco-feature-user-2.7.8.tgz",
- "integrity": "sha512-LpeA7aqNle7YT+eVCKK2ZWF3pu6G2DmPbJxMN4TyOCmQ4A0yidKx2iCjHQ1LNTO0WP55GZ5/U0Ny7ik6DTsbjg==",
+ "version": "2.7.9",
+ "resolved": "https://flock.jfrog.io/artifactory/api/npm/flock-npm/@flock-community/flock-eco-feature-user/-/@flock-community/flock-eco-feature-user-2.7.9.tgz",
+ "integrity": "sha512-vbHb2AXag50oc3Ra7n2wi1ygN8nkpRe4bpmNBn+a2kxyarrtlltcE4yZjfAWAAuHRnf9+VU22eT/uwMyeKfOZA==",
"dependencies": {
- "@flock-community/flock-eco-core": "^2.7.8"
+ "@flock-community/flock-eco-core": "^2.7.9"
}
},
"node_modules/@flock-community/flock-eco-webpack": {
@@ -32131,19 +32131,19 @@
}
},
"@flock-community/flock-eco-core": {
- "version": "2.7.8",
- "resolved": "https://flock.jfrog.io/artifactory/api/npm/flock-npm/@flock-community/flock-eco-core/-/@flock-community/flock-eco-core-2.7.8.tgz",
- "integrity": "sha512-K/VS2vwG7kQnFd9O+1hk/a50tqil+0C2Qu4LqEVhyEB4OFAs1ugQnhM8L/Fl1/XdjM1QPw6gSyeDWRbFodjebg==",
+ "version": "2.7.9",
+ "resolved": "https://flock.jfrog.io/artifactory/api/npm/flock-npm/@flock-community/flock-eco-core/-/@flock-community/flock-eco-core-2.7.9.tgz",
+ "integrity": "sha512-5XNqKPE5zNOwCSlWoKAN9AiIwF1OfjDVnG3uQXCEY/rWRLPOpWCI0nFvvMmCGKHna71cr+Hy25j8gZ5308tgNQ==",
"requires": {
"typescript": "4.5.5"
}
},
"@flock-community/flock-eco-feature-user": {
- "version": "2.7.8",
- "resolved": "https://flock.jfrog.io/artifactory/api/npm/flock-npm/@flock-community/flock-eco-feature-user/-/@flock-community/flock-eco-feature-user-2.7.8.tgz",
- "integrity": "sha512-LpeA7aqNle7YT+eVCKK2ZWF3pu6G2DmPbJxMN4TyOCmQ4A0yidKx2iCjHQ1LNTO0WP55GZ5/U0Ny7ik6DTsbjg==",
+ "version": "2.7.9",
+ "resolved": "https://flock.jfrog.io/artifactory/api/npm/flock-npm/@flock-community/flock-eco-feature-user/-/@flock-community/flock-eco-feature-user-2.7.9.tgz",
+ "integrity": "sha512-vbHb2AXag50oc3Ra7n2wi1ygN8nkpRe4bpmNBn+a2kxyarrtlltcE4yZjfAWAAuHRnf9+VU22eT/uwMyeKfOZA==",
"requires": {
- "@flock-community/flock-eco-core": "^2.7.8"
+ "@flock-community/flock-eco-core": "^2.7.9"
}
},
"@flock-community/flock-eco-webpack": {
diff --git a/package.json b/package.json
index 410ed0ce4..b2e3d5855 100644
--- a/package.json
+++ b/package.json
@@ -17,8 +17,8 @@
"dependencies": {
"@babel/runtime": "^7.7.7",
"@date-io/dayjs": "^1.3.13",
- "@flock-community/flock-eco-core": "^2.7.8",
- "@flock-community/flock-eco-feature-user": "^2.7.8",
+ "@flock-community/flock-eco-core": "^2.7.9",
+ "@flock-community/flock-eco-feature-user": "^2.7.9",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
diff --git a/pom.xml b/pom.xml
index 3a6048567..fa7e9fc30 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
flock-eco-application-parent
com.github.flock-community.flock-eco
- 2.7.8
+ 2.7.9
flock.community.eco.workday
@@ -45,6 +45,10 @@
org.springframework.boot
spring-boot-starter-actuator
+
+ org.springframework.security
+ spring-security-oauth2-resource-server
+
org.springframework.cloud
spring-cloud-gcp-starter-sql-postgresql
diff --git a/readme.md b/readme.md
index 98ce4ed8d..fef700fca 100644
--- a/readme.md
+++ b/readme.md
@@ -1,22 +1,44 @@
# Flock Workday
-## Run
+about
+
+## Get started
+
+Run the Workday app, with the backend and frontend bundled together:
```bash
-./mvnw clean compile spring-boot:run -Dspring-boot.run.profiles=develop -Pdevelop
+./mvnw clean compile spring-boot:run \
+ -Dspring-boot.run.profiles=develop \
+ -P develop \
+ -P frontend
```
-Mark `src/develop/kotlin` as source directory.
-
-Add `develop` to the springboot `application` run configuration active profiles.
+Run a full build of backend and frontend, and build a Docker 🐳 image
```bash
-./mvnw clean install
-npm install
+./mvnw clean install jib:dockerBuild\
+ -P develop \
+ -P frontend \
+ -Djib.container.environment=SPRING_PROFILES_ACTIVE=develop \
+ --file pom.xml
```
-Run `application` and `npm start`. Make sure you're using the correct node version. If you have nvm installed,
-run `nvm use` to set node to the version defined in .nvmrc
+### Backend tips
+
+- ℹ️ Mark `src/develop/kotlin` as source directory, if not done automatically by your IDE.
+- ℹ️ Add `develop` to the springboot `application` run configuration active profiles. (
+ Find [`🔗Application.kt`](src/main/kotlin/Application.kt), run the app one time, and adjust the config)
+- ℹ️ The `com.github.eirslett` dependency is used to package the frontend together with the backend, but this only
+ happens if the `frontend` profile is active (`-P` flag).
+ - Example: `./mvnw clean install -P frontend`)
+
+### Frontend tips
+
+- ℹ️ Workday makes use of [nvm](https://github.com/nvm-sh/nvm) and an [`🔗.nvmrc`](./.nvmrc) to define the Node version.
+ By running `nvm use`, you activate the configured Node version.
+- ℹ️ To run the frontend independently, run `npm install` and then `npm start`.
+ - A webpack devServer is used to proxy api calls towards the backend, which is expected to run on port 8080. See
+ the [`🔗webpack.config.js`](./webpack.config.js) for more details.
## Users
@@ -30,12 +52,31 @@ run `nvm use` to set node to the version defined in .nvmrc
## Linting
-Use `ktlint` to lint kotlin files or `eslint` for javascript files
+Use `ktlint` to lint kotlin files
+
+```bash
+# check code style (it's also bound to "mvn verify")
+./mvnw antrun:run@ktlint
+```
+
+```bash
+# fix code style deviations (runs built-in formatter)
+./mvnw antrun:run@ktlint-format
+```
+
+Use `prettier` for javascript/typescript files
+
+```bash
+# fix code styles for js files with eslint
+npm run lint format
+```
+
+[Install the prettier plugin in your IDE](https://prettier.io/docs/en/editors.html) to get the most out of prettier.
## Database
- Generate diff file with liquibase
- ```
+ ```bash
./mvnw clean compile liquibase:update liquibase:diff
```
- Rename `db.changelog-diff.yaml` to `db.changelog-#.yaml`
@@ -49,23 +90,38 @@ Use `ktlint` to lint kotlin files or `eslint` for javascript files
relativeToChangelogFile: true
```
-```bash
-# check code style (it's also bound to "mvn verify")
-$ ./mvnw antrun:run@ktlint
- src/main/kotlin/Main.kt:10:10: Unused import
+## Integration with Ory (authentication and authorization)
-# fix code style deviations (runs built-in formatter)
-$ ./mvnw antrun:run@ktlint-format
+See also [Identity access management](./docs/identity-access-management.md) for the prerequisites and more detailed
+information
-# fix code styles for js files with eslint
-$ npm run lint
+It is possible to run workday locally, integrating with the Ory stack (Kratos, Oathkeeper, Keto) for identity access and
+permission management using the docker compose file.
+
+```bash
+# Yolo, TL;DR
+# Start everything authentication/authorization related
+docker compose up -d
+
+# Alternatively, start the spring app through IDE, with profile develop-kratos active
+./mvnw clean compile spring-boot:run \
+ -Dspring-boot.run.profiles=develop,develop-kratos \
+ -P develop \
+ -P frontend
+
+# Go
+open http://workday.flock.local:8081
```
-## Generate secrets
+### Development info regarding the Ory stack
+
+See [Authentication and Authorization](./docs/authentication-and-authorization.md)
+
+## Generate secrets (deprecated?)
Generate secrets to deploy via travis-ci
-```
+```bash
tar cvf secrets.tar ./service-account.json src/main/resources/application-cloud.properties
travis encrypt-file secrets.tar --add
```
diff --git a/src/develop/kotlin/mocks/LoadAssignmentData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadAssignmentData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadAssignmentData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadAssignmentData.kt
diff --git a/src/develop/kotlin/mocks/LoadClientData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadClientData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadClientData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadClientData.kt
diff --git a/src/develop/kotlin/mocks/LoadContractData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadContractData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadContractData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadContractData.kt
diff --git a/src/develop/kotlin/mocks/LoadData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadData.kt
diff --git a/src/develop/kotlin/mocks/LoadEventData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadEventData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadEventData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadEventData.kt
diff --git a/src/develop/kotlin/mocks/LoadEventRatingData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadEventRatingData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadEventRatingData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadEventRatingData.kt
diff --git a/src/develop/kotlin/mocks/LoadExpensesData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadExpensesData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadExpensesData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadExpensesData.kt
diff --git a/src/develop/kotlin/mocks/LoadHolidayData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadHolidayData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadHolidayData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadHolidayData.kt
diff --git a/src/develop/kotlin/mocks/LoadPersonData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadPersonData.kt
similarity index 98%
rename from src/develop/kotlin/mocks/LoadPersonData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadPersonData.kt
index f0bff2d73..327191f90 100644
--- a/src/develop/kotlin/mocks/LoadPersonData.kt
+++ b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadPersonData.kt
@@ -3,7 +3,6 @@ package community.flock.eco.workday.mocks
import community.flock.eco.feature.user.model.User
import community.flock.eco.workday.model.Person
import community.flock.eco.workday.repository.PersonRepository
-import mocks.users
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component
import java.time.Instant
@@ -73,7 +72,7 @@ class LoadPersonData(
init {
val userMap = userData.data.associateBy { it.name }
- users.forEach {
+ mockUsers.forEach {
createPerson(
firstname = it.firstName,
lastname = it.lastName,
diff --git a/src/develop/kotlin/mocks/LoadProjectData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadProjectData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadProjectData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadProjectData.kt
diff --git a/src/develop/kotlin/mocks/LoadSickdaysData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadSickdaysData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadSickdaysData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadSickdaysData.kt
diff --git a/src/develop/kotlin/community/flock/eco/workday/mocks/LoadUserData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadUserData.kt
new file mode 100644
index 000000000..144820c6f
--- /dev/null
+++ b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadUserData.kt
@@ -0,0 +1,116 @@
+package community.flock.eco.workday.mocks
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import community.flock.eco.core.authorities.Authority
+import community.flock.eco.feature.user.forms.UserAccountOauthForm
+import community.flock.eco.feature.user.forms.UserAccountPasswordForm
+import community.flock.eco.feature.user.model.User
+import community.flock.eco.feature.user.model.UserAccountOauthProvider
+import community.flock.eco.feature.user.services.UserAccountService
+import community.flock.eco.feature.user.services.UserAuthorityService
+import community.flock.eco.workday.authorities.ExpenseAuthority
+import community.flock.eco.workday.authorities.HolidayAuthority
+import community.flock.eco.workday.authorities.SickdayAuthority
+import community.flock.eco.workday.authorities.WorkDayAuthority
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
+import org.springframework.stereotype.Component
+import java.io.File
+import java.io.IOException
+import java.util.*
+
+
+@Component
+@ConditionalOnProperty(prefix = "flock.eco.workday", name = ["develop"])
+class LoadUserData(
+ @Value("\${flock.eco.workday.login}")
+ private val loginType: String,
+ private val userAccountService: UserAccountService,
+ userAuthorityService: UserAuthorityService,
+ private val objectMapper: ObjectMapper
+) {
+
+ val data: MutableSet = mutableSetOf()
+
+ val workerRoles = setOf(
+ HolidayAuthority.READ,
+ HolidayAuthority.WRITE,
+ SickdayAuthority.READ,
+ SickdayAuthority.WRITE,
+ WorkDayAuthority.READ,
+ WorkDayAuthority.WRITE,
+ WorkDayAuthority.TOTAL_HOURS,
+ ExpenseAuthority.READ,
+ ExpenseAuthority.WRITE
+ )
+ private val allAuthorities = userAuthorityService.allAuthorities()
+
+ private val workerAuthorities = userAuthorityService.allAuthorities().filter { workerRoles.contains(it) }
+
+ init {
+ if (loginType == "KRATOS") {
+ mockUsers.forEach { createUserAccountOAuth(it, getKratosIdentities()) }
+ } else {
+ mockUsers.forEach { createUserAccountPassword(it) }
+ }
+
+ }
+
+ private fun getKratosIdentities() = try {
+ objectMapper.readValue>(File(KRATOS_IDENTITIES_FILE_LOCATION))
+ } catch (e: IOException) {
+ LOG.warn("Could not find existing Kratos identities. Will create users without Kratos link")
+ throw IllegalStateException(
+ "Kratos identities could not be found and linked to users known in Workday. " +
+ "Be sure to run `docker compose up -d` to generate the kratos identity file"
+ )
+ }
+
+ private final fun createUserAccountPassword(mockUser: MockUser) = UserAccountPasswordForm(
+ name = mockUser.firstName,
+ email = "${mockUser.firstName.lowercase()}@sesam.straat",
+ password = mockUser.firstName.lowercase(),
+ authorities = mockUser.authorities.map { it.toName() }.toSet()
+ )
+ .save()
+
+
+ private final fun createUserAccountOAuth(mockUser: MockUser, kratosIdentities: List): User {
+ val email = "${mockUser.firstName.lowercase()}@sesam.straat"
+ return UserAccountOauthForm(
+ name = mockUser.firstName,
+ email = email,
+ provider = UserAccountOauthProvider.KRATOS,
+ reference = kratosIdentities.find { it.email == email }?.id ?: mockUser.kratosId,
+ authorities = mockUser.authorities.map { it.toName() }.toSet()
+ )
+ .save()
+ }
+
+ val MockUser.authorities: List
+ get() {
+ return when (role) {
+ Role.ADMIN -> allAuthorities
+ Role.USER -> workerAuthorities
+ }
+ }
+
+
+ private fun UserAccountPasswordForm.save(): User =
+ userAccountService.createUserAccountPassword(this)
+ .user
+ .also { data.add(it) }
+
+ private fun UserAccountOauthForm.save(): User =
+ userAccountService.createUserAccountOauth(this)
+ .user
+ .also { data.add(it) }
+
+ companion object {
+ private const val KRATOS_IDENTITIES_FILE_LOCATION = "./docker/kratos/identities/existing_identities.json"
+ private val LOG: Logger = LoggerFactory.getLogger(LoadUserData::class.java)
+ }
+}
diff --git a/src/develop/kotlin/mocks/LoadWorkDayData.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/LoadWorkDayData.kt
similarity index 100%
rename from src/develop/kotlin/mocks/LoadWorkDayData.kt
rename to src/develop/kotlin/community/flock/eco/workday/mocks/LoadWorkDayData.kt
diff --git a/src/develop/kotlin/community/flock/eco/workday/mocks/Users.kt b/src/develop/kotlin/community/flock/eco/workday/mocks/Users.kt
new file mode 100644
index 000000000..70e532bcf
--- /dev/null
+++ b/src/develop/kotlin/community/flock/eco/workday/mocks/Users.kt
@@ -0,0 +1,44 @@
+package community.flock.eco.workday.mocks
+
+import java.time.LocalDate
+
+enum class Role {
+ USER, ADMIN
+}
+
+data class MockUser(
+ val firstName: String,
+ val lastName: String,
+ val kratosId: String,
+ val role: Role = Role.USER,
+ val active: Boolean = true,
+ val birthdate: LocalDate? = null,
+ val joinDate: LocalDate? = null
+)
+
+data class KratosIdentity(val id: String, val email: String)
+
+val mockUsers = listOf(
+ MockUser(
+ "Tommy",
+ "Dog",
+ kratosId = "00000000-0000-0000-0000-000000000001",
+ birthdate = LocalDate.of(1980, 5, 8),
+ joinDate = LocalDate.of(2000, 5, 8)
+ ),
+ MockUser(
+ "Ieniemienie",
+ "Mouse",
+ kratosId = "00000000-0000-0000-0000-000000000002",
+ birthdate = LocalDate.now()
+ ),
+ MockUser(
+ "Pino",
+ "Woodpecker",
+ kratosId = "00000000-0000-0000-0000-000000000003",
+ joinDate = LocalDate.of(1983, 6, 7)
+ ),
+ MockUser("Bert", "Muppets", kratosId = "00000000-0000-0000-0000-000000000004", role = Role.ADMIN),
+ MockUser("Ernie", "Muppets", kratosId = "00000000-0000-0000-0000-000000000005"),
+ MockUser("Aart", "Staartjes", kratosId = "00000000-0000-0000-0000-000000000006", active = false)
+)
diff --git a/src/develop/kotlin/mocks/LoadUserData.kt b/src/develop/kotlin/mocks/LoadUserData.kt
deleted file mode 100644
index aecea41fc..000000000
--- a/src/develop/kotlin/mocks/LoadUserData.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-package community.flock.eco.workday.mocks
-
-import community.flock.eco.core.authorities.Authority
-import community.flock.eco.feature.user.forms.UserAccountPasswordForm
-import community.flock.eco.feature.user.model.User
-import community.flock.eco.feature.user.services.UserAccountService
-import community.flock.eco.feature.user.services.UserAuthorityService
-import community.flock.eco.workday.authorities.ExpenseAuthority
-import community.flock.eco.workday.authorities.HolidayAuthority
-import community.flock.eco.workday.authorities.SickdayAuthority
-import community.flock.eco.workday.authorities.WorkDayAuthority
-import mocks.Role
-import mocks.users
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
-import org.springframework.stereotype.Component
-
-@Component
-@ConditionalOnProperty(prefix = "flock.eco.workday", name = ["develop"])
-class LoadUserData(
- private val userAccountService: UserAccountService,
- userAuthorityService: UserAuthorityService
-) {
- val data: MutableSet = mutableSetOf()
-
- val workerRoles = setOf(
- HolidayAuthority.READ,
- HolidayAuthority.WRITE,
- SickdayAuthority.READ,
- SickdayAuthority.WRITE,
- WorkDayAuthority.READ,
- WorkDayAuthority.WRITE,
- WorkDayAuthority.TOTAL_HOURS,
- ExpenseAuthority.READ,
- ExpenseAuthority.WRITE
- )
-
- private val allAuthorities = userAuthorityService.allAuthorities()
- private val workerAuthorities = userAuthorityService.allAuthorities().filter { workerRoles.contains(it) }
-
- init {
- users.forEach { create(it) }
- }
-
- private final fun create(user: mocks.User) = UserAccountPasswordForm(
- name = user.firstName,
- email = "${user.firstName.toLowerCase()}@sesam.straat",
- password = user.firstName.toLowerCase(),
- authorities = user.authorities.map { it.toName() }.toSet()
- )
- .save()
-
- val mocks.User.authorities: List
- get() {
- return when (role) {
- Role.ADMIN -> allAuthorities
- Role.USER -> workerAuthorities
- }
- }
-
- private fun UserAccountPasswordForm.save(): User =
- userAccountService.createUserAccountPassword(this)
- .user
- .also { data.add(it) }
-}
diff --git a/src/develop/kotlin/mocks/Users.kt b/src/develop/kotlin/mocks/Users.kt
deleted file mode 100644
index bba080647..000000000
--- a/src/develop/kotlin/mocks/Users.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package mocks
-
-import java.time.LocalDate
-
-enum class Role {
- USER, ADMIN
-}
-
-data class User(
- val firstName: String,
- val lastName: String,
- val role: Role = Role.USER,
- val active: Boolean = true,
- val birthdate: LocalDate? = null,
- val joinDate: LocalDate? = null
-)
-
-val users = listOf(
- User(
- "Tommy",
- "Dog",
- birthdate = LocalDate.of(1980, 5, 8),
- joinDate = LocalDate.of(2000, 5, 8)
- ),
- User(
- "Ieniemienie",
- "Mouse",
- birthdate = LocalDate.now()
- ),
- User(
- "Pino",
- "Woodpecker",
- joinDate = LocalDate.of(1983, 6, 7)
- ),
- User("Bert", "Muppets", role = Role.ADMIN),
- User("Ernie", "Muppets"),
- User("Aart", "Staartjes", active = false)
-)
diff --git a/src/main/kotlin/authentication/KratosIdentity.kt b/src/main/kotlin/authentication/KratosIdentity.kt
index c52b72ce7..030671590 100644
--- a/src/main/kotlin/authentication/KratosIdentity.kt
+++ b/src/main/kotlin/authentication/KratosIdentity.kt
@@ -1,13 +1,5 @@
package community.flock.eco.workday.authentication
-data class KratosIdentity(
- val userId: UserId,
- val emailAddress: EmailAddress
-) {
- @JvmInline
- value class UserId(val value: String)
-
- @JvmInline
- value class EmailAddress(val value: String)
-}
+@JvmInline
+value class KratosUserId(val value: String)
diff --git a/src/main/kotlin/authentication/OathkeeperProxy.kt b/src/main/kotlin/authentication/OathkeeperProxy.kt
index e58f60dff..aa370ecca 100644
--- a/src/main/kotlin/authentication/OathkeeperProxy.kt
+++ b/src/main/kotlin/authentication/OathkeeperProxy.kt
@@ -2,18 +2,19 @@ package community.flock.eco.workday.authentication
import community.flock.eco.feature.user.services.UserAccountService
import community.flock.eco.feature.user.services.UserSecurityService
-import community.flock.eco.workday.authentication.KratosIdentity.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.security.oauth2.core.oidc.OidcIdToken
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.security.web.util.matcher.RequestMatcher
import org.springframework.stereotype.Component
import java.io.IOException
+import java.time.Instant
import javax.servlet.FilterChain
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
@@ -21,29 +22,34 @@ import javax.servlet.http.HttpServletResponse
@Component
class OathkeeperProxyAuthenticationProvider(
+ private val userAccountService: UserAccountService
) : AuthenticationProvider {
- @Autowired
- lateinit var userAccountService: UserAccountService
-
@Throws(AuthenticationException::class)
override fun authenticate(authentication: Authentication): Authentication {
- val kratosIdentity = authentication.credentials as KratosIdentity?
-
- val account =
- kratosIdentity?.let {
- userAccountService.findUserAccountPasswordByUserEmail(it.emailAddress.value)
- }
+ val kratosIdentity = authentication.credentials as KratosUserId?
+ val account = kratosIdentity?.let { userAccountService.findUserAccountOauthByReference(it.value) }
return if (account != null) {
- val userSecurityPassword = UserSecurityService.UserSecurityPassword(account)
+ val userSecurityOauth2 = UserSecurityService.UserSecurityOauth2(
+ account, OidcIdToken(
+ "token-value",
+ Instant.EPOCH, Instant.MAX,
+ mapOf(
+ Pair("sub", account.reference),
+ Pair("name", account.user.name ?: "anon"),
+ Pair("email", account.user.email),
+ Pair("kratos_user_id", kratosIdentity.value)
+ )
+ )
+ )
PreAuthenticatedAuthenticationToken(
- userSecurityPassword.username,
- userSecurityPassword,
- userSecurityPassword.authorities
+ userSecurityOauth2.account.user.code,
+ userSecurityOauth2,
+ userSecurityOauth2.authorities
)
} else {
PreAuthenticatedAuthenticationToken(
- kratosIdentity?.emailAddress ?: "anonymous",
+ kratosIdentity?.value ?: "anonymous",
null
)
}
@@ -67,19 +73,11 @@ class OathkeeperProxyAuthenticationProcessingFilter(
@Throws(AuthenticationException::class, IOException::class, ServletException::class)
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
// Extract from request
- val email = request.getHeader("X-Email")
- val userId = request.getHeader("X-User");
-
- if ((email == null || userId == null)) {
- throw InvalidOathkeeperProxyHeadersException("Invalid authentication headers (X-User: $userId, X-Email: $email")
- }
- val auth = KratosIdentity(
- UserId(userId),
- EmailAddress(email)
- )
+ val userId = request.getHeader("X-User")
+ ?: throw InvalidOathkeeperProxyHeadersException("Invalid authentication headers (X-User)")
// Create a token object ot pass to Authentication Provider
- val token = PreAuthenticatedAuthenticationToken(email, auth)
+ val token = PreAuthenticatedAuthenticationToken(userId, KratosUserId(userId))
return authenticationManager.authenticate(token)
}
diff --git a/src/main/kotlin/config/WebSecurityConfig.kt b/src/main/kotlin/config/WebSecurityConfig.kt
index 800d74e68..38c5a69ef 100644
--- a/src/main/kotlin/config/WebSecurityConfig.kt
+++ b/src/main/kotlin/config/WebSecurityConfig.kt
@@ -10,6 +10,8 @@ import community.flock.eco.workday.authorities.HolidayAuthority
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
+import org.springframework.core.convert.converter.Converter
+import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
@@ -18,17 +20,50 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer
import org.springframework.security.config.http.SessionCreationPolicy
+import org.springframework.security.oauth2.core.oidc.OidcIdToken
+import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
import org.springframework.security.web.util.matcher.NegatedRequestMatcher
import org.springframework.stereotype.Component
-
+import java.util.*
+
+internal class CustomAuthenticationConverter(
+ private val userAccountService: UserAccountService,
+) : Converter {
+ override fun convert(jwt: Jwt): AbstractAuthenticationToken {
+
+ val account = userAccountService.findUserAccountOauthByReference(jwt.subject)
+ return if (account != null) {
+ val userSecurityOauth2 = UserSecurityService.UserSecurityOauth2(
+ account, OidcIdToken(
+ jwt.tokenValue,
+ jwt.issuedAt, jwt.expiresAt,
+ jwt.claims
+ )
+ )
+ PreAuthenticatedAuthenticationToken(
+ userSecurityOauth2.account.user.code,
+ userSecurityOauth2,
+ userSecurityOauth2.authorities
+ )
+ } else {
+ PreAuthenticatedAuthenticationToken(
+ jwt.subject ?: "anonymous",
+ null
+ )
+ }
+ }
+}
@Component
class MyUserSecurityService(
private val userSecurityService: UserSecurityService,
- private val oathkeeperProxyAuthenticationProvider:OathkeeperProxyAuthenticationProvider
+ private val userAccountService: UserAccountService,
+ private val oathkeeperProxyAuthenticationProvider: OathkeeperProxyAuthenticationProvider
) {
+
fun googleLogin(http: HttpSecurity): OAuth2LoginConfigurer.UserInfoEndpointConfig {
return userSecurityService.googleLogin(http)
}
@@ -37,12 +72,29 @@ class MyUserSecurityService(
return userSecurityService.databaseLogin(http)
}
- fun kratosLogin(http: HttpSecurity, authenticationManager: AuthenticationManager): Unit {
+ fun kratosLoginJWT(http: HttpSecurity, authenticationManager: AuthenticationManager): Unit {
http
- .httpBasic().disable() //No Http Basic Login
- .formLogin().disable() //No Form Login
- .logout()
- .addLogoutHandler(RedirectLogoutHandler()) //No Logout
+ .httpBasic().disable() //No Http Basic Login
+ .formLogin().disable() //No Form Login
+ .logout().addLogoutHandler(RedirectLogoutHandler()) //No Logout
+
+ // No Session pls
+ http
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ .and()
+ .oauth2ResourceServer {
+ it.jwt { jwt ->
+ jwt.jwtAuthenticationConverter(CustomAuthenticationConverter(userAccountService))
+ }
+ }
+ }
+
+ fun kratosLoginHeader(http: HttpSecurity, authenticationManager: AuthenticationManager): Unit {
+ http
+ .httpBasic().disable() //No Http Basic Login
+ .formLogin().disable() //No Form Login
+ .logout().addLogoutHandler(RedirectLogoutHandler()) //No Logout
+
// No Session pls
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
@@ -54,7 +106,6 @@ class MyUserSecurityService(
authenticationManager
), AnonymousAuthenticationFilter::class.java
)
-
}
fun testLogin(http: HttpSecurity): FormLoginConfigurer {
@@ -75,11 +126,6 @@ class WebSecurityConfig : WebSecurityConfigurerAdapter() {
@Autowired
lateinit var userSecurityService: MyUserSecurityService
- @Autowired
- lateinit var userAccountService: UserAccountService
-
- @Autowired
- lateinit var oathkeeperProxyAuthenticationProvider: OathkeeperProxyAuthenticationProvider
@Value("\${flock.eco.workday.login:TEST}")
lateinit var loginType: String
@@ -95,11 +141,6 @@ class WebSecurityConfig : WebSecurityConfigurerAdapter() {
userAuthorityService.addAuthority(HolidayAuthority::class.java)
http
- .httpBasic().disable() //No Http Basic Login
- .formLogin().disable() //No Form Login
- .logout()
- .addLogoutHandler(RedirectLogoutHandler()) //No Logout
- .and()
.headers()
.frameOptions()
.sameOrigin()
@@ -119,19 +160,22 @@ class WebSecurityConfig : WebSecurityConfigurerAdapter() {
.antMatchers("/h2/**").permitAll()
.antMatchers("/api/events/**").permitAll()
.antMatchers(*SWAGGER_WHITELIST).permitAll()
+ .anyRequest().authenticated()
http
.cors()
- http
- // No Session pls
- .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and()
- .authenticationProvider(oathkeeperProxyAuthenticationProvider)
- .addFilterBefore(
- OathkeeperProxyAuthenticationProcessingFilter(
- NegatedRequestMatcher(AntPathRequestMatcher("/error")),
- authenticationManager()
- ), AnonymousAuthenticationFilter::class.java
- )
+
+ when (loginType.uppercase(Locale.getDefault())) {
+ "GOOGLE" ->
+ userSecurityService.googleLogin(http)
+ .and()
+ .defaultSuccessUrl("/", true)
+
+ "DATABASE" -> userSecurityService.databaseLogin(http).loginPage("/").loginProcessingUrl("/login")
+ "KRATOS" -> userSecurityService.kratosLoginJWT(http, authenticationManager())
+ else -> userSecurityService.testLogin(http).loginPage("/").loginProcessingUrl("/login")
+ }
+
+
}
}
diff --git a/src/main/resources/application-develop-kratos.properties b/src/main/resources/application-develop-kratos.properties
new file mode 100644
index 000000000..eb0c57f69
--- /dev/null
+++ b/src/main/resources/application-develop-kratos.properties
@@ -0,0 +1,47 @@
+# trim me down to only the kratos related properties. I should only be used in combination with the `develop` profile / properties
+flock.eco.cloud.stub.enabled=true
+
+flock.eco.workday.develop=true
+flock.eco.workday.login=KRATOS
+flock.eco.workday.bucket.documents=flock-eco-workday-documents
+
+
+spring.security.oauth2.resourceserver.jwt.issuer-uri= http://accounts.flock.local:8081
+spring.security.oauth2.resourceserver.jwt.jws-algorithm=RS256
+spring.security.oauth2.resourceserver.jwt.jwk-set-uri= http://accounts.flock.local:8081/.well-known/jwks.json
+
+spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE
+spring.datasource.username=sa
+spring.datasource.password=
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.platform=h2
+
+spring.h2.console.enabled=true
+spring.h2.console.path=/h2
+spring.h2.console.settings.web-allow-others=true
+
+spring.jpa.database=h2
+spring.cloud.gcp.sql.enabled=false
+
+logging.level.org.hibernate.SQL=DEBUG
+logging.level.org.springframework.security=DEBUG
+logging.level.org.springframework.security.web.authentication=TRACE
+
+server.port=${PORT:8080}
+server.forward-headers-strategy=NATIVE
+server.compression.enabled=true
+server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css,image/jpeg
+
+flock.eco.feature.exactonline.clientId=745c6eae-3f5c-4ccd-ae78-4217ff32d621
+flock.eco.feature.exactonline.clientSecret=eEVRgN8GU8wJ
+flock.eco.feature.exactonline.redirectUri=http://localhost:3000/api/exactonline/redirect
+flock.eco.feature.exactonline.requestUri=https://start.exactonline.nl
+
+workday.average_sick_days = 5
+workday.average_training_days = 5
+workday.average_public_days = 9
+workday.average_holi_days = 25
+
+spring.cloud.gcp.storage.enabled = false
+#spring.cloud.gcp.credentials.location=file:token.json
+google.drive.sheets.workday.templateId=16dSJHdp-LphqhpodNPrpDCOJLSJmDEe-NNGiZR0lbqM
diff --git a/src/main/resources/application-develop.properties b/src/main/resources/application-develop.properties
index 7114c84db..45cea5d73 100644
--- a/src/main/resources/application-develop.properties
+++ b/src/main/resources/application-develop.properties
@@ -19,7 +19,6 @@ spring.cloud.gcp.sql.enabled=false
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.springframework.security=DEBUG
-logging.level.org.springframework.security.web.authentication=TRACE
server.port=${PORT:8080}
server.forward-headers-strategy=NATIVE