From 5c4e550c07dc2a7c67e54057cc17aed347bbb3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Thu, 10 Aug 2023 17:06:50 +0200 Subject: [PATCH] Set CORS rules for created buckets When creating a bucket for a user, a CORS rule is set to allow access to the bucket from the website #59 --- README.md | 35 ++++++++---------- app/api/endpoints/buckets.py | 26 +++++++++++++ app/core/config.py | 18 ++++----- app/tests/mocks/mock_s3_resource.py | 57 ++++++++++++++++++++++++++++- 4 files changed, 105 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index cfbff3d..5e67fe7 100644 --- a/README.md +++ b/README.md @@ -21,21 +21,22 @@ user-friendly manner. 👠### Mandatory / Recommended Variables -| Variable | Default | Value | Description | -|----------------------------------------|--------------------|---------------------------------|------------------------------------------------------------------------------------| -| `DB_HOST` | unset | <db hostname / IP> | IP or Hostname Adress 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 | +| 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 @@ -45,7 +46,3 @@ user-friendly manner. 👠| `BACKEND_CORS_ORIGINS` | `[]` | json formatted list of urls | List of valid CORS origins | | `SQLALCHEMY_VERBOSE_LOGGER` | `false` | `<"true"|"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 | - -## Getting started -This service depends on multiple other services. See [DEVELOPING.md](DEVELOPING.md) how to set these up for developing -on your local machine. diff --git a/app/api/endpoints/buckets.py b/app/api/endpoints/buckets.py index 8888ad5..6dc2036 100644 --- a/app/api/endpoints/buckets.py +++ b/app/api/endpoints/buckets.py @@ -22,6 +22,31 @@ router = APIRouter(prefix="/buckets", tags=["Bucket"]) bucket_authorization = AuthorizationDependency(resource="bucket") Authorization = Annotated[Callable[[str], Awaitable[Any]], Depends(bucket_authorization)] +cors_rule = { + "CORSRules": [ + { + "ID": "websiteaccess", + "AllowedHeaders": [ + "amz-sdk-invocation-id", + "amz-sdk-request", + "authorization", + "content-type", + "x-amz-content-sha256", + "x-amz-copy-source", + "x-amz-date", + "x-amz-user-agent", + "content-md5", + ], + "AllowedMethods": ["GET", "PUT", "POST", "DELETE"], + "AllowedOrigins": [str(settings.CLOWM_URL)[:-1]], + "ExposeHeaders": [ + "Etag", + ], + "MaxAgeSeconds": 100, + }, + ] +} + @router.get("", response_model=List[BucketOutSchema], summary="List buckets of user") async def list_buckets( @@ -151,6 +176,7 @@ async def create_bucket( } ) s3_bucket.Policy().put(Policy=bucket_policy) + s3_bucket.Cors().put(CORSConfiguration=cors_rule) # type: ignore[arg-type] return BucketOutSchema( **{ "description": db_bucket.description, diff --git a/app/core/config.py b/app/core/config.py index 1492a58..08a9c43 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,7 +1,7 @@ from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional -from pydantic import AnyHttpUrl, AnyUrl, Field, computed_field, field_validator +from pydantic import AnyHttpUrl, AnyUrl, Field, computed_field from pydantic_settings import BaseSettings, SettingsConfigDict @@ -29,6 +29,11 @@ def _load_public_key(pub_key_val: Optional[str], pub_key_file: Optional[Path]) - 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: Optional[str] = Field( @@ -46,15 +51,6 @@ class Settings(BaseSettings): # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = Field([], description="List of all valid CORS origins") - @field_validator("BACKEND_CORS_ORIGINS", mode="before") - @classmethod - def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: - if isinstance(v, str) and not v.startswith("["): - return [i.strip() for i in v.split(",")] - elif isinstance(v, (list, str)): - return v - raise ValueError(v) - 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.") diff --git a/app/tests/mocks/mock_s3_resource.py b/app/tests/mocks/mock_s3_resource.py index e67ad2c..b302dd4 100644 --- a/app/tests/mocks/mock_s3_resource.py +++ b/app/tests/mocks/mock_s3_resource.py @@ -1,8 +1,13 @@ from datetime import datetime -from typing import Dict, List, Optional +from typing import TYPE_CHECKING, Dict, List, Optional from botocore.exceptions import ClientError +if TYPE_CHECKING: + from mypy_boto3_s3.type_defs import CORSConfigurationTypeDef +else: + CORSConfigurationTypeDef = object + class MockS3Object: """ @@ -120,6 +125,52 @@ class MockS3BucketPolicy: self.policy = Policy +class MockS3CorsRule: + """ + Mock S3 Cors Configuration for the boto3 BucketCors for testing purposes. + + Functions + --------- + put(CORSConfiguration: CORSConfig) -> None + Save a new bucket CORS rule. + + Attributes + ---------- + rules : str + List of all CORS rules on the bucket. + """ + + def __init__(self) -> None: + self.rules: Optional[CORSConfigurationTypeDef] = None + + def put(self, CORSConfiguration: CORSConfigurationTypeDef) -> None: + """ + Save a new bucket CORS rule. + + Parameters + ---------- + CORSConfiguration : mypy_boto3_s3.type_defs.CORSConfigurationTypeDef + The new policy as str. + + Notes + ----- + A configuration has the following form + { + "CORSRules": [ + { + "ID": string, + "AllowedHeaders": List[string], + "AllowedMethods": List[string], + "AllowedOrigins": List[string], + "ExposeHeaders": List[string], + "MaxAgeSeconds": int, + }, + ] + } + """ + self.rules = CORSConfiguration + + class MockS3Bucket: """ Mock S3 bucket for the boto3 Bucket for testing purposes. @@ -236,6 +287,7 @@ class MockS3Bucket: self.objects = MockS3Bucket.MockS3ObjectList() self._parent_service: MockS3ServiceResource = parent_service self.policy = MockS3BucketPolicy(name) + self.cors = MockS3CorsRule() def Policy(self) -> MockS3BucketPolicy: """ @@ -248,6 +300,9 @@ class MockS3Bucket: """ return self.policy + def Cors(self) -> MockS3CorsRule: + return self.cors + def create(self) -> None: """ Create the bucket in the mock S3 service. -- GitLab