diff --git a/README.md b/README.md index cfbff3d3dc826cbaff2fef99d21a20ad8548bfe3..5e67fe7e42ab2052fb5807eb984fdaca9284fb03 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 8888ad51786d01aedffe848a55f9f8ee099bf222..6dc2036a14468fbbcca021d9db0914934664dcaf 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 1492a586239abdac5f4112e18790ba006c8d4f0f..08a9c43a31334a0054001186de1753fb9aa1246b 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 e67ad2caff404c2e55b1aa94c3db6a011f3b3ec5..b302dd403b502166644f2649756ac913deaae530 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.