Skip to content
Snippets Groups Projects
Verified Commit 5c4e550c authored by Daniel Göbel's avatar Daniel Göbel
Browse files

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
parent 4e7ee32b
No related branches found
No related tags found
2 merge requests!71Delete dev branch,!56Resolve "Set CORS rule on Bucket level"
Pipeline #34709 passed
...@@ -21,21 +21,22 @@ user-friendly manner. 👍 ...@@ -21,21 +21,22 @@ user-friendly manner. 👍
### Mandatory / Recommended Variables ### Mandatory / Recommended Variables
| Variable | Default | Value | Description | | Variable | Default | Value | Description |
|----------------------------------------|--------------------|---------------------------------|------------------------------------------------------------------------------------| |----------------------------------------|-------------------------|---------------------------------|------------------------------------------------------------------------------------|
| `DB_HOST` | unset | <db hostname / IP> | IP or Hostname Adress of DB | | `DB_HOST` | unset | <db hostname / IP> | IP or Hostname Address of DB |
| `DB_PORT` | 3306 | Number | Port of the database | | `DB_PORT` | 3306 | Number | Port of the database |
| `DB_USER` | unset | \<db username> | Username of the database user | | `DB_USER` | unset | \<db username> | Username of the database user |
| `DB_PASSWORD` | unset | \<db password> | Password of the database user | | `DB_PASSWORD` | unset | \<db password> | Password of the database user |
| `DB_DATABASE` | unset | \<db name> | Name of the database | | `DB_DATABASE` | unset | \<db name> | Name of the database |
| `OBJECT_GATEWAY_URI` | unset | HTTP URL | HTTP URL of the Ceph Object Gateway | | `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_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_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` | | `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_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. | | `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 | | `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 | | `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 ### Optional Variables
...@@ -45,7 +46,3 @@ user-friendly manner. 👍 ...@@ -45,7 +46,3 @@ user-friendly manner. 👍
| `BACKEND_CORS_ORIGINS` | `[]` | json formatted list of urls | List of valid CORS origins | | `BACKEND_CORS_ORIGINS` | `[]` | json formatted list of urls | List of valid CORS origins |
| `SQLALCHEMY_VERBOSE_LOGGER` | `false` | `<"true"&#x7c;"false">` | Enables verbose SQL output.<br>Should be `false` in production | | `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 | | `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.
...@@ -22,6 +22,31 @@ router = APIRouter(prefix="/buckets", tags=["Bucket"]) ...@@ -22,6 +22,31 @@ router = APIRouter(prefix="/buckets", tags=["Bucket"])
bucket_authorization = AuthorizationDependency(resource="bucket") bucket_authorization = AuthorizationDependency(resource="bucket")
Authorization = Annotated[Callable[[str], Awaitable[Any]], Depends(bucket_authorization)] 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") @router.get("", response_model=List[BucketOutSchema], summary="List buckets of user")
async def list_buckets( async def list_buckets(
...@@ -151,6 +176,7 @@ async def create_bucket( ...@@ -151,6 +176,7 @@ async def create_bucket(
} }
) )
s3_bucket.Policy().put(Policy=bucket_policy) s3_bucket.Policy().put(Policy=bucket_policy)
s3_bucket.Cors().put(CORSConfiguration=cors_rule) # type: ignore[arg-type]
return BucketOutSchema( return BucketOutSchema(
**{ **{
"description": db_bucket.description, "description": db_bucket.description,
......
from pathlib import Path 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 from pydantic_settings import BaseSettings, SettingsConfigDict
...@@ -29,6 +29,11 @@ def _load_public_key(pub_key_val: Optional[str], pub_key_file: Optional[Path]) - ...@@ -29,6 +29,11 @@ def _load_public_key(pub_key_val: Optional[str], pub_key_file: Optional[Path]) -
class Settings(BaseSettings): 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.") API_PREFIX: str = Field("/api", description="Path Prefix for all API endpoints.")
public_key_value: Optional[str] = Field( public_key_value: Optional[str] = Field(
...@@ -46,15 +51,6 @@ class Settings(BaseSettings): ...@@ -46,15 +51,6 @@ class Settings(BaseSettings):
# BACKEND_CORS_ORIGINS is a JSON-formatted list of origins # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = Field([], description="List of all valid CORS 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_HOST: str = Field(..., description="Host of the database.")
DB_USER: str = Field(..., description="Username in the database.") DB_USER: str = Field(..., description="Username in the database.")
DB_PASSWORD: str = Field(..., description="Password for the database user.") DB_PASSWORD: str = Field(..., description="Password for the database user.")
......
from datetime import datetime from datetime import datetime
from typing import Dict, List, Optional from typing import TYPE_CHECKING, Dict, List, Optional
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
if TYPE_CHECKING:
from mypy_boto3_s3.type_defs import CORSConfigurationTypeDef
else:
CORSConfigurationTypeDef = object
class MockS3Object: class MockS3Object:
""" """
...@@ -120,6 +125,52 @@ class MockS3BucketPolicy: ...@@ -120,6 +125,52 @@ class MockS3BucketPolicy:
self.policy = Policy 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: class MockS3Bucket:
""" """
Mock S3 bucket for the boto3 Bucket for testing purposes. Mock S3 bucket for the boto3 Bucket for testing purposes.
...@@ -236,6 +287,7 @@ class MockS3Bucket: ...@@ -236,6 +287,7 @@ class MockS3Bucket:
self.objects = MockS3Bucket.MockS3ObjectList() self.objects = MockS3Bucket.MockS3ObjectList()
self._parent_service: MockS3ServiceResource = parent_service self._parent_service: MockS3ServiceResource = parent_service
self.policy = MockS3BucketPolicy(name) self.policy = MockS3BucketPolicy(name)
self.cors = MockS3CorsRule()
def Policy(self) -> MockS3BucketPolicy: def Policy(self) -> MockS3BucketPolicy:
""" """
...@@ -248,6 +300,9 @@ class MockS3Bucket: ...@@ -248,6 +300,9 @@ class MockS3Bucket:
""" """
return self.policy return self.policy
def Cors(self) -> MockS3CorsRule:
return self.cors
def create(self) -> None: def create(self) -> None:
""" """
Create the bucket in the mock S3 service. Create the bucket in the mock S3 service.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment