diff --git a/.gitignore b/.gitignore
index 1a91d71400bc0ab62c6332878eb62cbd63213d48..e00963ccc4a37100a59a7cd02cec34ccf14d2c67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,5 +6,9 @@ env/
 venv/
 ENV/
 .coverage
-oidc_dev/
-traefik
+config.yaml
+config.json
+config.toml
+test-config.yaml
+test-config.json
+test-config.toml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9ae62486846705eb350d515dd492200b20bc2133..8fa9c3b423e8bd526327603778b98d847c52cc3b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,21 +1,20 @@
 image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/python:3.12-slim
 
 variables:
+  FF_NETWORK_PER_BUILD: 1
   PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
   PYTHONPATH: "$CI_PROJECT_DIR"
-  OBJECT_GATEWAY_URI: "http://127.0.0.1:8000"
-  BUCKET_CEPH_ACCESS_KEY: ""
-  BUCKET_CEPH_SECRET_KEY: ""
-  USER_CEPH_ACCESS_KEY: ""
-  USER_CEPH_SECRET_KEY: ""
-  BUCKET_CEPH_USERNAME: ""
-  FF_NETWORK_PER_BUILD: 1
-  PUBLIC_KEY_VALUE: "empty"
-  OPA_URI: "http://127.0.0.1:8181"
-  DB_PASSWORD: "random"
-  DB_USER: "random"
-  DB_DATABASE: "random"
-  DB_HOST: "random"
+  CLOWM_S3__ADMIN_ACCESS_KEY: "nonempty"
+  CLOWM_S3__ADMIN_SECRET_KEY: "nonempty"
+  CLOWM_S3__USERNAME: "clowm-bucket-manager"
+  CLOWM_S3__URI: "http://127.0.0.1:8001"
+  CLOWM_S3__ACCESS_KEY: "nonempty"
+  CLOWM_S3__SECRET_KEY: "nonempty"
+  CLOWM_PUBLIC_KEY: "nonempty"
+  CLOWM_OPA__URI: "http://127.0.0.1:8181"
+  CLOWM_CLUSTER__SLURM__TOKEN: "empty"
+  CLOWM_CLUSTER__SLURM__URI: "http://127.0.0.1:8002"
+  CLOWM_UI_URI: "http://localhost"
 
 cache:
   paths:
@@ -33,26 +32,43 @@ default:
     - python -m pip install --disable-pip-version-check --upgrade -r requirements.txt -r requirements-dev.txt
 
 stages: # List of stages for jobs, and their order of execution
+  - lint
   - test
   - deploy
 
+lint-test-job: # Runs linters checks on code
+  stage: lint
+  before_script:
+    - python --version  # For debugging
+    - pip install --disable-pip-version-check virtualenv
+    - virtualenv venv
+    - source venv/bin/activate
+    - python -m pip install --disable-pip-version-check --upgrade -r requirements.txt -r requirements-dev.txt --upgrade-strategy=eager
+  script:
+    - ./scripts/lint.sh
+
 integration-test-job: # Runs integration tests with the database
   stage: test
   variables:
-    DB_PASSWORD: "$TEST_DB_PASSWORD"
-    DB_USER: "test_api_user"
-    DB_DATABASE: "integration-test-db"
-    DB_HOST: "integration-test-db"
+    CLOWM_DB__PASSWORD: "$TEST_DB_PASSWORD"
+    CLOWM_DB__USER: "test_api_user"
+    CLOWM_DB__NAME: "integration-test-db"
+    CLOWM_DB__HOST: "integration-test-db"
   services:
     - name: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/mysql:8
       alias: integration-test-db
       variables:
         MYSQL_RANDOM_ROOT_PASSWORD: "yes"
-        MYSQL_DATABASE: "$DB_DATABASE"
-        MYSQL_USER: "$DB_USER"
-        MYSQL_PASSWORD: "$DB_PASSWORD"
+        MYSQL_DATABASE: "$CLOWM_DB__NAME"
+        MYSQL_USER: "$CLOWM_DB__USER"
+        MYSQL_PASSWORD: "$CLOWM_DB__PASSWORD"
     - name: $CI_REGISTRY/cmg/clowm/clowm-database:v3.1
       alias: upgrade-db
+      variables:
+        DB_HOST: "$CLOWM_DB__HOST"
+        DB_DATABASE: "$CLOWM_DB__NAME"
+        DB_USER: "$CLOWM_DB__USER"
+        DB_PASSWORD: "$CLOWM_DB__PASSWORD"
   script:
     - python app/check_database_connection.py
     - pytest --junitxml=integration-report.xml --cov=app --cov-report=term-missing app/tests/crud
@@ -67,20 +83,25 @@ integration-test-job: # Runs integration tests with the database
 e2e-test-job: # Runs e2e tests on the API endpoints
   stage: test
   variables:
-    DB_PASSWORD: "$TEST_DB_PASSWORD"
-    DB_USER: "test_api_user"
-    DB_DATABASE: "e2e-test-db"
-    DB_HOST: "e2e-test-db"
+    CLOWM_DB__PASSWORD: "$TEST_DB_PASSWORD"
+    CLOWM_DB__USER: "test_api_user"
+    CLOWM_DB__NAME: "e2e-test-db"
+    CLOWM_DB__HOST: "e2e-test-db"
   services:
     - name: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/mysql:8
       alias: e2e-test-db
       variables:
         MYSQL_RANDOM_ROOT_PASSWORD: "yes"
-        MYSQL_DATABASE: "$DB_DATABASE"
-        MYSQL_USER: "$DB_USER"
-        MYSQL_PASSWORD: "$DB_PASSWORD"
+        MYSQL_DATABASE: "$CLOWM_DB__NAME"
+        MYSQL_USER: "$CLOWM_DB__USER"
+        MYSQL_PASSWORD: "$CLOWM_DB__PASSWORD"
     - name: $CI_REGISTRY/cmg/clowm/clowm-database:v3.1
       alias: upgrade-db
+      variables:
+        DB_HOST: "$CLOWM_DB__HOST"
+        DB_DATABASE: "$CLOWM_DB__NAME"
+        DB_USER: "$CLOWM_DB__USER"
+        DB_PASSWORD: "$CLOWM_DB__PASSWORD"
   script:
     - python app/check_database_connection.py
     - pytest --junitxml=e2e-report.xml --cov=app --cov-report=term-missing app/tests/api
@@ -94,8 +115,13 @@ e2e-test-job: # Runs e2e tests on the API endpoints
 
 unit-test-job: # Runs unit tests
   stage: test
+  variables:
+    CLOWM_DB__PASSWORD: "nonempty"
+    CLOWM_DB__USER: "nonempty"
+    CLOWM_DB__NAME: "nonempty"
+    CLOWM_DB__HOST: "nonempty"
   script:
-    - pytest --junitxml=unit-report.xml --noconftest --cov=app --cov-report=term-missing app/tests/unit
+    - pytest --junitxml=unit-report.xml --cov=app --cov-report=term-missing app/tests/unit
     - mkdir coverage-unit
     - mv .coverage coverage-unit
   artifacts:
@@ -124,45 +150,48 @@ combine-test-coverage-job: # Combine coverage reports from different test jobs
         coverage_format: cobertura
         path: $CI_PROJECT_DIR/coverage.xml
 
-lint-test-job: # Runs linters checks on code
-  stage: test
-  script:
-    - ./scripts/lint.sh
-
-publish-main-docker-container-job:
+.publish-docker-container:
   stage: deploy
   image:
-    name: gcr.io/kaniko-project/executor:v1.20.1-debug
-    entrypoint: [""]
-  dependencies: []
+    name: gcr.io/kaniko-project/executor:v1.22.0-debug
+    entrypoint: [ "" ]
+  dependencies: [ ]
+  cache: [ ]
+  before_script:
+    - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"},\"$CI_DEPENDENCY_PROXY_SERVER\":{\"auth\":\"$(printf "%s:%s" ${CI_DEPENDENCY_PROXY_USER} "${CI_DEPENDENCY_PROXY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
+
+.publish-main-docker-container:
+  extends: .publish-docker-container
   only:
     refs:
       - main
-  before_script:
-    - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"},\"$CI_DEPENDENCY_PROXY_SERVER\":{\"auth\":\"$(printf "%s:%s" ${CI_DEPENDENCY_PROXY_USER} "${CI_DEPENDENCY_PROXY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
+
+publish-main-docker-container-job:
+  extends: .publish-main-docker-container
   script:
     - /kaniko/executor
       --context "${CI_PROJECT_DIR}"
       --dockerfile "${CI_PROJECT_DIR}/Dockerfile"
       --destination "${CI_REGISTRY_IMAGE}:main-${CI_COMMIT_SHA}"
       --destination "${CI_REGISTRY_IMAGE}:main-latest"
-      --cleanup
+
+publish-main-gunicorn-docker-container-job:
+  extends: .publish-main-docker-container
+  script:
     - /kaniko/executor
       --context "${CI_PROJECT_DIR}"
       --dockerfile "${CI_PROJECT_DIR}/Dockerfile-Gunicorn"
-      --destination "${CI_REGISTRY_IMAGE}:main-${CI_COMMIT_SHA}-gunicorn"
-      --destination "${CI_REGISTRY_IMAGE}:main-latest-gunicorn"
+      --destination "${CI_REGISTRY_IMAGE}:main-gunicorn-${CI_COMMIT_SHA}"
+      --destination "${CI_REGISTRY_IMAGE}:main-gunicorn-latest"
 
-publish-docker-container-job:
-  stage: deploy
-  image:
-    name: gcr.io/kaniko-project/executor:v1.20.1-debug
-    entrypoint: [""]
-  dependencies: []
+
+.publish-version-docker-container:
+  extends: .publish-docker-container
   only:
     - tags
-  before_script:
-    - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"},\"$CI_DEPENDENCY_PROXY_SERVER\":{\"auth\":\"$(printf "%s:%s" ${CI_DEPENDENCY_PROXY_USER} "${CI_DEPENDENCY_PROXY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
+
+publish-docker-container-job:
+  extends: .publish-version-docker-container
   script:
     - /kaniko/executor
       --context "${CI_PROJECT_DIR}"
@@ -171,11 +200,15 @@ publish-docker-container-job:
       --destination "${CI_REGISTRY_IMAGE}:$(echo ${CI_COMMIT_TAG} | cut -d'.' -f1-2)"
       --destination "${CI_REGISTRY_IMAGE}:$(echo ${CI_COMMIT_TAG} | cut -d'.' -f1)"
       --destination "${CI_REGISTRY_IMAGE}:latest"
-      --cleanup
+
+
+publish-gunicorn-docker-container-job:
+  extends: .publish-version-docker-container
+  script:
     - /kaniko/executor
       --context "${CI_PROJECT_DIR}"
       --dockerfile "${CI_PROJECT_DIR}/Dockerfile-Gunicorn"
-      --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}-gunicorn"
-      --destination "${CI_REGISTRY_IMAGE}:$(echo ${CI_COMMIT_TAG} | cut -d'.' -f1-2)-gunicorn"
-      --destination "${CI_REGISTRY_IMAGE}:$(echo ${CI_COMMIT_TAG} | cut -d'.' -f1)-gunicorn"
-      --destination "${CI_REGISTRY_IMAGE}:latest-gunicorn"
+      --destination "${CI_REGISTRY_IMAGE}:gunicorn-${CI_COMMIT_TAG}"
+      --destination "${CI_REGISTRY_IMAGE}:gunicorn-$(echo ${CI_COMMIT_TAG} | cut -d'.' -f1-2)"
+      --destination "${CI_REGISTRY_IMAGE}:gunicorn-$(echo ${CI_COMMIT_TAG} | cut -d'.' -f1)"
+      --destination "${CI_REGISTRY_IMAGE}:gunicorn-latest"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index dfe22a4e1c0381be7e5f6756580484f5cfb761d4..ff91b0d74cffb33ef2ced1ef0de704b7ffe3171f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@
 # See https://pre-commit.com/hooks.html for more hooks
 repos:
 -   repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.5.0
+    rev: v4.6.0
     hooks:
     -   id: end-of-file-fixer
     -   id: check-added-large-files
@@ -15,13 +15,13 @@ repos:
     -   id: check-merge-conflict
     -   id: check-ast
 -   repo: https://github.com/psf/black
-    rev: 24.2.0
+    rev: 24.4.0
     hooks:
     -   id: black
         files: app
         args: [--check]
 -   repo: https://github.com/charliermarsh/ruff-pre-commit
-    rev: 'v0.2.2'
+    rev: 'v0.3.7'
     hooks:
     -   id: ruff
 -   repo: https://github.com/PyCQA/isort
@@ -31,7 +31,7 @@ repos:
         files: app
         args: [-c]
 -   repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.8.0
+    rev: v1.9.0
     hooks:
     -   id: mypy
         files: app
@@ -39,5 +39,5 @@ repos:
         additional_dependencies:
             - boto3-stubs-lite[s3]<1.35.0
             - sqlalchemy>=2.0.0,<2.1.0
-            - pydantic<2.7.0
+            - pydantic<2.8.0
             - types-requests
diff --git a/Dockerfile b/Dockerfile
index 5650e0243160cc2f3003094c0fcbc06e3cc9ed74..60a1b2ad47d72ec322409f29fff467bddb7ab16e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,13 +3,12 @@ ENV PORT=8000
 EXPOSE $PORT
 
 # dumb-init forwards the kill signal to the python process
-RUN apt-get update && apt-get -y install dumb-init
-RUN apt-get clean
+RUN apt-get update && apt-get -y install dumb-init && apt-get clean
 ENTRYPOINT ["/usr/bin/dumb-init", "--"]
 STOPSIGNAL SIGINT
-RUN pip install --disable-pip-version-check --no-cache-dir httpx[cli] "uvicorn<0.28.0"
+RUN pip install --disable-pip-version-check --no-cache-dir httpx[cli] "uvicorn<0.30.0"
 
-HEALTHCHECK --interval=30s --timeout=2s CMD httpx http://localhost:$PORT/health || exit 1
+HEALTHCHECK --interval=5s --timeout=2s CMD httpx http://localhost:$PORT/health || exit 1
 
 RUN useradd -m worker
 USER worker
diff --git a/Dockerfile-Gunicorn b/Dockerfile-Gunicorn
index 916c9c2afcd7dcbda7b4f2b9dd9cb7ed83750d21..f5a25296798f3ba88366ac6f9ea9f3c551a1432f 100644
--- a/Dockerfile-Gunicorn
+++ b/Dockerfile-Gunicorn
@@ -4,11 +4,11 @@ EXPOSE $PORT
 WORKDIR /app/
 ENV PYTHONPATH=/app
 
-RUN pip install --disable-pip-version-check --no-cache-dir httpx[cli] "gunicorn<21.3.0" "uvicorn<0.28.0"
+RUN pip install --disable-pip-version-check --no-cache-dir httpx[cli] "gunicorn<21.3.0" "uvicorn<0.30.0"
 COPY ./gunicorn_conf.py /app/gunicorn_conf.py
 COPY ./start_service_gunicorn.sh /app/entrypoint.sh
 
-HEALTHCHECK --interval=30s --timeout=2s CMD httpx http://localhost:$PORT/health || exit 1
+HEALTHCHECK --interval=5s --timeout=2s CMD httpx http://localhost:$PORT/health || exit 1
 
 COPY ./scripts/prestart.sh /app/prestart.sh
 COPY ./requirements.txt /app/requirements.txt
diff --git a/README.md b/README.md
index 7f4f57c555dfbf235a13bf4a87c0006c3ffd22a1..78b3c31e9850546d555f364268aece47cbb4bfbd 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
 # CloWM S3Proxy Service
 
 ## Description
-Openstack is shipping with an integrated UI to access the Object Store provided by Ceph. Unfortunately, this UI does not allow
+
+Openstack is shipping with an integrated UI to access the Object Store provided by Ceph. Unfortunately, this UI does not
+allow
 fine-grained control who can access a bucket or object. You can either make it accessible for everyone or nobody, but
 Ceph can do this and much more. 👎
 This is the backend for a new UI which can leverage the additional powerful functionality provided by Ceph in a
@@ -15,38 +17,59 @@ user-friendly manner. 👍
 | Fine-grained Access Control |           ❌           |   ✅    |
 
 ### Concept
+
 ![Visualization of Concept](figures/cloud_object_storage.svg)
 
-## Environment Variables
-
-### Mandatory / Recommended Variables
-
-| Variable                               | Default                 | Value                           | Description                                                                        |
-|----------------------------------------|-------------------------|---------------------------------|------------------------------------------------------------------------------------|
-| `DB_HOST`                              | unset                   | <db hostname / IP>              | IP or Hostname Address of DB                                                       |
-| `DB_PORT`                              | 3306                    | Number                          | Port of the database                                                               |
-| `DB_USER`                              | unset                   | \<db username>                  | Username of the database user                                                      |
-| `DB_PASSWORD`                          | unset                   | \<db password>                  | Password of the database user                                                      |
-| `DB_DATABASE`                          | unset                   | \<db name>                      | Name of the database                                                               |
-| `OBJECT_GATEWAY_URI`                   | unset                   | HTTP URL                        | HTTP URL of the Ceph Object Gateway                                                |
-| `BUCKET_CEPH_ACCESS_KEY`               | unset                   | \<access key>                   | Access key for the Ceph Object Gateway user with unlimited buckets.                |
-| `BUCKET_CEPH_SECRET_KEY`               | unset                   | \<secret key>                   | Secret key for the Ceph Object Gateway user with unlimited buckets.                |
-| `BUCKET_CEPH_USERNAME`                 | unset                   | \<ceph username>                | ID of the user in ceph who owns all the buckets. Owner of `BUCKET_CEPH_ACCESS_KEY` |
-| `USER_CEPH_ACCESS_KEY`                 | unset                   | \<access key>                   | Access key for the Ceph Object Gateway user with `user:*` privileges               |
-| `USER_CEPH_SECRET_KEY`                 | unset                   | \<secret key>                   | Secret key for the Ceph Object Gateway user with `user:*` privileges.              |
-| `PUBLIC_KEY_VALUE` / `PUBLIC_KEY_FILE` | randomly generated      | Public Key / Path to Public Key | Public part of RSA Key in PEM format to verify JWTs                                |
-| `OPA_URI`                              | unset                   | HTTP URL                        | HTTP URL of the OPA service                                                        |
-| `CLOWM_URL`                            | `http://localhost:8080` | HTTP URL                        | HTTP URL of the CloWM website                                                      |
-
-### Optional Variables
-
-| Variable                    | Default              | Value                       | Description                                                    |
-|-----------------------------|----------------------|-----------------------------|----------------------------------------------------------------|
-| `API_PREFIX`                | `/api`               | URL path                    | Prefix before every URL path                                   |
-| `SQLALCHEMY_VERBOSE_LOGGER` | `false`              | `<"true"&#x7c;"false">`     | Enables verbose SQL output.<br>Should be `false` in production |
-| `OPA_POLICY_PATH`           | `/clowm/authz/allow` | URL path                    | Path to the OPA Policy for Authorization                       |
-| `OTLP_GRPC_ENDPOINT`        | unset                | <hostname / IP>             | OTLP compatible endpoint to send traces via gRPC, e.g. Jaeger  |
+## Configuration
+
+### General
+
+| Env variable             | Config file key | Default       | Value    | Example                | Description                                                                                                           |
+|--------------------------|-----------------|---------------|----------|------------------------|-----------------------------------------------------------------------------------------------------------------------|
+| `CLOWM_CONFIG_FILE_YAML` | -               | `config.yaml` | Filepath | `/path/to/config.yaml` | Path to a YAML file to read the config. See [example-config/example-config.yaml](example-config/example-config.yaml). |
+| `CLOWM_CONFIG_FILE_TOML` | -               | `config.toml` | Filepath | `/path/to/config.toml` | Path to a TOML file to read the config. See [example-config/example-config.toml](example-config/example-config.toml). |
+| `CLOWM_CONFIG_FILE_JSON` | -               | `config.json` | Filepath | `/path/to/config.json` | Path to a JSON file to read the config. See [example-config/example-config.json](example-config/example-config.json). |
+| `CLOWM_API_PREFIX`       | `api_prefix`    | unset         | URI path | `/api`                 | Prefix before every URL path                                                                                          |
+| * `CLOWM_UI_URI`         | `ui_uri`        | unset         | HTTP URL | `https://localhost`    | HTTP URL of the CloWM website                                                                                         |
+
+### Database
+
+| Env variable           | Config file key | Default     | Value              | Example       | Description                                                    |
+|------------------------|-----------------|-------------|--------------------|---------------|----------------------------------------------------------------|
+| `CLOWM_DB__HOST`       | `db.host`       | `localhost` | <db hostname / IP> | `localhost`   | IP or Hostname Address of DB                                   |
+| `CLOWM_DB__PORT`       | `db.port`       | 3306        | Integer            | 3306          | Port of the database                                           |
+| * `CLOWM_DB__USER`     | `db.user`       | unset       | String             | `db-user`     | Username of the database user                                  |
+| * `CLOWM_DB__PASSWORD` | `db.password`   | unset       | String             | `db-password` | Password of the database user                                  |
+| * `CLOWM_DB__NAME`     | `db.name`       | unset       | String             | `db-name`     | Name of the database                                           |
+| `CLOWM_DB__VERBOSE`    | `db.verbose`    | `false`     | Boolean            | `false`       | Enables verbose SQL output.<br>Should be `false` in production |
+
+### S3
+
+| Env variable                   | Config file key       | Default | Value    | Example                  | Description                                                                      |
+|--------------------------------|-----------------------|---------|----------|--------------------------|----------------------------------------------------------------------------------|
+| * `CLOWM_S3__URI`              | `s3.uri`              | unset   | HTTP URL | `http://localhost`       | URI of the S3 Object Storage                                                     |
+| * `CLOWM_S3__ACCESS_KEY`       | `s3.acess_key`        | unset   | String   | `ZR7U56KMK20VW`          | Access key for the S3 that owns the buckets                                      |
+| * `CLOWM_S3__SECRET_KEY`       | `s3.secret_key`       | unset   | String   | `9KRUU41EGSCB3H9ODECNHW` | Secret key for the S3 that owns the buckets                                      |
+| * `CLOWM_S3__USERNAME`         | `s3.username`         | unset   | String   | `clowm-bucket-manager`   | ID of the user in ceph who owns all the buckets. Owner of `CLOWM_S3__ACCESS_KEY` |
+| * `CLOWM_S3__ADMIN_ACCESS_KEY` | `s3.admin_acess_key`  | unset   | String   | `ZR7U56KMK20VW`          | Access key for the Ceph Object Gateway user with `user:*` privileges             |
+| * `CLOWM_S3__ADMIN_SECRET_KEY` | `s3.admin_secret_key` | unset   | String   | `9KRUU41EGSCB3H9ODECNHW` | Secret key for the Ceph Object Gateway user with `user:*` privileges.            |
+
+### Security
+
+| Env variable                                   | Config file key                  | Default | Value                           | Example            | Description                                         |
+|------------------------------------------------|----------------------------------|---------|---------------------------------|--------------------|-----------------------------------------------------|
+| * `CLOWM_PUBLIC_KEY` / `CLOWM_PUBLIC_KEY_FILE` | `public_key` / `public_key_file` | unset   | Public Key / Path to Public Key | `/path/to/key.pub` | Public part of RSA Key in PEM format to verify JWTs |
+| * `CLOWM_OPA__URI`                             | `opa.uri`                        | unset   | HTTP URL                        | `http://localhost` | URI of the OPA Service                              |
+
+### Monitoring
+
+| Env variable                | Config file key      | Default | Value   | Example     | Description                                                                                  |
+|-----------------------------|----------------------|---------|---------|-------------|----------------------------------------------------------------------------------------------|
+| `CLOWM_OTLP__GRPC_ENDPOINT` | `otlp.grpc_endpoint` | unset   | String  | `localhost` | OTLP compatible endpoint to send traces via gRPC, e.g. Jaeger. If unset, no traces are sent. |
+| `CLOWM_OTLP__SECURE`        | `otlp.secure`        | `false` | Boolean | `false`     | Connection type                                                                              |
+
 
 ## License
 
-The API is licensed under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. See the [License](LICENSE) file for more information.
+The API is licensed under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. See
+the [License](LICENSE) file for more information.
diff --git a/app/api/dependencies.py b/app/api/dependencies.py
index e05eaddec0b7ec691ee6a4aee8fd53896ee9f354..e391753eea990287b98f6fb0a8e181fa50c4a74c 100644
--- a/app/api/dependencies.py
+++ b/app/api/dependencies.py
@@ -55,9 +55,7 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:  # pragma: no cover
     db : AsyncGenerator[AsyncSession, None]
         Async session object with the database
     """
-    async with get_async_session(
-        str(settings.SQLALCHEMY_DATABASE_ASYNC_URI), verbose=settings.SQLALCHEMY_VERBOSE_LOGGER
-    ) as db:
+    async with get_async_session(str(settings.db.dsn_async), verbose=settings.db.verbose) as db:
         yield db
 
 
diff --git a/app/api/endpoints/bucket_permissions.py b/app/api/endpoints/bucket_permissions.py
index 56c5424e92e0eb6856dc355e12d97bd1a6b68ddb..f54efbff056682ddf1d6a53cfacfb74269833905 100644
--- a/app/api/endpoints/bucket_permissions.py
+++ b/app/api/endpoints/bucket_permissions.py
@@ -20,9 +20,7 @@ from app.ceph.s3 import get_s3_bucket_policy, put_s3_bucket_policy
 from app.crud import DuplicateError
 from app.crud.crud_bucket_permission import CRUDBucketPermission
 from app.otlp import start_as_current_span_async
-from app.schemas.bucket_permission import BucketPermissionIn as PermissionSchemaIn
-from app.schemas.bucket_permission import BucketPermissionOut as PermissionSchemaOut
-from app.schemas.bucket_permission import BucketPermissionParameters as PermissionParametersSchema
+from app.schemas.bucket_permission import BucketPermissionIn, BucketPermissionOut, BucketPermissionParameters
 
 router = APIRouter(prefix="/permissions", tags=["BucketPermission"])
 permission_authorization = AuthorizationDependency(resource="bucket_permission")
@@ -32,7 +30,7 @@ tracer = trace.get_tracer_provider().get_tracer(__name__)
 
 @router.get(
     "",
-    response_model=list[PermissionSchemaOut],
+    response_model=list[BucketPermissionOut],
     summary="Get all permissions.",
     response_model_exclude_none=True,
 )
@@ -48,7 +46,7 @@ async def list_permissions(
         CRUDBucketPermission.PermissionStatus | SkipJsonSchema[None],
         Query(description="Status of Bucket Permissions to fetch"),
     ] = None,
-) -> list[PermissionSchemaOut]:
+) -> list[BucketPermissionOut]:
     """
     List all the bucket permissions in the system.\n
     Permission `bucket_permission:list_all` required.
@@ -79,12 +77,12 @@ async def list_permissions(
     bucket_permissions = await CRUDBucketPermission.list(
         db, permission_types=permission_types, permission_status=permission_status
     )
-    return [PermissionSchemaOut.from_db_model(p) for p in bucket_permissions]
+    return [BucketPermissionOut.from_db_model(p) for p in bucket_permissions]
 
 
 @router.post(
     "",
-    response_model=PermissionSchemaOut,
+    response_model=BucketPermissionOut,
     status_code=status.HTTP_201_CREATED,
     summary="Create a permission.",
     response_model_exclude_none=True,
@@ -95,8 +93,8 @@ async def create_permission(
     current_user: CurrentUser,
     s3: S3Resource,
     authorization: Authorization,
-    permission: Annotated[PermissionSchemaIn, Body(..., description="Permission to create")],
-) -> PermissionSchemaOut:
+    permission: Annotated[BucketPermissionIn, Body(..., description="Permission to create")],
+) -> BucketPermissionOut:
     """
     Create a permission for a bucket and user.\n
     Permission `bucket_permission:create` required.
@@ -146,12 +144,12 @@ async def create_permission(
     json_policy["Statement"] += permission.map_to_bucket_policy_statement(permission_db.uid)
     put_s3_bucket_policy(s3, bucket_name=permission.bucket_name, policy=json.dumps(json_policy))
 
-    return PermissionSchemaOut.from_db_model(permission_db)
+    return BucketPermissionOut.from_db_model(permission_db)
 
 
 @router.get(
     "/user/{uid}",
-    response_model=list[PermissionSchemaOut],
+    response_model=list[BucketPermissionOut],
     summary="Get all permissions for a user.",
     response_model_exclude_none=True,
 )
@@ -169,7 +167,7 @@ async def list_permissions_per_user(
         CRUDBucketPermission.PermissionStatus | SkipJsonSchema[None],
         Query(description="Status of Bucket Permissions to fetch"),
     ] = None,
-) -> list[PermissionSchemaOut]:
+) -> list[BucketPermissionOut]:
     """
     List all the bucket permissions for the given user.\n
     Permission `bucket_permission:list_user` required if current user is the target the bucket permission,
@@ -206,12 +204,12 @@ async def list_permissions_per_user(
     bucket_permissions = await CRUDBucketPermission.list(
         db, uid=user.uid, permission_types=permission_types, permission_status=permission_status
     )
-    return [PermissionSchemaOut.from_db_model(p) for p in bucket_permissions]
+    return [BucketPermissionOut.from_db_model(p) for p in bucket_permissions]
 
 
 @router.get(
     "/bucket/{bucket_name}",
-    response_model=list[PermissionSchemaOut],
+    response_model=list[BucketPermissionOut],
     summary="Get all permissions for a bucket.",
     response_model_exclude_none=True,
 )
@@ -229,7 +227,7 @@ async def list_permissions_per_bucket(
         CRUDBucketPermission.PermissionStatus | SkipJsonSchema[None],
         Query(description="Status of Bucket Permissions to fetch"),
     ] = None,
-) -> list[PermissionSchemaOut]:
+) -> list[BucketPermissionOut]:
     """
     List all the bucket permissions for the given bucket.\n
     Permission `bucket_permission:list_bucket` required if current user is owner of the bucket,
@@ -266,12 +264,12 @@ async def list_permissions_per_bucket(
     bucket_permissions = await CRUDBucketPermission.list(
         db, bucket_name=bucket.name, permission_types=permission_types, permission_status=permission_status
     )
-    return [PermissionSchemaOut.from_db_model(p) for p in bucket_permissions]
+    return [BucketPermissionOut.from_db_model(p) for p in bucket_permissions]
 
 
 @router.get(
     "/bucket/{bucket_name}/user/{uid}",
-    response_model=PermissionSchemaOut,
+    response_model=BucketPermissionOut,
     summary="Get permission for bucket and user combination.",
     response_model_exclude_none=True,
 )
@@ -282,7 +280,7 @@ async def get_permission_for_bucket(
     current_user: CurrentUser,
     authorization: Authorization,
     user: PathUser,
-) -> PermissionSchemaOut:
+) -> BucketPermissionOut:
     """
     Get the bucket permissions for the specific combination of bucket and user.\n
     The owner of the bucket and the grantee of the permission can view it.\n
@@ -312,7 +310,7 @@ async def get_permission_for_bucket(
     await authorization(rbac_operation)
     bucket_permission = await CRUDBucketPermission.get(db, bucket.name, user.uid)
     if bucket_permission:
-        return PermissionSchemaOut.from_db_model(bucket_permission)
+        return BucketPermissionOut.from_db_model(bucket_permission)
     raise HTTPException(
         status.HTTP_404_NOT_FOUND,
         detail=f"Permission for combination of bucket={bucket.name} and user={user.uid} doesn't exists",
@@ -369,7 +367,7 @@ async def delete_permission(
             detail=f"Permission for combination of bucket={bucket.name} and user={str(user.uid)} doesn't exists",
         )
     await CRUDBucketPermission.delete(db, bucket_name=bucket_permission.bucket_name, uid=bucket_permission.uid)
-    bucket_permission_schema = PermissionSchemaOut.from_db_model(bucket_permission)
+    bucket_permission_schema = BucketPermissionOut.from_db_model(bucket_permission)
     s3_policy = get_s3_bucket_policy(s3, bucket_name=bucket_permission_schema.bucket_name)
     policy = json.loads(s3_policy.policy)
     policy["Statement"] = [
@@ -381,7 +379,7 @@ async def delete_permission(
 @router.put(
     "/bucket/{bucket_name}/user/{uid}",
     status_code=status.HTTP_200_OK,
-    response_model=PermissionSchemaOut,
+    response_model=BucketPermissionOut,
     summary="Update a bucket permission",
     response_model_exclude_none=True,
 )
@@ -393,8 +391,8 @@ async def update_permission(
     s3: S3Resource,
     authorization: Authorization,
     user: PathUser,
-    permission_parameters: Annotated[PermissionParametersSchema, Body(..., description="Permission to create")],
-) -> PermissionSchemaOut:
+    permission_parameters: Annotated[BucketPermissionParameters, Body(..., description="Permission to create")],
+) -> BucketPermissionOut:
     """
     Update a permission for a bucket and user.\n
     Permission `bucket_permission:update` required.
@@ -433,7 +431,7 @@ async def update_permission(
             detail=f"Permission for combination of bucket={bucket.name} and user={user.uid} doesn't exists",
         )
     updated_permission = await CRUDBucketPermission.update_permission(db, bucket_permission, permission_parameters)
-    updated_permission_schema = PermissionSchemaOut.from_db_model(updated_permission)
+    updated_permission_schema = BucketPermissionOut.from_db_model(updated_permission)
     s3_policy = get_s3_bucket_policy(s3, bucket_name=bucket.name)
     policy = json.loads(s3_policy.policy)
     policy["Statement"] = [
diff --git a/app/api/endpoints/buckets.py b/app/api/endpoints/buckets.py
index 96ef449274495a1f283a5a36aea2cbd68d764fb9..90b3f7e1955490a1bd88bedb50f9c165cd3d473d 100644
--- a/app/api/endpoints/buckets.py
+++ b/app/api/endpoints/buckets.py
@@ -1,6 +1,5 @@
 import json
-from functools import reduce
-from typing import TYPE_CHECKING, Annotated, Any, Awaitable, Callable
+from typing import Annotated, Any, Awaitable, Callable, Iterable
 from uuid import UUID
 
 from botocore.exceptions import ClientError
@@ -10,7 +9,7 @@ from opentelemetry import trace
 from pydantic.json_schema import SkipJsonSchema
 
 from app.api.dependencies import AuthorizationDependency, CurrentBucket, CurrentUser, DBSession, S3Resource
-from app.ceph.s3 import get_s3_bucket_objects, put_s3_bucket_policy
+from app.ceph.s3 import get_s3_bucket_objects, get_s3_bucket_policy, put_s3_bucket_policy
 from app.core.config import settings
 from app.crud import DuplicateError
 from app.crud.crud_bucket import CRUDBucket
@@ -19,15 +18,11 @@ from app.otlp import start_as_current_span_async
 from app.schemas.bucket import BucketIn as BucketInSchema
 from app.schemas.bucket import BucketOut as BucketOutSchema
 
-if TYPE_CHECKING:
-    from mypy_boto3_s3.service_resource import ObjectSummary
-else:
-    ObjectSummary = object
-
 router = APIRouter(prefix="/buckets", tags=["Bucket"])
 bucket_authorization = AuthorizationDependency(resource="bucket")
 Authorization = Annotated[Callable[[str], Awaitable[Any]], Depends(bucket_authorization)]
 tracer = trace.get_tracer_provider().get_tracer(__name__)
+ANONYMOUS_ACCESS_SID = "AnonymousAccess"
 
 cors_rule = {
     "CORSRules": [
@@ -45,7 +40,7 @@ cors_rule = {
                 "content-md5",
             ],
             "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
-            "AllowedOrigins": [str(settings.CLOWM_URL)[:-1]],
+            "AllowedOrigins": [str(settings.ui_uri)[:-1]],
             "ExposeHeaders": [
                 "Etag",
             ],
@@ -55,11 +50,29 @@ cors_rule = {
 }
 
 
+def get_anonymously_bucket_policy(bucket_name: str) -> list[dict[str, Any]]:
+    return [
+        {
+            "Sid": ANONYMOUS_ACCESS_SID,
+            "Effect": "Allow",
+            "Principal": "*",
+            "Resource": f"arn:aws:s3:::{bucket_name}/*",
+            "Action": ["s3:GetObject"],
+        },
+        {
+            "Sid": ANONYMOUS_ACCESS_SID,
+            "Effect": "Allow",
+            "Principal": "*",
+            "Resource": f"arn:aws:s3:::{bucket_name}",
+            "Action": ["s3:ListBucket"],
+        },
+    ]
+
+
 @router.get("", response_model=list[BucketOutSchema], summary="List buckets of user")
 @start_as_current_span_async("api_list_buckets", tracer=tracer)
 async def list_buckets(
     db: DBSession,
-    s3: S3Resource,
     current_user: CurrentUser,
     authorization: Authorization,
     owner_id: Annotated[
@@ -72,7 +85,7 @@ async def list_buckets(
     bucket_type: Annotated[
         CRUDBucket.BucketType, Query(description="Type of the bucket to get. Ignored when `user` parameter not set")
     ] = CRUDBucket.BucketType.ALL,
-) -> list[BucketOutSchema]:
+) -> Iterable[Bucket]:
     """
     List all the buckets in the system or of the desired user where the user has READ permissions for.\n
     Permission `bucket:list` required if the current user is the owner of the bucket,
@@ -94,7 +107,7 @@ async def list_buckets(
         Async function to ask the auth service for authorization. Dependency Injection.
     Returns
     -------
-    buckets : list[app.schemas.bucket.BucketOut]
+    buckets : list[clowmdb.models.Bucket]
         All the buckets for which the user has READ permissions.
     """
     current_span = trace.get_current_span()
@@ -107,22 +120,7 @@ async def list_buckets(
     else:
         buckets = await CRUDBucket.get_for_user(db, owner_id, bucket_type)
 
-    def map_buckets(bucket: Bucket) -> BucketOutSchema:
-        # define function to fetch objects only one time for each bucket
-        objects = list(get_s3_bucket_objects(s3, bucket.name))
-        return BucketOutSchema(
-            **{
-                "description": bucket.description,
-                "name": bucket.name,
-                "created_at": bucket.created_at,
-                "owner_id": bucket.owner_id,
-                "num_objects": sum(1 for obj in objects if not obj.key.endswith("/")),
-                "size": reduce(lambda x, y: x + y.size, objects, 0),
-                "owner_constraint": bucket.owner_constraint,
-            }
-        )
-
-    return list(map(map_buckets, buckets))
+    return buckets
 
 
 @router.post(
@@ -138,7 +136,7 @@ async def create_bucket(
     db: DBSession,
     s3: S3Resource,
     authorization: Authorization,
-) -> BucketOutSchema:
+) -> Bucket:
     """
     Create a bucket for the current user.\n
     The name of the bucket has some constraints.
@@ -161,7 +159,7 @@ async def create_bucket(
 
     Returns
     -------
-    bucket : app.schemas.bucket.BucketOut
+    bucket : clowmdb.models.Bucket
         The newly created bucket.
     """
     current_span = trace.get_current_span()
@@ -187,7 +185,7 @@ async def create_bucket(
                 {
                     "Sid": "ProxyOwnerPerm",
                     "Effect": "Allow",
-                    "Principal": {"AWS": [f"arn:aws:iam:::user/{settings.BUCKET_CEPH_USERNAME}"]},
+                    "Principal": {"AWS": [f"arn:aws:iam:::user/{settings.s3.username}"]},
                     "Action": ["s3:GetObject"],
                     "Resource": [f"arn:aws:s3:::{db_bucket.name}/*"],
                 },
@@ -205,27 +203,17 @@ async def create_bucket(
     with tracer.start_as_current_span("s3_put_bucket_cors_rules") as span:
         span.set_attribute("bucket_name", db_bucket.name)
         s3_bucket.Cors().put(CORSConfiguration=cors_rule)  # type: ignore[arg-type]
-    return BucketOutSchema(
-        **{
-            "description": db_bucket.description,
-            "name": db_bucket.name,
-            "created_at": db_bucket.created_at,
-            "owner_id": db_bucket.owner.uid,
-            "num_objects": 0,
-            "size": 0,
-        }
-    )
+    return db_bucket
 
 
 @router.get("/{bucket_name}", response_model=BucketOutSchema, summary="Get a bucket by its name")
 @start_as_current_span_async("api_get_bucket", tracer=tracer)
 async def get_bucket(
     bucket: CurrentBucket,
-    s3: S3Resource,
     current_user: CurrentUser,
     authorization: Authorization,
     db: DBSession,
-) -> BucketOutSchema:
+) -> Bucket:
     """
     Get a bucket by its name if the current user has READ permissions for the bucket.\n
     Permission `bucket:read` required if the current user is the owner of the bucket,
@@ -235,8 +223,6 @@ async def get_bucket(
     ----------
     bucket : clowmdb.models.Bucket
         Bucket with the name provided in the URL path. Dependency Injection.
-    s3 : boto3_type_annotations.s3.ServiceResource
-        S3 Service to perform operations on buckets in Ceph. Dependency Injection.
     authorization : Callable[[str], Awaitable[Any]]
         Async function to ask the auth service for authorization. Dependency Injection.
     db : sqlalchemy.ext.asyncio.AsyncSession.
@@ -246,7 +232,7 @@ async def get_bucket(
 
     Returns
     -------
-    bucket : app.schemas.bucket.BucketOut
+    bucket : clowmdb.models.Bucket
         Bucket with the provided name.
     """
     trace.get_current_span().set_attribute("bucket_name", bucket.name)
@@ -256,18 +242,58 @@ async def get_bucket(
         else "read"
     )
     await authorization(rbac_operation)
-    objects: list[ObjectSummary] = list(get_s3_bucket_objects(s3, bucket.name))
-    return BucketOutSchema(
-        **{
-            "description": bucket.description,
-            "name": bucket.name,
-            "created_at": bucket.created_at,
-            "owner_id": bucket.owner_id,
-            "num_objects": sum(1 for obj in objects if not obj.key.endswith("/")),
-            "size": reduce(lambda x, y: x + y.size, objects, 0),
-            "owner_constraint": bucket.owner_constraint,
-        }
-    )
+    return bucket
+
+
+@router.patch("/{bucket_name}/public", response_model=BucketOutSchema, summary="Toggle public status")
+@start_as_current_span_async("api_toggle_bucket_public_state", tracer=tracer)
+async def update_bucket_public_state(
+    bucket: CurrentBucket,
+    current_user: CurrentUser,
+    authorization: Authorization,
+    db: DBSession,
+    s3: S3Resource,
+) -> Bucket:
+    """
+    Toggle the buckets public state. A bucket with an owner constraint can't be made public.\n
+    Permission `bucket:update` required if the current user is the owner of the bucket,
+    otherwise `bucket:update_any` required.
+    \f
+    Parameters
+    ----------
+    bucket : clowmdb.models.Bucket
+        Bucket with the name provided in the URL path. Dependency Injection.
+    authorization : Callable[[str], Awaitable[Any]]
+        Async function to ask the auth service for authorization. Dependency Injection.
+    db : sqlalchemy.ext.asyncio.AsyncSession.
+        Async database session to perform query on. Dependency Injection.
+    current_user : clowmdb.models.User
+        Current user. Dependency Injection.
+    s3 : boto3_type_annotations.s3.ServiceResource
+        S3 Service to perform operations on buckets in Ceph. Dependency Injection.
+
+    Returns
+    -------
+    bucket : clowmdb.models.Bucket
+        Bucket with the toggled public state.
+    """
+    trace.get_current_span().set_attributes({"bucket_name": bucket.name, "old_public_state": bucket.public})
+    rbac_operation = "update" if bucket.owner_id == current_user.uid else "update_any"
+    await authorization(rbac_operation)
+    if bucket.owner_constraint is not None:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=f"can't make a bucket with owner constraint {bucket.owner_constraint} public",
+        )
+    s3_policy = get_s3_bucket_policy(s3, bucket_name=bucket.name)
+    json_policy = json.loads(s3_policy.policy)
+    if bucket.public:
+        json_policy["Statement"] = [stmt for stmt in json_policy["Statement"] if stmt["Sid"] != ANONYMOUS_ACCESS_SID]
+    else:
+        json_policy["Statement"] += get_anonymously_bucket_policy(bucket.name)
+    put_s3_bucket_policy(s3, bucket_name=bucket.name, policy=json.dumps(json_policy))
+    await CRUDBucket.update_public_state(db=db, public=not bucket.public, bucket_name=bucket.name)
+    return bucket
 
 
 @router.delete("/{bucket_name}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete a bucket")
diff --git a/app/ceph/rgw.py b/app/ceph/rgw.py
index 563cc2bb487b501f0e14f144aef7597ef9af53b3..3b538f06ad5cc22ba40b868f520dadfdb0f48c05 100644
--- a/app/ceph/rgw.py
+++ b/app/ceph/rgw.py
@@ -9,10 +9,10 @@ from app.schemas.s3key import S3Key
 tracer = trace.get_tracer_provider().get_tracer(__name__)
 
 rgw = RGWAdmin(
-    access_key=settings.USER_CEPH_ACCESS_KEY,
-    secret_key=settings.USER_CEPH_SECRET_KEY,
-    secure=str(settings.OBJECT_GATEWAY_URI).startswith("https"),
-    server=str(settings.OBJECT_GATEWAY_URI).split("://")[-1][:-1],
+    access_key=settings.s3.admin_access_key,
+    secret_key=settings.s3.admin_secret_key.get_secret_value(),
+    secure=settings.s3.uri.scheme == "https",
+    server=str(settings.s3.uri).split("://")[-1][:-1],
 )
 
 
diff --git a/app/ceph/s3.py b/app/ceph/s3.py
index d41f225ac8396173aa8b392ba776106f4f0d93fb..bb48798f5532388291d1b04fa098d5621b3dfcff 100644
--- a/app/ceph/s3.py
+++ b/app/ceph/s3.py
@@ -17,10 +17,10 @@ tracer = trace.get_tracer_provider().get_tracer(__name__)
 
 s3_resource: S3ServiceResource = resource(
     service_name="s3",
-    endpoint_url=str(settings.OBJECT_GATEWAY_URI)[:-1],
-    aws_access_key_id=settings.BUCKET_CEPH_ACCESS_KEY,
-    aws_secret_access_key=settings.BUCKET_CEPH_SECRET_KEY,
-    verify=str(settings.OBJECT_GATEWAY_URI).startswith("https"),
+    endpoint_url=str(settings.s3.uri)[:-1],
+    aws_access_key_id=settings.s3.access_key,
+    aws_secret_access_key=settings.s3.secret_key.get_secret_value(),
+    verify=settings.s3.uri.scheme == "https",
 )
 
 
diff --git a/app/check_ceph_connection.py b/app/check_ceph_connection.py
index 4a3716ea39af355a8183d2441fdf8560aa1409ea..f19bdb9348875e98981e71f22ed0d06d069669ec 100644
--- a/app/check_ceph_connection.py
+++ b/app/check_ceph_connection.py
@@ -20,7 +20,7 @@ wait_seconds = 2
 )
 def init() -> None:
     try:
-        httpx.get(str(settings.OBJECT_GATEWAY_URI), timeout=5.0)
+        httpx.get(str(settings.s3.uri), timeout=5.0)
     except Exception as e:
         logger.error(e)
         raise e
diff --git a/app/check_database_connection.py b/app/check_database_connection.py
index 30102f8561f941951fee1bef510f4ab8b2376b81..af929d5f38e963150642eaf2db549b427d8cde54 100644
--- a/app/check_database_connection.py
+++ b/app/check_database_connection.py
@@ -22,7 +22,7 @@ wait_seconds = 3
 )
 def init() -> None:
     try:
-        with get_session(url=str(settings.SQLALCHEMY_DATABASE_NORMAL_URI)) as db:
+        with get_session(url=str(settings.db.dsn_sync)) as db:
             # Try to create session to check if DB is awake
             db_revision = db.execute(text("SELECT version_num FROM alembic_version LIMIT 1")).scalar_one_or_none()
             if db_revision != latest_revision:
diff --git a/app/core/config.py b/app/core/config.py
index 4b9a41f7f4fb489fc387b5c6315f82a4a8558085..6b14107965b74695c1ae03775ccab54e47648681 100644
--- a/app/core/config.py
+++ b/app/core/config.py
@@ -1,111 +1,148 @@
-from pathlib import Path
-from typing import Any
+import os
+from functools import cached_property
+from typing import Literal, Type
+
+from pydantic import AnyHttpUrl, BaseModel, Field, FilePath, MySQLDsn, NameEmail, SecretStr
+from pydantic_settings import (
+    BaseSettings,
+    JsonConfigSettingsSource,
+    PydanticBaseSettingsSource,
+    SettingsConfigDict,
+    TomlConfigSettingsSource,
+    YamlConfigSettingsSource,
+)
+
+
+class DBSettings(BaseModel):
+    port: int = Field(3306, description="Port of the database.")
+    host: str = Field("localhost", description="Host of the database.")
+    name: str = Field(..., description="Name of the database.")
+    user: str = Field(..., description="Username in the database.")
+    password: SecretStr = Field(..., description="Password for the database user.")
+    verbose: bool = Field(False, description="Flag whether to print the SQL Queries in the logs.")
+
+    @cached_property
+    def dsn_sync(self) -> MySQLDsn:
+        return MySQLDsn.build(
+            scheme="mysql+pymysql",
+            password=self.password.get_secret_value(),
+            host=self.host,
+            port=self.port,
+            path=self.name,
+            username=self.user,
+        )
 
-from pydantic import AnyHttpUrl, AnyUrl, Field, computed_field
-from pydantic_settings import BaseSettings, SettingsConfigDict
+    @cached_property
+    def dsn_async(self) -> MySQLDsn:
+        return MySQLDsn.build(
+            scheme="mysql+aiomysql",
+            password=self.password.get_secret_value(),
+            host=self.host,
+            port=self.port,
+            path=self.name,
+            username=self.user,
+        )
 
 
-def _assemble_db_uri(values: dict[str, Any], async_flag: bool = True) -> Any:
-    return AnyUrl.build(
-        scheme=f"mysql+{'aiomysql' if async_flag else 'pymysql'}",
-        password=values.get("DB_PASSWORD"),
-        username=values.get("DB_USER"),
-        port=values.get("DB_PORT"),
-        host=values.get("DB_HOST"),  # type: ignore[arg-type]
-        path=f"{values.get('DB_DATABASE') or ''}",
+class S3Settings(BaseModel):
+    uri: AnyHttpUrl = Field(..., description="URI of the S3 Object Storage.")
+    access_key: str = Field(..., description="Access key for the S3 that owns the buckets.")
+    secret_key: SecretStr = Field(..., description="Secret key for the S3 that owns the buckets.")
+    username: str = Field(
+        ..., description="ID of the user in ceph who owns all the buckets. Owner of 'CLOWM_S3__ACCESS_KEY'"
+    )
+    admin_access_key: str = Field(
+        ..., description="Access key for the Ceph Object Gateway with 'user:read,user:write privileges'."
+    )
+    admin_secret_key: SecretStr = Field(
+        ..., description="Secret key for the Ceph Object Gateway with 'user:read,user:write privileges'."
     )
 
 
-def _load_public_key(pub_key_val: str | None, pub_key_file: Path | None) -> str:
-    pub_key = ""
-    if pub_key_val is not None:
-        pub_key = pub_key_val
-    if pub_key_file is not None:
-        with open(pub_key_file) as f:
-            pub_key = f.read()
-    if len(pub_key) == 0:
-        raise ValueError("PUBLIC_KEY_VALUE or PUBLIC_KEY_FILE must be set")
-    return pub_key
-
+class OPASettings(BaseModel):
+    uri: AnyHttpUrl = Field(..., description="URI of the OPA Service")
 
-class Settings(BaseSettings):
-    CLOWM_URL: AnyHttpUrl = Field(
-        AnyHttpUrl("http://localhost:8080"),
-        description="Base HTTP URL where the CloWM service is reachable.",
-        examples=["http://localhost:8080"],
-    )
-    API_PREFIX: str = Field("/api", description="Path Prefix for all API endpoints.")
 
-    public_key_value: str | None = Field(
-        None, description="Public RSA Key in PEM format to sign the JWTs.", validation_alias="PUBLIC_KEY_VALUE"
-    )
-    public_key_file: Path | None = Field(
-        None, description="Path to Public RSA Key in PEM format to sign the JWTs.", validation_alias="PUBLIC_KEY_FILE"
+class OTLPSettings(BaseModel):
+    grpc_endpoint: str | None = Field(
+        None, description="OTLP compatible endpoint to send traces via gRPC, e.g. Jaeger", examples=["localhost:8080"]
     )
+    secure: bool = Field(False, description="Connection type")
 
-    @computed_field  # type: ignore[misc]
-    @property
-    def PUBLIC_KEY(self) -> str:
-        return _load_public_key(self.public_key_value, self.public_key_file)
-
-    DB_HOST: str = Field(..., description="Host of the database.")
-    DB_USER: str = Field(..., description="Username in the database.")
-    DB_PASSWORD: str = Field(..., description="Password for the database user.")
-    DB_DATABASE: str = Field(..., description="Name of the database.")
-    DB_PORT: int = Field(3306, description="Port of the database.")
-    SQLALCHEMY_VERBOSE_LOGGER: bool = Field(False, description="Flag whether to print the SQL Queries in the logs.")
-
-    @computed_field  # type: ignore[misc]
-    @property
-    def SQLALCHEMY_DATABASE_ASYNC_URI(self) -> AnyUrl:
-        return _assemble_db_uri(
-            {
-                "DB_HOST": self.DB_HOST,
-                "DB_USER": self.DB_USER,
-                "DB_PASSWORD": self.DB_PASSWORD,
-                "DB_DATABASE": self.DB_DATABASE,
-                "DB_PORT": self.DB_PORT,
-            },
-            async_flag=True,
-        )
-
-    @computed_field  # type: ignore[misc]
-    @property
-    def SQLALCHEMY_DATABASE_NORMAL_URI(self) -> AnyUrl:
-        return _assemble_db_uri(
-            {
-                "DB_HOST": self.DB_HOST,
-                "DB_USER": self.DB_USER,
-                "DB_PASSWORD": self.DB_PASSWORD,
-                "DB_DATABASE": self.DB_DATABASE,
-                "DB_PORT": self.DB_PORT,
-            },
-            async_flag=False,
-        )
 
-    OBJECT_GATEWAY_URI: AnyHttpUrl = Field(..., description="URI of the Ceph Object Gateway.")
-    USER_CEPH_ACCESS_KEY: str = Field(
-        ..., description="Access key for the Ceph Object Gateway with 'user:read,user:write privileges'."
-    )
-    USER_CEPH_SECRET_KEY: str = Field(
-        ..., description="Secret key for the Ceph Object Gateway with 'user:read,user:write privileges'."
+class EmailSettings(BaseModel):
+    server: str | Literal["console"] | None = Field(
+        None,
+        description="Hostname of SMTP server. If `console`, emails are printed to the console. If None, no emails are sent",
     )
-    BUCKET_CEPH_ACCESS_KEY: str = Field(
-        ..., description="Access key for the Ceph Object Gateway with unlimited buckets."
+    port: int = Field(587, description="Port of the SMTP server")
+    sender_email: NameEmail = Field(
+        NameEmail(email="no-reply@clowm.com", name="CloWM"), description="Email address from which the emails are sent."
     )
-    BUCKET_CEPH_SECRET_KEY: str = Field(
-        ..., description="Secret key for the Ceph Object Gateway with unlimited buckets."
+    reply_email: NameEmail | None = Field(None, description="Email address in the `Reply-To` header.")
+    connection_security: Literal["ssl", "tls"] | Literal["starttls"] | None = Field(
+        None, description="Connection security to the SMTP server."
     )
-    BUCKET_CEPH_USERNAME: str = Field(
-        ..., description="ID of the user in ceph who owns all the buckets. Owner of 'BUCKET_CEPH_ACCESS_KEY'"
+    local_hostname: str | None = Field(None, description="Overwrite the local hostname from which the emails are sent.")
+    ca_path: FilePath | None = Field(None, description="Path to a custom CA certificate.")
+    key_path: FilePath | None = Field(None, description="Path to the CA key.")
+    user: str | None = Field(None, description="Username to use for SMTP login")
+    password: SecretStr | None = Field(None, description="Password to use for SMTP login")
+
+
+class Settings(BaseSettings):
+    api_prefix: str = Field("/api/s3proxy-service", description="Path Prefix for all API endpoints.")
+    public_key: SecretStr | None = Field(None, description="Public RSA Key in PEM format to verify the JWTs.")
+    public_key_file: FilePath | None = Field(
+        None, description="Path to Public RSA Key in PEM format to verify the JWTs."
     )
-    OPA_URI: AnyHttpUrl = Field(..., description="URI of the OPA Service")
-    OPA_POLICY_PATH: str = Field("/clowm/authz/allow", description="Path to the OPA Policy for Authorization")
-    OTLP_GRPC_ENDPOINT: str | None = Field(
-        None, description="OTLP compatible endpoint to send traces via gRPC, e.g. Jaeger"
+    ui_uri: AnyHttpUrl = Field(..., description="URL of the UI")
+
+    @cached_property
+    def public_key_value(self) -> SecretStr:
+        pub_key = ""
+        if self.public_key is not None:
+            pub_key = self.public_key.get_secret_value()
+        if self.public_key_file is not None:
+            with open(self.public_key_file) as f:
+                pub_key = f.read()
+        if len(pub_key) == 0:
+            raise ValueError("CLOWM_PUBLIC_KEY or CLOWM_PUBLIC_KEY_FILE must be set")
+        return SecretStr(pub_key)
+
+    db: DBSettings
+    smtp: EmailSettings = EmailSettings()
+    s3: S3Settings
+    opa: OPASettings
+    otlp: OTLPSettings | None = None
+
+    model_config = SettingsConfigDict(
+        env_prefix="CLOWM_",
+        env_file=".env",
+        extra="ignore",
+        secrets_dir="/run/secrets" if os.path.isdir("/run/secrets") else None,
+        env_nested_delimiter="__",
+        yaml_file=os.getenv("CLOWM_CONFIG_FILE_YAML", "config.yaml"),
+        toml_file=os.getenv("CLOWM_CONFIG_FILE_TOML", "config.toml"),
+        json_file=os.getenv("CLOWM_CONFIG_FILE_JSON", "config.json"),
     )
 
-    model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", secrets_dir="/run/secrets", extra="ignore")
+    @classmethod
+    def settings_customise_sources(
+        cls,
+        settings_cls: Type[BaseSettings],
+        init_settings: PydanticBaseSettingsSource,
+        env_settings: PydanticBaseSettingsSource,
+        dotenv_settings: PydanticBaseSettingsSource,
+        file_secret_settings: PydanticBaseSettingsSource,
+    ) -> tuple[PydanticBaseSettingsSource, ...]:
+        return (
+            env_settings,
+            dotenv_settings,
+            YamlConfigSettingsSource(settings_cls),
+            TomlConfigSettingsSource(settings_cls),
+            JsonConfigSettingsSource(settings_cls),
+        )
 
 
 settings = Settings()
diff --git a/app/core/security.py b/app/core/security.py
index a592a3e407687320f74bd0e8167d898521a47493..b4f79f766a331d615e93ba8085cbcbc649039156 100644
--- a/app/core/security.py
+++ b/app/core/security.py
@@ -1,11 +1,12 @@
+from typing import Type, TypeVar
+
 from authlib.jose import JsonWebToken
 from fastapi import HTTPException, status
 from httpx import AsyncClient
 from opentelemetry import trace
 
 from app.core.config import settings
-from app.otlp import start_as_current_span_async
-from app.schemas.security import AuthzRequest, AuthzResponse
+from app.schemas.security import AuthzRequest, AuthzResponse, OPARequest, OPAResponse
 
 ISSUER = "clowm"
 ALGORITHM = "RS256"
@@ -13,6 +14,8 @@ jwt = JsonWebToken([ALGORITHM])
 
 tracer = trace.get_tracer_provider().get_tracer(__name__)
 
+T = TypeVar("T", bound=OPAResponse)
+
 
 def decode_token(token: str) -> dict[str, str]:  # pragma: no cover
     """
@@ -30,7 +33,7 @@ def decode_token(token: str) -> dict[str, str]:  # pragma: no cover
     """
     claims = jwt.decode(
         s=token,
-        key=settings.PUBLIC_KEY,
+        key=settings.public_key_value.get_secret_value(),
         claims_options={
             "iss": {"essential": True},
             "sub": {"essential": True},
@@ -41,7 +44,38 @@ def decode_token(token: str) -> dict[str, str]:  # pragma: no cover
     return claims
 
 
-@start_as_current_span_async("authorization", tracer=tracer)
+async def _request_opa(request: OPARequest, response_type: Type[T], *, client: AsyncClient) -> T:
+    """
+    Wrapper to send a generic request to OPA.
+
+    Parameters
+    ----------
+    request : OPARequest
+        The request for OPA.
+    response_type : Type[OPAResponse -> T]
+        The class to which the response will be parsed. T must inherit from OPAResponse.
+    client : httpx.AsyncClient
+        An async http client with an open connection.
+
+    Returns
+    -------
+    response : T
+        The parsed response from OPA.
+    """
+    with tracer.start_as_current_span(
+        "opa_request",
+        attributes={"opa_path": request.opa_path.strip("/"), "body": request.model_dump_json(indent=4)},
+    ) as span:
+        response = await client.post(
+            str(settings.opa.uri) + request.opa_path.strip("/"),
+            content=request.model_dump_json(),
+            headers={"content-type": "application/json"},
+        )
+        parsed_response = response_type.model_validate_json(response.content)
+        span.set_attribute("decision_id", str(parsed_response.decision_id))
+        return parsed_response
+
+
 async def request_authorization(request_params: AuthzRequest, client: AsyncClient) -> AuthzResponse:
     """
     Send a request to OPA for a policy decision. Raise an HTTPException with status 403 if authorization is denied.
@@ -58,17 +92,23 @@ async def request_authorization(request_params: AuthzRequest, client: AsyncClien
     response : app.schemas.security.AuthzResponse
         Response by the Auth service about the authorization request
     """
-    current_span = trace.get_current_span()
-    current_span.set_attributes({"resource": request_params.resource, "operation": request_params.operation})
-    response = await client.post(
-        f"{settings.OPA_URI}v1/data{settings.OPA_POLICY_PATH}", json={"input": request_params.model_dump()}
-    )
 
-    parsed_response = AuthzResponse.model_validate(response.json())
-    current_span.set_attribute("decision_id", str(parsed_response.decision_id))
-    if not parsed_response.result:  # pragma: no cover
+    with tracer.start_as_current_span(
+        "authorization",
+        attributes={
+            "resource": request_params.resource,
+            "operation": request_params.operation,
+            "uid": request_params.uid,
+        },
+    ):
+        response = await _request_opa(
+            request=OPARequest(input=request_params, opa_path="/v1/data/clowm/authz/allow"),
+            response_type=AuthzResponse,
+            client=client,
+        )
+    if not response.result:  # pragma: no cover
         raise HTTPException(
             status_code=status.HTTP_403_FORBIDDEN,
-            detail=f"Action forbidden. Decision ID {str(parsed_response.decision_id)}",
+            detail=f"Action forbidden. Decision ID {response.decision_id}",
         )
-    return parsed_response
+    return response
diff --git a/app/crud/crud_bucket.py b/app/crud/crud_bucket.py
index bdc8ae7e1d37f0f5ade6d1f0be762bbf141daa68..729e030b271ae376a1d6a50a1b2e82b7da49a931 100644
--- a/app/crud/crud_bucket.py
+++ b/app/crud/crud_bucket.py
@@ -5,7 +5,7 @@ from uuid import UUID
 from clowmdb.models import Bucket
 from clowmdb.models import BucketPermission as BucketPermissionDB
 from opentelemetry import trace
-from sqlalchemy import delete, func, or_, select
+from sqlalchemy import delete, func, or_, select, update
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from app.crud import DuplicateError
@@ -181,9 +181,30 @@ class CRUDBucket:
                 raise DuplicateError(f"Bucket {bucket.name} exists already")
             db.add(bucket)
             await db.commit()
-            await db.refresh(bucket)
             return bucket
 
+    @staticmethod
+    async def update_public_state(db: AsyncSession, bucket_name: str, public: bool) -> None:
+        """
+        Update the public state of a bucket
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession
+            Async database session to perform query on.
+        bucket_name : str
+            Name of a bucket.
+        public : bool
+            New public state of the bucket.
+        """
+        stmt = update(Bucket).where(Bucket.name == bucket_name).values(public=public)
+        with tracer.start_as_current_span(
+            "db_update_bucket_public_state",
+            attributes={"bucket_name": bucket_name, "public": public},
+        ):
+            await db.execute(stmt)
+            await db.commit()
+
     @staticmethod
     async def delete(db: AsyncSession, bucket_name: str) -> None:
         """
diff --git a/app/main.py b/app/main.py
index 0fad3197f0c886c5da38e9e5f986a1dac288bc44..a3212bc5327a108469f8c613ccbd980b6a4fa30a 100644
--- a/app/main.py
+++ b/app/main.py
@@ -49,18 +49,20 @@ app = FastAPI(
     },
     generate_unique_id_function=custom_generate_unique_id,
     license_info={"name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0"},
-    root_path=settings.API_PREFIX,
+    root_path=settings.api_prefix,
     openapi_url=None,  # create it manually to enable caching on client side
     lifespan=lifespan,
 )
-if settings.API_PREFIX:  # pragma: no cover
-    app.servers.insert(0, {"url": settings.API_PREFIX})
+if settings.api_prefix:  # pragma: no cover
+    app.servers.insert(0, {"url": settings.api_prefix})
 
-if settings.OTLP_GRPC_ENDPOINT is not None and len(settings.OTLP_GRPC_ENDPOINT) > 0:  # pragma: no cover
+if (
+    settings.otlp is not None and settings.otlp.grpc_endpoint is not None and len(settings.otlp.grpc_endpoint) > 0
+):  # pragma: no cover
     resource = Resource(attributes={SERVICE_NAME: "clowm-s3proxy-service"})
     provider = TracerProvider(resource=resource)
     provider.add_span_processor(
-        BatchSpanProcessor(OTLPSpanExporter(endpoint=settings.OTLP_GRPC_ENDPOINT, insecure=True))
+        BatchSpanProcessor(OTLPSpanExporter(endpoint=settings.otlp.grpc_endpoint, insecure=not settings.otlp.secure))
     )
     trace.set_tracer_provider(provider)
 
diff --git a/app/schemas/bucket.py b/app/schemas/bucket.py
index c763f54bca1c3322cd2c31c2b8566e2263a5efe7..568dc201916d3cfe6b133222e1884c896a8648c2 100644
--- a/app/schemas/bucket.py
+++ b/app/schemas/bucket.py
@@ -26,7 +26,7 @@ class _BaseBucket(BaseModel):
         ...,
         examples=["This is a sample description of a bucket"],
         description="Description of the bucket",
-        min_length=32,
+        min_length=16,
         max_length=2**16,
     )
 
@@ -55,11 +55,6 @@ class BucketOut(_BaseBucket):
         description="Time when the bucket was created as UNIX timestamp",
     )
     owner_id: UUID = Field(..., description="UID of the owner", examples=["1d3387f3-95c0-4813-8767-2cad87faeebf"])
-    num_objects: int = Field(..., description="Number of Objects in this bucket", examples=[6])
-    size: int = Field(..., description="Total size of objects in this bucket in bytes", examples=[3256216])
     owner_constraint: Bucket.Constraint | None = Field(None, description="Constraint for the owner of the bucket")
-    description: str = Field(
-        ...,
-        description="Description of the bucket",
-    )
+    public: bool = Field(..., description="Flag if the bucket is anonymously readable")
     model_config = ConfigDict(from_attributes=True)
diff --git a/app/schemas/security.py b/app/schemas/security.py
index d065001db07cf1c76133a507398889f46f8a0bac..3f8d0f508357faa22f1e208680a2a84a71307ac4 100644
--- a/app/schemas/security.py
+++ b/app/schemas/security.py
@@ -1,25 +1,40 @@
 from datetime import datetime
+from typing import Any
 
 from pydantic import BaseModel, Field
 
 from app.schemas import UUID
 
 
-class AuthzResponse(BaseModel):
-    """Schema for a response from OPA"""
+class OPARequest(BaseModel):
+    opa_path: str = Field(
+        ...,
+        description="URL path where OPA is queried",
+        exclude=True,
+        examples=["/v1/data/clowm/authz/allow"],
+    )
+    input: Any
+
 
+class OPAResponse(BaseModel):
     decision_id: UUID = Field(
         ...,
         description="Decision ID for for the specific decision",
         examples=["8851dce0-7546-4e81-a89d-111cbec376c1"],
     )
+    result: Any
+
+
+class AuthzResponse(OPAResponse):
+    """Schema for a response from OPA"""
+
     result: bool = Field(..., description="Result of the Authz request")
 
 
 class AuthzRequest(BaseModel):
     """Schema for a Request to OPA"""
 
-    uid: str = Field(..., description="lifescience id of user", examples=["28c5353b8bb34984a8bd4169ba94c606"])
+    uid: str = Field(..., description="UID of user", examples=["28c5353b8bb34984a8bd4169ba94c606"])
     operation: str = Field(..., description="Operation the user wants to perform", examples=["read"])
     resource: str = Field(..., description="Resource the operation should be performed on", examples=["bucket"])
 
diff --git a/app/tests/api/test_bucket_permissions.py b/app/tests/api/test_bucket_permissions.py
index 153e0d8b0adee8eebb7ff1cbe2868464c609d5af..001dcd77df6532ad690ab12b8e5af4b634ceeea4 100644
--- a/app/tests/api/test_bucket_permissions.py
+++ b/app/tests/api/test_bucket_permissions.py
@@ -5,6 +5,7 @@ import pytest
 from clowmdb.models import Bucket, BucketPermission
 from fastapi import status
 from httpx import AsyncClient
+from pydantic import TypeAdapter
 from sqlalchemy import update
 from sqlalchemy.ext.asyncio import AsyncSession
 
@@ -33,11 +34,11 @@ class TestBucketPermissionRoutesGet(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         response = await client.get(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(random_bucket_permission_schema.uid)}",  # noqa:E501
@@ -45,7 +46,7 @@ class TestBucketPermissionRoutesGet(_TestBucketPermissionRoutes):
         )
         assert response.status_code == status.HTTP_200_OK
 
-        permission = BucketPermissionOut.model_validate(response.json())
+        permission = BucketPermissionOut.model_validate_json(response.content)
 
         assert permission
         assert permission.uid == random_bucket_permission_schema.uid
@@ -64,11 +65,11 @@ class TestBucketPermissionRoutesGet(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         response = await client.get(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(uuid4())}",
@@ -90,13 +91,13 @@ class TestBucketPermissionRoutesGet(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         response = await client.get(
             f"{self.base_path}/bucket/{random_bucket.name}/user/{str(random_second_user.user.uid)}",
@@ -117,18 +118,18 @@ class TestBucketPermissionRoutesGet(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         response = await client.get(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(random_second_user.user.uid)}",
             headers=random_second_user.auth_headers,
         )
         assert response.status_code == status.HTTP_200_OK
-        permission = BucketPermissionOut.model_validate(response.json())
+        permission = BucketPermissionOut.model_validate_json(response.content)
 
         assert permission
         assert permission.uid == random_bucket_permission_schema.uid
@@ -148,13 +149,13 @@ class TestBucketPermissionRoutesGet(_TestBucketPermissionRoutes):
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(db, random_bucket_permission_schema.bucket_name, random_third_user.user.uid)
 
@@ -177,21 +178,23 @@ class TestBucketPermissionRoutesGet(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         response = await client.get(
             f"{self.base_path}/user/{str(random_bucket_permission_schema.uid)}",
             headers=random_second_user.auth_headers,
         )
         assert response.status_code == status.HTTP_200_OK
-        permission_list = response.json()
-        assert isinstance(permission_list, list)
-        assert len(permission_list) == 1
-        permission = BucketPermissionOut.model_validate(permission_list[0])
+
+        ta = TypeAdapter(list[BucketPermissionOut])
+        permissions = ta.validate_json(response.content)
+        assert len(permissions) == 1
+
+        permission = permissions[0]
         assert permission.uid == random_bucket_permission_schema.uid
         assert permission.bucket_name == random_bucket_permission_schema.bucket_name
 
@@ -208,18 +211,20 @@ class TestBucketPermissionRoutesGet(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         response = await client.get(self.base_path, headers=random_user.auth_headers, params={"allow_admin": True})
         assert response.status_code == status.HTTP_200_OK
-        permission_list = response.json()
-        assert isinstance(permission_list, list)
-        assert len(permission_list) == 1
-        permission = BucketPermissionOut.model_validate(permission_list[0])
+
+        ta = TypeAdapter(list[BucketPermissionOut])
+        permissions = ta.validate_json(response.content)
+        assert len(permissions) == 1
+
+        permission = permissions[0]
         assert permission.uid == random_bucket_permission_schema.uid
         assert permission.bucket_name == random_bucket_permission_schema.bucket_name
 
@@ -236,21 +241,23 @@ class TestBucketPermissionRoutesGet(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         response = await client.get(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}",
             headers=random_user.auth_headers,
         )
         assert response.status_code == status.HTTP_200_OK
-        permission_list = response.json()
-        assert isinstance(permission_list, list)
-        assert len(permission_list) == 1
-        permission = BucketPermissionOut.model_validate(permission_list[0])
+
+        ta = TypeAdapter(list[BucketPermissionOut])
+        permissions = ta.validate_json(response.content)
+        assert len(permissions) == 1
+
+        permission = permissions[0]
         assert permission.uid == random_bucket_permission_schema.uid
         assert permission.bucket_name == random_bucket_permission_schema.bucket_name
 
@@ -267,11 +274,11 @@ class TestBucketPermissionRoutesGet(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         response = await client.get(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}",
@@ -291,14 +298,16 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=uuid4())
-        response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.model_dump())
+        response = await client.post(
+            self.base_path, headers=random_user.auth_headers, content=permission.model_dump_json()
+        )
         assert response.status_code == status.HTTP_404_NOT_FOUND
 
     @pytest.mark.asyncio
@@ -311,16 +320,18 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing who is owner of the bucket. pytest fixture.
+            Random user for testing who is owner of the bucket.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=random_user.user.uid)
-        response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.model_dump())
+        response = await client.post(
+            self.base_path, headers=random_user.auth_headers, content=permission.model_dump_json()
+        )
         assert response.status_code == status.HTTP_400_BAD_REQUEST
 
     @pytest.mark.asyncio
@@ -336,16 +347,18 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         permission = BucketPermissionSchema(
             bucket_name=random_bucket_permission_schema.bucket_name, uid=random_bucket_permission_schema.uid
         )
-        response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.model_dump())
+        response = await client.post(
+            self.base_path, headers=random_user.auth_headers, content=permission.model_dump_json()
+        )
         assert response.status_code == status.HTTP_400_BAD_REQUEST
 
     @pytest.mark.asyncio
@@ -361,16 +374,16 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=random_second_user.user.uid)
 
         response = await client.post(
-            self.base_path, headers=random_second_user.auth_headers, json=permission.model_dump()
+            self.base_path, headers=random_second_user.auth_headers, content=permission.model_dump_json()
         )
         assert response.status_code == status.HTTP_403_FORBIDDEN
 
@@ -389,22 +402,24 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         """
         permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=random_second_user.user.uid)
 
-        response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.model_dump())
+        response = await client.post(
+            self.base_path, headers=random_user.auth_headers, content=permission.model_dump_json()
+        )
 
         assert response.status_code == status.HTTP_201_CREATED
-        created_permission = BucketPermissionOut.model_validate(response.json())
+        created_permission = BucketPermissionOut.model_validate_json(response.content)
         assert created_permission.uid == random_second_user.user.uid
         assert created_permission.bucket_name == random_bucket.name
 
@@ -423,15 +438,15 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         update_stmt = (
             update(Bucket).where(Bucket.name == random_bucket.name).values(owner_constraint=Bucket.Constraint.READ)
@@ -440,7 +455,9 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes):
         await db.commit()
         permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=random_second_user.user.uid)
 
-        response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.model_dump())
+        response = await client.post(
+            self.base_path, headers=random_user.auth_headers, content=permission.model_dump_json()
+        )
         assert response.status_code == status.HTTP_403_FORBIDDEN
 
 
@@ -458,11 +475,11 @@ class TestBucketPermissionRoutesDelete(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         response = await client.delete(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(random_bucket_permission_schema.uid)}",  # noqa:E501
@@ -483,11 +500,11 @@ class TestBucketPermissionRoutesDelete(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         response = await client.get(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(random_bucket_permission_schema.uid)}",  # noqa:E501
@@ -508,9 +525,9 @@ class TestBucketPermissionRoutesDelete(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         response = await client.delete(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(uuid4())}",
@@ -532,11 +549,11 @@ class TestBucketPermissionRoutesDelete(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         response = await client.delete(
             f"{self.base_path}/bucket/{random_bucket.name}/user/{str(random_second_user.user.uid)}",
@@ -558,13 +575,13 @@ class TestBucketPermissionRoutesDelete(_TestBucketPermissionRoutes):
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(db, random_bucket_permission_schema.bucket_name, random_third_user.user.uid)
         response = await client.delete(
@@ -588,9 +605,9 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         new_from_time = round(datetime(2022, 1, 1, 0, 0).timestamp())
         new_params = BucketPermissionParametersSchema(
@@ -602,10 +619,10 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes):
         response = await client.put(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(random_bucket_permission_schema.uid)}",  # noqa:E501
             headers=random_user.auth_headers,
-            json=new_params.model_dump(),
+            content=new_params.model_dump_json(),
         )
         assert response.status_code == status.HTTP_200_OK
-        updated_permission = BucketPermissionOut.model_validate(response.json())
+        updated_permission = BucketPermissionOut.model_validate_json(response.content)
         assert updated_permission.uid == random_bucket_permission_schema.uid
         assert updated_permission.bucket_name == random_bucket_permission_schema.bucket_name
         if new_params.from_timestamp is not None and new_params.to_timestamp is not None:
@@ -627,9 +644,9 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         new_params = BucketPermissionParametersSchema(
             permission=BucketPermission.Permission.READWRITE,
@@ -638,7 +655,7 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes):
         response = await client.put(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(uuid4())}",
             headers=random_user.auth_headers,
-            json=new_params.model_dump(),
+            content=new_params.model_dump_json(),
         )
         assert response.status_code == status.HTTP_404_NOT_FOUND
 
@@ -656,11 +673,11 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         new_params = BucketPermissionParametersSchema(
             permission=BucketPermission.Permission.READWRITE,
@@ -669,7 +686,7 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes):
         response = await client.put(
             f"{self.base_path}/bucket/{random_bucket.name}/user/{str(random_second_user.user.uid)}",
             headers=random_user.auth_headers,
-            json=new_params.model_dump(),
+            content=new_params.model_dump_json(),
         )
         assert response.status_code == status.HTTP_404_NOT_FOUND
 
@@ -686,11 +703,11 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         new_params = BucketPermissionParametersSchema(
             permission=BucketPermission.Permission.READWRITE,
@@ -699,7 +716,7 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes):
         response = await client.put(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(random_second_user.user.uid)}",
             headers=random_second_user.auth_headers,
-            json=new_params.model_dump(),
+            content=new_params.model_dump_json(),
         )
         assert response.status_code == status.HTTP_403_FORBIDDEN
 
@@ -716,11 +733,11 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         new_params = BucketPermissionParametersSchema(
             permission=BucketPermission.Permission.READWRITE,
@@ -729,6 +746,6 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes):
         response = await client.put(
             f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(random_bucket_permission_schema.uid)}",  # noqa:E501
             headers=random_third_user.auth_headers,
-            json=new_params.model_dump(),
+            content=new_params.model_dump_json(),
         )
         assert response.status_code == status.HTTP_403_FORBIDDEN
diff --git a/app/tests/api/test_buckets.py b/app/tests/api/test_buckets.py
index e629bd74c25fef223b4b3a6fb3be45147a8097bc..10b5f1434c15872e692889231b11218a293754fb 100644
--- a/app/tests/api/test_buckets.py
+++ b/app/tests/api/test_buckets.py
@@ -2,12 +2,15 @@ import pytest
 from clowmdb.models import Bucket, BucketPermission
 from fastapi import status
 from httpx import AsyncClient
+from pydantic import TypeAdapter
+from sqlalchemy import update
 from sqlalchemy.ext.asyncio import AsyncSession
 
+from app.api.endpoints.buckets import ANONYMOUS_ACCESS_SID
 from app.crud.crud_bucket import CRUDBucket
 from app.schemas.bucket import BucketIn, BucketOut
 from app.tests.mocks.mock_s3_resource import MockS3ServiceResource
-from app.tests.utils.bucket import add_permission_for_bucket, delete_bucket
+from app.tests.utils.bucket import add_permission_for_bucket, delete_bucket, make_bucket_public
 from app.tests.utils.cleanup import CleanupList
 from app.tests.utils.user import UserWithAuthHeader
 from app.tests.utils.utils import random_lower_string
@@ -28,19 +31,20 @@ class TestBucketRoutesGet(_TestBucketRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         response = await client.get(f"{self.base_path}", headers=random_second_user.auth_headers)
         assert response.status_code == status.HTTP_200_OK
 
-        buckets = response.json()
+        ta = TypeAdapter(list[BucketOut])
+        buckets = ta.validate_json(response.content)
 
         assert len(buckets) == 1
-        bucket = BucketOut.model_validate(buckets[0])
+        bucket = buckets[0]
 
         assert bucket.name == random_bucket.name
         assert bucket.owner_id == random_bucket.owner_id
@@ -58,21 +62,22 @@ class TestBucketRoutesGet(_TestBucketRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         response = await client.get(
             f"{self.base_path}", params={"owner_id": str(random_bucket.owner_id)}, headers=random_user.auth_headers
         )
         assert response.status_code == status.HTTP_200_OK
 
-        buckets = response.json()
+        ta = TypeAdapter(list[BucketOut])
+        buckets = ta.validate_json(response.content)
 
         assert len(buckets) == 1
-        bucket = BucketOut.model_validate(buckets[0])
+        bucket = buckets[0]
 
         assert bucket.name == random_bucket.name
         assert bucket.owner_id == random_bucket.owner_id
@@ -90,16 +95,16 @@ class TestBucketRoutesGet(_TestBucketRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         response = await client.get(f"{self.base_path}/{random_bucket.name}", headers=random_user.auth_headers)
         assert response.status_code == status.HTTP_200_OK
 
-        bucket = BucketOut.model_validate(response.json())
+        bucket = BucketOut.model_validate_json(response.content)
 
         assert bucket.name == random_bucket.name
         assert bucket.owner_id == random_bucket.owner.uid
@@ -112,9 +117,9 @@ class TestBucketRoutesGet(_TestBucketRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         response = await client.get(f"{self.base_path}/impossible_bucket_name", headers=random_user.auth_headers)
         assert response.status_code == status.HTTP_404_NOT_FOUND
@@ -124,20 +129,53 @@ class TestBucketRoutesGet(_TestBucketRoutes):
         self, client: AsyncClient, random_bucket: Bucket, random_second_user: UserWithAuthHeader
     ) -> None:
         """
-        Test for getting a foreign bucket with permission by its name.
+        Test for getting a foreign bucket without permission.
 
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random user which is not the owner of the bucket. pytest fixture.
+            Random user who is not the owner of the bucket.
         """
         response = await client.get(f"{self.base_path}/{random_bucket.name}", headers=random_second_user.auth_headers)
         assert response.status_code == status.HTTP_403_FORBIDDEN
 
+    @pytest.mark.asyncio
+    async def test_get_foreign_public_bucket(
+        self,
+        client: AsyncClient,
+        random_bucket: Bucket,
+        random_second_user: UserWithAuthHeader,
+        db: AsyncSession,
+        mock_s3_service: MockS3ServiceResource,
+    ) -> None:
+        """
+        Test for getting a foreign public bucket.
+
+        Parameters
+        ----------
+        client : httpx.AsyncClient
+            HTTP Client to perform the request on.
+        random_bucket : clowmdb.models.Bucket
+            Random bucket for testing.
+        random_second_user : app.tests.utils.user.UserWithAuthHeader
+            Random user who is not the owner of the bucket.
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource
+            Mock S3 Service to manipulate objects.
+        """
+        await make_bucket_public(db=db, s3=mock_s3_service, bucket_name=random_bucket.name)
+        response = await client.get(f"{self.base_path}/{random_bucket.name}", headers=random_second_user.auth_headers)
+        assert response.status_code == status.HTTP_200_OK
+
+        bucket = BucketOut.model_validate_json(response.content)
+
+        assert bucket.name == random_bucket.name
+
 
 class TestBucketRoutesCreate(_TestBucketRoutes):
     @pytest.mark.asyncio
@@ -154,11 +192,11 @@ class TestBucketRoutesCreate(_TestBucketRoutes):
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         cleanup : app.tests.utils.utils.CleanupList
             Cleanup object where (async) functions can be registered which get executed after a (failed) test.
         """
@@ -168,18 +206,20 @@ class TestBucketRoutesCreate(_TestBucketRoutes):
             db=db,
             bucket_name=bucket_info.name,
         )
-        response = await client.post(self.base_path, headers=random_user.auth_headers, json=bucket_info.model_dump())
+        response = await client.post(
+            self.base_path, headers=random_user.auth_headers, content=bucket_info.model_dump_json()
+        )
 
         assert response.status_code == status.HTTP_201_CREATED
-        bucket = BucketOut.model_validate(response.json())
+        bucket = BucketOut.model_validate_json(response.content)
         assert bucket
         assert bucket.name == bucket_info.name
         assert bucket.owner_id == random_user.user.uid
 
-        dbBucket = await CRUDBucket.get(db, bucket_info.name)
-        assert dbBucket
-        assert dbBucket.name == bucket_info.name
-        assert dbBucket.owner_id == random_user.user.uid
+        db_bucket = await CRUDBucket.get(db, bucket_info.name)
+        assert db_bucket
+        assert db_bucket.name == bucket_info.name
+        assert db_bucket.owner_id == random_user.user.uid
 
     @pytest.mark.asyncio
     async def test_create_duplicated_bucket(
@@ -194,18 +234,121 @@ class TestBucketRoutesCreate(_TestBucketRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         bucket_info = BucketIn(name=random_bucket.name, description=random_lower_string(127))
-        response = await client.post(self.base_path, headers=random_user.auth_headers, json=bucket_info.model_dump())
+        response = await client.post(
+            self.base_path, headers=random_user.auth_headers, content=bucket_info.model_dump_json()
+        )
 
         assert response.status_code == status.HTTP_400_BAD_REQUEST
 
 
+class TestBucketRoutesUpdate(_TestBucketRoutes):
+    @pytest.mark.asyncio
+    async def test_make_bucket_public(
+        self,
+        client: AsyncClient,
+        random_bucket: Bucket,
+        random_user: UserWithAuthHeader,
+        mock_s3_service: MockS3ServiceResource,
+    ) -> None:
+        """
+        Test for getting a foreign public bucket.
+
+        Parameters
+        ----------
+        client : httpx.AsyncClient
+            HTTP Client to perform the request on.
+        random_bucket : clowmdb.models.Bucket
+            Random bucket for testing.
+        random_user : app.tests.utils.user.UserWithAuthHeader
+            Random user who is owner of the bucket.
+        mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource
+            Mock S3 Service to manipulate objects.
+        """
+        response = await client.patch(f"{self.base_path}/{random_bucket.name}/public", headers=random_user.auth_headers)
+        assert response.status_code == status.HTTP_200_OK
+
+        bucket = BucketOut.model_validate_json(response.content)
+
+        assert bucket.name == random_bucket.name
+        assert bucket.public
+
+        assert ANONYMOUS_ACCESS_SID in mock_s3_service.Bucket(bucket.name).Policy().policy
+
+    @pytest.mark.asyncio
+    async def test_make_bucket_private(
+        self,
+        client: AsyncClient,
+        random_bucket: Bucket,
+        random_user: UserWithAuthHeader,
+        mock_s3_service: MockS3ServiceResource,
+        db: AsyncSession,
+    ) -> None:
+        """
+        Test for getting a foreign public bucket.
+
+        Parameters
+        ----------
+        client : httpx.AsyncClient
+            HTTP Client to perform the request on.
+        random_bucket : clowmdb.models.Bucket
+            Random bucket for testing.
+        random_user : app.tests.utils.user.UserWithAuthHeader
+            Random user who is owner of the bucket.
+        mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource
+            Mock S3 Service to manipulate objects.
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        """
+        await make_bucket_public(db=db, s3=mock_s3_service, bucket_name=random_bucket.name)
+        response = await client.patch(f"{self.base_path}/{random_bucket.name}/public", headers=random_user.auth_headers)
+        assert response.status_code == status.HTTP_200_OK
+
+        bucket = BucketOut.model_validate_json(response.content)
+
+        assert bucket.name == random_bucket.name
+        assert not bucket.public
+
+        assert ANONYMOUS_ACCESS_SID not in mock_s3_service.Bucket(bucket.name).Policy().policy
+
+    @pytest.mark.asyncio
+    async def test_make_bucket_public_with_owner_constraint(
+        self,
+        client: AsyncClient,
+        random_bucket: Bucket,
+        random_user: UserWithAuthHeader,
+        db: AsyncSession,
+    ) -> None:
+        """
+        Test for getting a foreign public bucket.
+
+        Parameters
+        ----------
+        client : httpx.AsyncClient
+            HTTP Client to perform the request on.
+        random_bucket : clowmdb.models.Bucket
+            Random bucket for testing.
+        random_user : app.tests.utils.user.UserWithAuthHeader
+            Random user who is owner of the bucket.
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        """
+        update_stmt = (
+            update(Bucket).where(Bucket.name == random_bucket.name).values(owner_constraint=Bucket.Constraint.READ)
+        )
+        await db.execute(update_stmt)
+        await db.commit()
+
+        response = await client.patch(f"{self.base_path}/{random_bucket.name}/public", headers=random_user.auth_headers)
+        assert response.status_code == status.HTTP_400_BAD_REQUEST
+
+
 class TestBucketRoutesDelete(_TestBucketRoutes):
     @pytest.mark.asyncio
     async def test_delete_empty_bucket(
@@ -220,11 +363,11 @@ class TestBucketRoutesDelete(_TestBucketRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         response = await client.delete(
             f"{self.base_path}/{random_bucket.name}", headers=random_user.auth_headers, params={"force_delete": False}
@@ -237,8 +380,8 @@ class TestBucketRoutesDelete(_TestBucketRoutes):
         self,
         client: AsyncClient,
         db: AsyncSession,
-        random_user: UserWithAuthHeader,
         random_second_user: UserWithAuthHeader,
+        random_third_user: UserWithAuthHeader,
         cleanup: CleanupList,
     ) -> None:
         """
@@ -246,12 +389,14 @@ class TestBucketRoutesDelete(_TestBucketRoutes):
 
         Parameters
         ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
-        random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            HTTP Client to perform the request on.
+        random_third_user : app.tests.utils.user.UserWithAuthHeader
+            Random user for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random user which is not the owner of the bucket. pytest fixture.
+            Random user who is not the owner of the bucket.
         cleanup : app.tests.utils.utils.CleanupList
             Cleanup object where (async) functions can be registered which get executed after a (failed) test.
         """
@@ -268,11 +413,11 @@ class TestBucketRoutesDelete(_TestBucketRoutes):
             bucket_name=bucket.name,
         )
         await add_permission_for_bucket(
-            db, bucket.name, random_user.user.uid, permission=BucketPermission.Permission.READWRITE
+            db, bucket.name, random_third_user.user.uid, permission=BucketPermission.Permission.READWRITE
         )
 
         response = await client.delete(
-            f"{self.base_path}/{bucket.name}", headers=random_user.auth_headers, params={"force_delete": False}
+            f"{self.base_path}/{bucket.name}", headers=random_third_user.auth_headers, params={"force_delete": False}
         )
 
         assert response.status_code == status.HTTP_403_FORBIDDEN
@@ -291,13 +436,13 @@ class TestBucketRoutesDelete(_TestBucketRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource
-            Mock S3 Service to manipulate objects. pytest fixture.
+            Mock S3 Service to manipulate objects.
         """
         mock_s3_service.create_object_in_bucket(bucket_name=random_bucket.name, key=random_lower_string())
         response = await client.delete(
@@ -320,13 +465,13 @@ class TestBucketRoutesDelete(_TestBucketRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource
-            Mock S3 Service to manipulate objects. pytest fixture.
+            Mock S3 Service to manipulate objects.
         """
         mock_s3_service.create_object_in_bucket(bucket_name=random_bucket.name, key=random_lower_string())
         response = await client.delete(
diff --git a/app/tests/api/test_s3_keys.py b/app/tests/api/test_s3_keys.py
index 1413da4aa9de94c44e6ec23ca97c4920e3df9ec5..46c37a2d00da0ad638675d98ffe6f69ea4778024 100644
--- a/app/tests/api/test_s3_keys.py
+++ b/app/tests/api/test_s3_keys.py
@@ -1,6 +1,7 @@
 import pytest
 from fastapi import status
 from httpx import AsyncClient
+from pydantic import TypeAdapter
 
 from app.schemas.s3key import S3Key
 from app.tests.mocks.mock_rgw_admin import MockRGWAdmin
@@ -25,11 +26,11 @@ class TestS3KeyRoutesGet(_TestS3KeyRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random foreign user for testing. pytest fixture.
+            Random foreign user for testing.
         """
         response = await client.get(
             f"{self.base_path}/{str(random_second_user.user.uid)}/keys", headers=random_user.auth_headers
@@ -49,18 +50,19 @@ class TestS3KeyRoutesGet(_TestS3KeyRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         response = await client.get(
             f"{self.base_path}/{str(random_user.user.uid)}/keys", headers=random_user.auth_headers
         )
-        keys = response.json()
         assert response.status_code == status.HTTP_200_OK
-        assert isinstance(keys, list)
+
+        ta = TypeAdapter(list[S3Key])
+        keys = ta.validate_json(response.content)
         assert len(keys) == 1
-        assert S3Key.model_validate(keys[0]).uid == random_user.user.uid
+        assert keys[0].uid == random_user.user.uid
 
     @pytest.mark.asyncio
     async def test_get_specific_s3_key_for_user(
@@ -75,18 +77,18 @@ class TestS3KeyRoutesGet(_TestS3KeyRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         mock_rgw_admin : app.tests.mocks.mock_rgw_admin.MockRGWAdmin
-            Mock class for rgwadmin package. pytest fixture.
+            Mock class for rgwadmin package.
         """
         s3_key = mock_rgw_admin.get_user(uid=str(random_user.user.uid))["keys"][0]
         response = await client.get(
             f"{self.base_path}/{str(random_user.user.uid)}/keys/{s3_key['access_key']}",
             headers=random_user.auth_headers,
         )
-        response_key = S3Key.model_validate(response.json())
+        response_key = S3Key.model_validate_json(response.content)
         assert response.status_code == status.HTTP_200_OK
         assert response_key.access_key == s3_key["access_key"]
         assert response_key.secret_key == s3_key["secret_key"]
@@ -106,13 +108,13 @@ class TestS3KeyRoutesGet(_TestS3KeyRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         mock_rgw_admin : app.tests.mocks.mock_rgw_admin.MockRGWAdmin
-            Mock class for rgwadmin package. pytest fixture.
+            Mock class for rgwadmin package.
         """
         s3_key = mock_rgw_admin.get_user(uid=str(random_user.user.uid))["keys"][0]
         response = await client.get(
@@ -129,9 +131,9 @@ class TestS3KeyRoutesGet(_TestS3KeyRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         response = await client.get(
             f"{self.base_path}/{str(random_user.user.uid)}/keys/impossible_key", headers=random_user.auth_headers
@@ -153,17 +155,17 @@ class TestS3KeyRoutesCreate(_TestS3KeyRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         mock_rgw_admin : app.tests.mocks.mock_rgw_admin.MockRGWAdmin
-            Mock class for rgwadmin package. pytest fixture.
+            Mock class for rgwadmin package.
         """
         old_s3_key = mock_rgw_admin.get_user(uid=str(random_user.user.uid))["keys"][0]
         response = await client.post(
             f"{self.base_path}/{str(random_user.user.uid)}/keys", headers=random_user.auth_headers
         )
-        new_key = S3Key.model_validate(response.json())
+        new_key = S3Key.model_validate_json(response.content)
 
         assert response.status_code == status.HTTP_201_CREATED
         assert new_key.access_key != old_s3_key["access_key"]
@@ -179,11 +181,11 @@ class TestS3KeyRoutesCreate(_TestS3KeyRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         response = await client.post(
             f"{self.base_path}/{str(random_second_user.user.uid)}/keys", headers=random_user.auth_headers
@@ -203,11 +205,11 @@ class TestS3KeyRoutesDelete(_TestS3KeyRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         mock_rgw_admin : app.tests.mocks.mock_rgw_admin.MockRGWAdmin
-            Mock class for rgwadmin package. pytest fixture.
+            Mock class for rgwadmin package.
         """
         new_s3_key = mock_rgw_admin.create_key(uid=str(random_user.user.uid))[-1]
         assert len(mock_rgw_admin.get_user(uid=str(random_user.user.uid))["keys"]) == 2
@@ -229,11 +231,11 @@ class TestS3KeyRoutesDelete(_TestS3KeyRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         mock_rgw_admin : app.tests.mocks.mock_rgw_admin.MockRGWAdmin
-            Mock class for rgwadmin package. pytest fixture.
+            Mock class for rgwadmin package.
         """
         assert len(mock_rgw_admin.get_user(uid=str(random_user.user.uid))["keys"]) == 1
         key_id = mock_rgw_admin.get_user(uid=str(random_user.user.uid))["keys"][0]
@@ -257,11 +259,11 @@ class TestS3KeyRoutesDelete(_TestS3KeyRoutes):
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         mock_rgw_admin : app.tests.mocks.mock_rgw_admin.MockRGWAdmin
-            Mock class for rgwadmin package. pytest fixture.
+            Mock class for rgwadmin package.
         """
         mock_rgw_admin.create_key(uid=str(random_user.user.uid))
         response = await client.delete(
diff --git a/app/tests/api/test_security.py b/app/tests/api/test_security.py
index d386176e47f9f5ce3a8727ec046dea34eeda5ecd..9ca034f911675112451f1bda9f0833a445c3c826 100644
--- a/app/tests/api/test_security.py
+++ b/app/tests/api/test_security.py
@@ -3,6 +3,8 @@ from fastapi import status
 from httpx import AsyncClient
 from sqlalchemy.ext.asyncio import AsyncSession
 
+from app.tests.mocks.mock_opa_service import MockOpaService
+from app.tests.utils.cleanup import CleanupList
 from app.tests.utils.user import UserWithAuthHeader
 
 
@@ -17,7 +19,7 @@ class TestJWTProtectedRoutes:
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         """
         response = await client.get(self.protected_route)
         error = response.json()
@@ -33,7 +35,7 @@ class TestJWTProtectedRoutes:
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         """
         response = await client.get(self.protected_route, headers={"Authorization": "Bearer not-a-jwt-token"})
         error = response.json()
@@ -49,12 +51,12 @@ class TestJWTProtectedRoutes:
         Parameters
         ----------
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         response = await client.get(
-            self.protected_route, params={"owner_id": str(random_user.user.uid)}, headers=random_user.auth_headers
+            self.protected_route, params={"maintainer_id": str(random_user.user.uid)}, headers=random_user.auth_headers
         )
         assert response.status_code == status.HTTP_200_OK
 
@@ -71,16 +73,48 @@ class TestJWTProtectedRoutes:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         client : httpx.AsyncClient
-            HTTP Client to perform the request on. pytest fixture.
+            HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         await db.delete(random_user.user)
         await db.commit()
 
         response = await client.get(
-            self.protected_route, params={"owner_id": str(random_user.user.uid)}, headers=random_user.auth_headers
+            self.protected_route, params={"user": str(random_user.user.uid)}, headers=random_user.auth_headers
         )
         assert response.status_code == status.HTTP_404_NOT_FOUND
+
+    @pytest.mark.asyncio
+    async def test_routed_with_insufficient_permissions(
+        self,
+        client: AsyncClient,
+        random_user: UserWithAuthHeader,
+        mock_opa_service: MockOpaService,
+        cleanup: CleanupList,
+    ) -> None:
+        """
+        Test with correct authorization header but with insufficient permissions.
+
+        Parameters
+        ----------
+        client : httpx.AsyncClient
+            HTTP Client to perform the request on.
+        random_user : app.tests.utils.user.UserWithAuthHeader
+            Random user for testing.
+        """
+
+        mock_opa_service.send_error = True
+
+        def reset_opa_service() -> None:
+            mock_opa_service.send_error = False
+
+        cleanup.add_task(reset_opa_service)
+
+        response = await client.get(
+            self.protected_route,
+            headers=random_user.auth_headers,
+        )
+        assert response.status_code == status.HTTP_403_FORBIDDEN
diff --git a/app/tests/conftest.py b/app/tests/conftest.py
index 1d269342168f2d65a97a5d7bb3140deab3c03f72..9abb2d95de4a23c4c5e3ab029a18fdc2d630caeb 100644
--- a/app/tests/conftest.py
+++ b/app/tests/conftest.py
@@ -2,8 +2,7 @@ import asyncio
 import json
 from functools import partial
 from secrets import token_urlsafe
-from typing import AsyncIterator, Callable, Generator
-from uuid import uuid4
+from typing import AsyncIterator, Generator, Iterator
 
 import httpx
 import pytest
@@ -13,17 +12,17 @@ from clowmdb.models import Bucket
 from clowmdb.models import BucketPermission as BucketPermissionDB
 from sqlalchemy.ext.asyncio import AsyncSession
 
-from app.api.dependencies import get_decode_jwt_function, get_httpx_client, get_rgw_admin, get_s3_resource
+from app.api.dependencies import get_db, get_decode_jwt_function, get_httpx_client, get_rgw_admin, get_s3_resource
 from app.core.config import settings
 from app.main import app
 from app.schemas.bucket_permission import BucketPermissionOut as BucketPermissionSchema
-from app.schemas.security import AuthzResponse
+from app.tests.mocks import DefaultMockHTTPService, MockHTTPService
+from app.tests.mocks.mock_opa_service import MockOpaService
 from app.tests.mocks.mock_rgw_admin import MockRGWAdmin
 from app.tests.mocks.mock_s3_resource import MockS3ServiceResource
 from app.tests.utils.bucket import create_random_bucket
 from app.tests.utils.cleanup import CleanupList
 from app.tests.utils.user import UserWithAuthHeader, create_random_user, decode_mock_token, get_authorization_headers
-from app.tests.utils.utils import request_admin_permission
 
 jwt_secret = token_urlsafe(32)
 
@@ -54,36 +53,49 @@ def mock_s3_service() -> MockS3ServiceResource:
     return MockS3ServiceResource()
 
 
+@pytest.fixture(scope="session")
+def mock_opa_service() -> Iterator[MockOpaService]:
+    mock_opa = MockOpaService()
+    yield mock_opa
+    mock_opa.reset()
+
+
+@pytest_asyncio.fixture(scope="session")
+async def mock_client(
+    mock_opa_service: MockOpaService,
+) -> AsyncIterator[httpx.AsyncClient]:
+    def mock_request_handler(request: httpx.Request) -> httpx.Response:
+        url = str(request.url)
+        handler: MockHTTPService
+        if url.startswith(str(settings.opa.uri)):
+            handler = mock_opa_service
+        else:
+            handler = DefaultMockHTTPService()
+        return handler.handle_request(request)
+
+    async with httpx.AsyncClient(transport=httpx.MockTransport(mock_request_handler)) as http_client:
+        yield http_client
+
+
 @pytest_asyncio.fixture(scope="module")
 async def client(
-    mock_rgw_admin: MockRGWAdmin, mock_s3_service: MockS3ServiceResource
+    mock_s3_service: MockS3ServiceResource,
+    db: AsyncSession,
+    mock_opa_service: MockOpaService,
+    mock_client: httpx.AsyncClient,
+    mock_rgw_admin: MockRGWAdmin,
 ) -> AsyncIterator[httpx.AsyncClient]:
     """
     Fixture for creating a TestClient and perform HTTP Request on it.
-    Overrides the dependency for the RGW admin operations.
+    Overrides several dependencies.
     """
 
-    def get_decode_token_function() -> Callable[[str], dict[str, str]]:
-        # Override the decode_jwt function with mock function for tests and inject random shared secret
-        return partial(decode_mock_token, secret=jwt_secret)
-
-    async def get_mock_httpx_client(allow_admin: bool = False) -> AsyncIterator[httpx.AsyncClient]:
-        def mock_request_handler(request: httpx.Request) -> httpx.Response:
-            response_body = {}
-            if str(request.url).startswith(str(settings.OPA_URI)):
-                response_body = AuthzResponse(
-                    result=not request_admin_permission(request) or allow_admin, decision_id=uuid4()
-                ).model_dump()
-            return httpx.Response(200, json=response_body)
-
-        async with httpx.AsyncClient(transport=httpx.MockTransport(mock_request_handler)) as http_client:
-            yield http_client
-
-    app.dependency_overrides[get_httpx_client] = get_mock_httpx_client
     app.dependency_overrides[get_rgw_admin] = lambda: mock_rgw_admin
+    app.dependency_overrides[get_httpx_client] = lambda: mock_client
     app.dependency_overrides[get_s3_resource] = lambda: mock_s3_service
-    app.dependency_overrides[get_decode_jwt_function] = get_decode_token_function
-    async with httpx.AsyncClient(app=app, base_url="http://localhost") as ac:
+    app.dependency_overrides[get_decode_jwt_function] = lambda: partial(decode_mock_token, secret=jwt_secret)
+    app.dependency_overrides[get_db] = lambda: db
+    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://localhost") as ac:  # type: ignore[arg-type]
         yield ac
     app.dependency_overrides = {}
 
@@ -93,46 +105,56 @@ async def db() -> AsyncIterator[AsyncSession]:
     """
     Fixture for creating a database session to connect to.
     """
-    async with get_async_session(
-        url=str(settings.SQLALCHEMY_DATABASE_ASYNC_URI), verbose=settings.SQLALCHEMY_VERBOSE_LOGGER
-    ) as dbSession:
+    async with get_async_session(url=str(settings.db.dsn_async), verbose=settings.db.verbose) as dbSession:
         yield dbSession
 
 
 @pytest_asyncio.fixture(scope="function")
-async def random_user(db: AsyncSession, mock_rgw_admin: MockRGWAdmin) -> AsyncIterator[UserWithAuthHeader]:
+async def random_user(
+    db: AsyncSession, mock_rgw_admin: MockRGWAdmin, mock_opa_service: MockOpaService
+) -> AsyncIterator[UserWithAuthHeader]:
     """
     Create a random user and deletes him afterwards.
     """
     user = await create_random_user(db)
     mock_rgw_admin.create_key(uid=str(user.uid))
+    mock_opa_service.add_user(user.lifescience_id, privileged=True)
     yield UserWithAuthHeader(user=user, auth_headers=get_authorization_headers(uid=user.uid, secret=jwt_secret))
+    mock_opa_service.delete_user(user.lifescience_id)
     mock_rgw_admin.delete_user(uid=str(user.uid))
     await db.delete(user)
     await db.commit()
 
 
 @pytest_asyncio.fixture(scope="function")
-async def random_second_user(db: AsyncSession, mock_rgw_admin: MockRGWAdmin) -> AsyncIterator[UserWithAuthHeader]:
+async def random_second_user(
+    db: AsyncSession, mock_rgw_admin: MockRGWAdmin, mock_opa_service: MockOpaService
+) -> AsyncIterator[UserWithAuthHeader]:
     """
     Create a random second user and deletes him afterwards.
     """
     user = await create_random_user(db)
     mock_rgw_admin.create_key(uid=str(user.uid))
+    mock_opa_service.add_user(user.lifescience_id)
     yield UserWithAuthHeader(user=user, auth_headers=get_authorization_headers(uid=user.uid, secret=jwt_secret))
+    mock_opa_service.delete_user(user.lifescience_id)
     mock_rgw_admin.delete_user(uid=str(user.uid))
     await db.delete(user)
     await db.commit()
 
 
 @pytest_asyncio.fixture(scope="function")
-async def random_third_user(db: AsyncSession, mock_rgw_admin: MockRGWAdmin) -> AsyncIterator[UserWithAuthHeader]:
+async def random_third_user(
+    db: AsyncSession, mock_rgw_admin: MockRGWAdmin, mock_opa_service: MockOpaService
+) -> AsyncIterator[UserWithAuthHeader]:
     """
     Create a random third user and deletes him afterwards.
     """
     user = await create_random_user(db)
     mock_rgw_admin.create_key(uid=str(user.uid))
+    mock_opa_service.add_user(user.lifescience_id)
     yield UserWithAuthHeader(user=user, auth_headers=get_authorization_headers(uid=user.uid, secret=jwt_secret))
+    mock_opa_service.delete_user(user.lifescience_id)
     mock_rgw_admin.delete_user(uid=str(user.uid))
     await db.delete(user)
     await db.commit()
@@ -143,7 +165,7 @@ async def random_bucket(
     db: AsyncSession, random_user: UserWithAuthHeader, mock_s3_service: MockS3ServiceResource
 ) -> AsyncIterator[Bucket]:
     """
-    Create a random user and deletes him afterwards.
+    Create a random bucket and deletes it afterwards.
     """
     bucket = await create_random_bucket(db, random_user.user)
     mock_s3_service.Bucket(name=bucket.name).create()
diff --git a/app/tests/crud/test_bucket.py b/app/tests/crud/test_bucket.py
index 479feb03a38dc2f7ef04cadcc7109367ad9e842d..91e34361cbb26f47741d877263201d58df1ccbcf 100644
--- a/app/tests/crud/test_bucket.py
+++ b/app/tests/crud/test_bucket.py
@@ -23,9 +23,9 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         buckets = await CRUDBucket.get_all(db)
         assert len(buckets) == 1
@@ -42,9 +42,9 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         bucket = await CRUDBucket.get(db, random_bucket.name)
         assert bucket
@@ -60,7 +60,7 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         """
         bucket = await CRUDBucket.get(db, "unknown Bucket")
         assert bucket is None
@@ -73,9 +73,9 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         buckets = await CRUDBucket.get_for_user(db, random_bucket.owner_id, CRUDBucket.BucketType.OWN)
 
@@ -96,11 +96,11 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         cleanup : app.tests.utils.utils.CleanupList
             Cleanup object where (async) functions can be registered which get executed after a (failed) test.
         """
@@ -139,11 +139,11 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         cleanup : app.tests.utils.utils.CleanupList
             Cleanup object where (async) functions can be registered which get executed after a (failed) test.
         """
@@ -179,11 +179,11 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, permission=BucketPermission.Permission.READ
@@ -204,11 +204,11 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, permission=BucketPermission.Permission.READWRITE
@@ -229,11 +229,11 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, permission=BucketPermission.Permission.WRITE
@@ -254,11 +254,11 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         await add_permission_for_bucket(
             db,
@@ -283,11 +283,11 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, from_=datetime.now() + timedelta(days=10)
@@ -307,11 +307,11 @@ class TestBucketCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, to=datetime.now() - timedelta(days=10)
@@ -336,9 +336,9 @@ class TestBucketCRUDCreate:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing. pytest fixture.
+            Random user for testing.
         cleanup : app.tests.utils.utils.CleanupList
             Cleanup object where (async) functions can be registered which get executed after a (failed) test.
         """
@@ -369,15 +369,39 @@ class TestBucketCRUDCreate:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         bucket_info = BucketIn(name=random_bucket.name, description=random_lower_string(127))
         with pytest.raises(DuplicateError):
             await CRUDBucket.create(db, bucket_info, random_bucket.owner_id)
 
 
+class TestBucketCRUDUpdate:
+    @pytest.mark.asyncio
+    async def test_update_public_state(self, db: AsyncSession, random_bucket: Bucket) -> None:
+        """
+        Test for deleting a bucket with the CRUD Repository.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_bucket : clowmdb.models.Bucket
+            Random bucket for testing.
+        """
+        old_public_state = random_bucket.public
+        await CRUDBucket.update_public_state(db=db, bucket_name=random_bucket.name, public=not old_public_state)
+
+        stmt = select(Bucket).where(Bucket.name == random_bucket.name)
+        bucket_db = await db.scalar(stmt)
+
+        assert bucket_db is not None
+        assert bucket_db == random_bucket
+        assert old_public_state != bucket_db.public
+
+
 class TestBucketCRUDDelete:
     @pytest.mark.asyncio
     async def test_delete_bucket(self, db: AsyncSession, random_bucket: Bucket) -> None:
@@ -387,9 +411,9 @@ class TestBucketCRUDDelete:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         await CRUDBucket.delete(db, random_bucket.name)
 
diff --git a/app/tests/crud/test_bucket_permission.py b/app/tests/crud/test_bucket_permission.py
index b9a3e8ed15e7c2af99a704d940aad3b8fda8017a..36224dd2cde39750bde5466dc310aa9946992ba2 100644
--- a/app/tests/crud/test_bucket_permission.py
+++ b/app/tests/crud/test_bucket_permission.py
@@ -26,9 +26,9 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket_permission : clowmdb.models.BucketPermission
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         bucket_permission = await CRUDBucketPermission.get(
             db, bucket_name=random_bucket_permission.bucket_name, uid=random_bucket_permission.uid
@@ -48,9 +48,9 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket_permission : clowmdb.models.BucketPermission
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         bucket_permissions = await CRUDBucketPermission.list(db, bucket_name=random_bucket_permission.bucket_name)
         assert len(bucket_permissions) == 1
@@ -73,11 +73,11 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(db, random_bucket.name, random_second_user.user.uid)
         await add_permission_for_bucket(
@@ -103,11 +103,11 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(db, random_bucket.name, random_second_user.user.uid)
         await add_permission_for_bucket(
@@ -137,11 +137,11 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, from_=datetime.now() + timedelta(weeks=1)
@@ -170,11 +170,11 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, to=datetime.now() - timedelta(weeks=1)
@@ -203,11 +203,11 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(
             db,
@@ -244,11 +244,11 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, from_=datetime.now() + timedelta(weeks=1)
@@ -277,11 +277,11 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, to=datetime.now() - timedelta(weeks=1)
@@ -310,11 +310,11 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         random_third_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(
             db,
@@ -346,9 +346,9 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket_permission : clowmdb.models.BucketPermission
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         bucket_permissions = await CRUDBucketPermission.list(db, uid=random_bucket_permission.uid)
         assert len(bucket_permissions) == 1
@@ -370,9 +370,9 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, permission=BucketPermissionDB.Permission.WRITE
@@ -396,9 +396,9 @@ class TestBucketPermissionCRUDGet:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random third user who has no permissions for the bucket. pytest fixture.
+            Random third user who has no permissions for the bucket.
         """
         await add_permission_for_bucket(
             db, random_bucket.name, random_second_user.user.uid, from_=datetime.now() + timedelta(weeks=1)
@@ -419,9 +419,9 @@ class TestBucketPermissionCRUDCreate:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=uuid4())
         with pytest.raises(KeyError):
@@ -437,11 +437,11 @@ class TestBucketPermissionCRUDCreate:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_user : app.tests.utils.user.UserWithAuthHeader
-            Random user for testing who is owner of the bucket. pytest fixture.
+            Random user for testing who is owner of the bucket.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=random_user.user.uid)
         with pytest.raises(ValueError):
@@ -457,9 +457,9 @@ class TestBucketPermissionCRUDCreate:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket_permission_schema : app.schemas.bucket_permission.BucketPermissionOut
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         permission = BucketPermissionSchema(
             bucket_name=random_bucket_permission_schema.bucket_name, uid=random_bucket_permission_schema.uid
@@ -477,11 +477,11 @@ class TestBucketPermissionCRUDCreate:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_second_user : app.tests.utils.user.UserWithAuthHeader
-            Random second user for testing. pytest fixture.
+            Random second user for testing.
         random_bucket : clowmdb.models.Bucket
-            Random bucket for testing. pytest fixture.
+            Random bucket for testing.
         """
         permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=random_second_user.user.uid)
         created_permission = await CRUDBucketPermission.create(db, permission)
@@ -501,9 +501,9 @@ class TestBucketPermissionCRUDDelete:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket_permission : clowmdb.models.BucketPermission
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         await CRUDBucketPermission.delete(
             db, bucket_name=random_bucket_permission.bucket_name, uid=random_bucket_permission.uid
@@ -531,9 +531,9 @@ class TestBucketPermissionCRUDUpdate:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_bucket_permission : clowmdb.models.BucketPermission
-            Bucket permission for a random bucket for testing. pytest fixture.
+            Bucket permission for a random bucket for testing.
         """
         new_from_time = round(datetime(2022, 1, 1, 0, 0).timestamp())
         new_params = BucketPermissionParametersSchema(
diff --git a/app/tests/crud/test_user.py b/app/tests/crud/test_user.py
index c9856e1fb219f1b0d0b6f86718660404d7334fe1..261aa06ed63c9f12ca79ba209888e8390a9aef6a 100644
--- a/app/tests/crud/test_user.py
+++ b/app/tests/crud/test_user.py
@@ -16,9 +16,9 @@ class TestUserCRUD:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         random_user : clowmdb.models.User
-            Random user for testing. pytest fixture.
+            Random user for testing.
         """
         user = await CRUDUser.get(db, random_user.user.uid)
         assert user
@@ -36,7 +36,7 @@ class TestUserCRUD:
         Parameters
         ----------
         db : sqlalchemy.ext.asyncio.AsyncSession.
-            Async database session to perform query on. pytest fixture.
+            Async database session to perform query on.
         """
         user = await CRUDUser.get(db, uuid4())
         assert user is None
diff --git a/app/tests/mocks/__init__.py b/app/tests/mocks/__init__.py
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..39fd604074907615c601d3679f21f64c83692e2e 100644
--- a/app/tests/mocks/__init__.py
+++ b/app/tests/mocks/__init__.py
@@ -0,0 +1,23 @@
+from abc import ABC, abstractmethod
+
+from fastapi import status
+from httpx import Request, Response
+
+
+class MockHTTPService(ABC):
+    def __init__(self) -> None:
+        self.send_error = False
+
+    @abstractmethod
+    def handle_request(self, request: Request) -> Response: ...
+
+    def reset(self) -> None:
+        self.send_error = False
+
+
+class DefaultMockHTTPService(MockHTTPService):
+    def handle_request(self, request: Request) -> Response:
+        return Response(
+            status_code=status.HTTP_404_NOT_FOUND if self.send_error else status.HTTP_200_OK,
+            json={},
+        )
diff --git a/app/tests/mocks/mock_opa_service.py b/app/tests/mocks/mock_opa_service.py
new file mode 100644
index 0000000000000000000000000000000000000000..a9eccf47e8f6c5d5c4e62ced45b1d3ae6a7cec84
--- /dev/null
+++ b/app/tests/mocks/mock_opa_service.py
@@ -0,0 +1,103 @@
+import json
+from uuid import uuid4
+
+from fastapi import status
+from httpx import Request, Response
+
+from app.schemas.security import AuthzRequest, AuthzResponse
+from app.tests.mocks import MockHTTPService
+
+_json_header = {"content-type": "application/json"}
+
+
+class MockOpaService(MockHTTPService):
+    """
+    Class to mock the Open Policy Agent service.
+    Has a simplified role management. A user can be either "Admin" or "Normal User".
+    """
+
+    def __init__(self) -> None:
+        super().__init__()
+        self._users: dict[str, bool] = {}
+
+    def add_user(self, uid: str, privileged: bool = False) -> None:
+        """
+        Add a user to the mock service.
+
+        Parameters
+        ----------
+        uid : str
+            ID of a user.
+        privileged : bool, default False
+            Flag if the user is an Admin or not.
+        """
+        self._users[uid] = privileged
+
+    def delete_user(self, uid: str) -> None:
+        """
+        Delete a user in the mock service.
+
+        Parameters
+        ----------
+        uid : str
+            ID of the user to delete.
+        """
+        if uid in self._users.keys():
+            del self._users[uid]
+
+    def reset(self) -> None:
+        """
+        Reset the mock service to its initial state.
+        """
+        super().reset()
+        self._users = {}
+
+    def handle_request(self, request: Request, **kwargs: bool) -> Response:
+        """
+        Handle the raw request that is sent to the mock service.
+
+        Parameters
+        ----------
+        request: httpx.Request
+            Raw HTTP request object.
+
+        Returns
+        -------
+        response : httpx.Response
+            Appropriate response to the received request.
+        """
+        if request.url.path == "/v1/data/clowm/authz/allow":
+            authz_request = AuthzRequest.model_validate(json.loads(request.read().decode("utf-8"))["input"])
+            if self.send_error or authz_request.uid not in self._users:
+                result = False
+            else:
+                result = not MockOpaService.request_admin_permission(authz_request) or self._users[authz_request.uid]
+            return Response(
+                status_code=status.HTTP_200_OK,
+                headers=_json_header,
+                content=AuthzResponse(result=result, decision_id=uuid4()).model_dump_json(),
+            )
+        return Response(
+            status_code=status.HTTP_404_NOT_FOUND,
+            json={},
+        )
+
+    @staticmethod
+    def request_admin_permission(authz_request: AuthzRequest) -> bool:
+        """
+        Helper function to determine if the authorization request needs the 'administrator' role.
+
+        Parameters
+        ----------
+        authz_request : app.schemas.security.AuthzRequest
+            Body of the request.
+
+        Returns
+        -------
+        decision : bool
+            Flag if the request needs the 'administrator' role
+        """
+        checks = "any" in authz_request.operation
+        if "bucket_permission" in authz_request.resource:
+            checks = checks or "all" in authz_request.operation
+        return checks
diff --git a/app/tests/unit/test_bucket_permission_scheme.py b/app/tests/unit/test_bucket_permission_scheme.py
index 263d0afc61a8d6eb5bf64f708de42f701fcddbb6..28dec2abce39c41dd6aa17edfc099ae6beac0055 100644
--- a/app/tests/unit/test_bucket_permission_scheme.py
+++ b/app/tests/unit/test_bucket_permission_scheme.py
@@ -29,7 +29,7 @@ class TestPermissionPolicyPermissionType(_TestPermissionPolicy):
         Parameters
         ----------
         random_base_permission : app.schemas.bucket_permission.BucketPermissionOut
-            Random base bucket permission for testing. pytest fixture.
+            Random base bucket permission for testing.
         """
         uid = uuid4()
         stmts = random_base_permission.map_to_bucket_policy_statement(uid=uid)
@@ -62,7 +62,7 @@ class TestPermissionPolicyPermissionType(_TestPermissionPolicy):
         Parameters
         ----------
         random_base_permission : app.schemas.bucket_permission.BucketPermissionOut
-            Random base bucket permission for testing. pytest fixture.
+            Random base bucket permission for testing.
         """
         random_base_permission.permission = BucketPermission.Permission.WRITE
         stmts = random_base_permission.map_to_bucket_policy_statement(uid=uuid4())
@@ -89,7 +89,7 @@ class TestPermissionPolicyPermissionType(_TestPermissionPolicy):
         Parameters
         ----------
         random_base_permission : app.schemas.bucket_permission.BucketPermissionOut
-            Random base bucket permission for testing. pytest fixture.
+            Random base bucket permission for testing.
         """
         random_base_permission.permission = BucketPermission.Permission.READWRITE
         stmts = random_base_permission.map_to_bucket_policy_statement(uid=uuid4())
@@ -119,7 +119,7 @@ class TestPermissionPolicyCondition(_TestPermissionPolicy):
         Parameters
         ----------
         random_base_permission : app.schemas.bucket_permission.BucketPermissionOut
-            Random base bucket permission for testing. pytest fixture.
+            Random base bucket permission for testing.
         """
         timestamp = round(datetime.now().timestamp())
         time = datetime.fromtimestamp(timestamp)  # avoid rounding error
@@ -147,7 +147,7 @@ class TestPermissionPolicyCondition(_TestPermissionPolicy):
         Parameters
         ----------
         random_base_permission : app.schemas.bucket_permission.BucketPermissionOut
-            Random base bucket permission for testing. pytest fixture.
+            Random base bucket permission for testing.
         """
         time = datetime.now()
         timestamp = round(datetime.now().timestamp())
@@ -176,7 +176,7 @@ class TestPermissionPolicyCondition(_TestPermissionPolicy):
         Parameters
         ----------
         random_base_permission : app.schemas.bucket_permission.BucketPermissionOut
-            Random base bucket permission for testing. pytest fixture.
+            Random base bucket permission for testing.
         """
         random_base_permission.file_prefix = random_lower_string(length=8) + "/" + random_lower_string(length=8) + "/"
 
diff --git a/app/tests/utils/bucket.py b/app/tests/utils/bucket.py
index 98bbfeaceaab770a4ff6f2d6c387744ba0a91623..cb1f01ca57a91714eecb33ad3428288cedc65a5f 100644
--- a/app/tests/utils/bucket.py
+++ b/app/tests/utils/bucket.py
@@ -1,10 +1,14 @@
+import json
 from datetime import datetime
 from uuid import UUID
 
 from clowmdb.models import Bucket, BucketPermission, User
-from sqlalchemy import delete
+from sqlalchemy import delete, update
 from sqlalchemy.ext.asyncio import AsyncSession
 
+from app.api.endpoints.buckets import get_anonymously_bucket_policy
+from app.tests.mocks.mock_s3_resource import MockS3ServiceResource
+
 from .utils import random_lower_string
 
 
@@ -78,3 +82,23 @@ async def add_permission_for_bucket(
     )
     db.add(perm)
     await db.commit()
+
+
+async def make_bucket_public(db: AsyncSession, bucket_name: str, s3: MockS3ServiceResource) -> None:
+    """
+    Creates Permission to a bucket for a user in the database.
+
+    Parameters
+    ----------
+    db : sqlalchemy.ext.asyncio.AsyncSession.
+        Async database session to perform query on.
+    bucket_name : str
+        Name of the bucket.
+    s3 : app.tests.mocks.mock_s3_resource.MockS3ServiceResource
+        Mock S3 Service to manipulate objects.
+    """
+    await db.execute(update(Bucket).where(Bucket.name == bucket_name).values(public=True))
+    await db.commit()
+    policy = json.loads(s3.Bucket(bucket_name).Policy().policy)
+    policy["Statement"] += get_anonymously_bucket_policy(bucket_name)
+    s3.Bucket(bucket_name).Policy().put(Policy=json.dumps(policy))
diff --git a/app/tests/utils/utils.py b/app/tests/utils/utils.py
index ba7a1a4fb4a9ccc51cbb1ac6e45c04db0470b6a6..d38ad6f14a161b07f42f9d6cc6bb156d94aca895 100644
--- a/app/tests/utils/utils.py
+++ b/app/tests/utils/utils.py
@@ -1,8 +1,6 @@
 import random
 import string
 
-import httpx
-
 
 def random_lower_string(length: int = 32) -> str:
     """
@@ -31,25 +29,3 @@ def random_ipv4_string() -> str:
         Random IPv4 address.
     """
     return ".".join(str(random.randint(0, 255)) for _ in range(4))
-
-
-def request_admin_permission(request: httpx.Request) -> bool:
-    """
-    Rudimentary helper function to determine if the authorization request needs the 'administrator' role.
-
-    Parameters
-    ----------
-    request : httpx.Request
-        Raw Request Object from httpx
-
-    Returns
-    -------
-    decision : bool
-        Flag if the request needs the 'administrator' role
-    """
-    request_body: dict[str, str] = eval(request.content.decode("utf-8"))["input"]
-    operation = request_body["operation"]
-    checks = "any" in operation
-    if "bucket_permission" in request_body["resource"]:
-        checks = checks or "all" in operation
-    return checks
diff --git a/example-config/example-config.json b/example-config/example-config.json
new file mode 100644
index 0000000000000000000000000000000000000000..171b9a3d582ededbf4d3521118ba28305ade3982
--- /dev/null
+++ b/example-config/example-config.json
@@ -0,0 +1,27 @@
+{
+  "db": {
+    "port": 3306,
+    "host": "localhost",
+    "user": "db-user",
+    "name": "db-name",
+    "password": "db-password",
+    "verbose": false
+  },
+  "s3": {
+    "uri": "https://s3-staging.bi.denbi.de",
+    "access_key": "xxx",
+    "secret_key": "xxx",
+    "username": "clowm-bucket-manager",
+    "admin_access_key": "xxx",
+    "admin_secret_key": "xxx"
+  },
+  "opa": {
+    "uri": "http://localhost:8181"
+  },
+  "otlp": {
+    "grpc_endpoint": "localhost:4317"
+  },
+  "api_prefix": "/api/s3proxy-service",
+  "public_key_file": "/path/to/key.pub",
+  "ui_uri": "http://localhost:9999"
+}
diff --git a/example-config/example-config.toml b/example-config/example-config.toml
new file mode 100644
index 0000000000000000000000000000000000000000..894951bac907e0325c724492f4ce9af6b5071f2c
--- /dev/null
+++ b/example-config/example-config.toml
@@ -0,0 +1,21 @@
+api_prefix = "/api/s3proxy-service"
+public_key_file= "/path/to/key.pub"
+ui_uri = "http://localhost:9999"
+[db]
+  port = 3306
+  host = "localhost"
+  user = "db-user"
+  name = "db-name"
+  password = "db-password"
+  verbose = false
+[s3]
+  uri = "https://s3-staging.bi.denbi.de"
+  access_key = "xxx"
+  secret_key = "xxx"
+  username = "clowm-bucket-manager"
+  admin_access_key = "xxx"
+  admin_secret_key = "xxx"
+[opa]
+  uri = "http://localhost:8181"
+[otlp]
+  grpc_endpoint = "localhost:4317"
diff --git a/example-config/example-config.yaml b/example-config/example-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..2280b5bd1e60e3b65acab82eae1bc39d33aed125
--- /dev/null
+++ b/example-config/example-config.yaml
@@ -0,0 +1,22 @@
+db:
+  port: 3306
+  host: "localhost"
+  user: "db-user"
+  name: "db-name"
+  password: "db-password"
+  verbose: false
+s3:
+  uri: "https://s3-staging.bi.denbi.de"
+  access_key: "xxx"
+  secret_key: "xxx"
+  resource_bucket: "clowm-resources"
+  username: "clowm-bucket-manager"
+  admin_access_key: "xxx"
+  admin_secret_key: "xxx"
+opa:
+  uri: "http://localhost:8181"
+otlp:
+  grpc_endpoint: "localhost:4317"
+api_prefix: "/api/s3proxy-service"
+public_key_file: "/path/to/key.pub"
+ui_uri: "http://localhost:9999"
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 7d331e2bbcfc83d8f42f1d89338d3328b28b60f5..9efc29a1be52a79a2300481986955bbb7b4c380f 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,17 +1,17 @@
 # test packages
-pytest>=8.0.0,<8.1.0
+pytest>=8.0.0,<8.2.0
 pytest-asyncio>=0.21.0,<0.22.0
-pytest-cov>=4.1.0,<4.2.0
+pytest-cov>=5.0.0,<5.1.0
 coverage[toml]>=7.4.0,<7.5.0
 # Linters
-ruff<0.3.0
-black>=24.2.0,<24.3.0
+ruff<0.4.0
+black>=24.2.0,<24.5.0
 isort>=5.13.0,<5.14.0
-mypy>=1.8.0,<1.9.0
+mypy>=1.8.0,<1.10.0
 # stubs for mypy
 boto3-stubs-lite[s3]>=1.34.0,<1.35.0
 types-requests
 # Miscellaneous
-pre-commit>=3.6.0,<3.7.0
+pre-commit>=3.6.0,<3.8.0
 python-dotenv
-uvicorn>=0.27.0,<0.28.0
+uvicorn>=0.27.0,<0.30.0
diff --git a/requirements.txt b/requirements.txt
index c334cf154cd215103f4b47de6feea81871d4a234..86c5c650b70353f740cd249e8f6b0edf34870f16 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,9 +2,10 @@
 clowmdb>=3.1.0,<3.2.0
 
 # Webserver packages
-fastapi>=0.109.0,<0.110.0
-pydantic<2.7.0
-pydantic-settings<2.3.0
+fastapi>=0.109.0,<0.111.0
+pydantic<2.8.0
+pydantic-settings[yaml, toml]>=2.2.0,<2.3.0
+email-validator>=2.1.0,<2.2.0
 # Database packages
 PyMySQL>=1.1.0,<1.2.0
 SQLAlchemy>=2.0.0,<2.1.0
diff --git a/scripts/lint.sh b/scripts/lint.sh
index 5246a7ff93a6c8e930a3069db441f0b66b2d5504..3ae0ba81d6719f3be5502679b18e0a149209d6f6 100755
--- a/scripts/lint.sh
+++ b/scripts/lint.sh
@@ -2,7 +2,14 @@
 
 set -x
 
+ruff --version
 ruff check app
-black app --check
+
+isort --version
 isort -c app
+
+black --version
+black app --check
+
+mypy --version
 mypy app