Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Always pass Tenant and DB as query params #1558

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/chroma-js-client-generate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Chroma Test JS Client Generation

on:
push:
branches:
- main
pull_request:
branches:
- main
- '**'
workflow_dispatch:

jobs:
generate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install Python Dependencies
run: pip install -r requirements.txt && pip install -r requirements_dev.txt
- name: Run Server
run: python -m chromadb.cli.cli run &
- name: Setup Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
- name: Install Dependencies
run: yarn
working-directory: ./clients/js
- name: Generate
run: yarn genapi
working-directory: ./clients/js
7 changes: 4 additions & 3 deletions chromadb/api/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ def get_database(
) -> Database:
"""Returns a database"""
resp = self._session.get(
self._api_url + "/databases/" + name,
params={"tenant": tenant},
self._api_url + "/databases",
params={"tenant": tenant, "database": name},
)
raise_chroma_error(resp)
resp_json = resp.json()
Expand All @@ -193,7 +193,8 @@ def create_tenant(self, name: str) -> None:
@override
def get_tenant(self, name: str) -> Tenant:
resp = self._session.get(
self._api_url + "/tenants/" + name,
self._api_url + "/tenants",
params={"tenant": name},
)
raise_chroma_error(resp)
resp_json = resp.json()
Expand Down
107 changes: 55 additions & 52 deletions chromadb/auth/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,58 +178,61 @@ def wrapped(*args: Any, **kwargs: Dict[Any, Any]) -> Any:
"function_kwargs": kwargs,
}
request = request_var.get()
if request:
_provider = authz_provider.get()
a_list: List[Union[str, AuthzAction]] = []
if not isinstance(action, list):
a_list = [action]
else:
a_list = cast(List[Union[str, AuthzAction]], action)
a_authz_responses = []
for a in a_list:
_action = a if isinstance(a, AuthzAction) else AuthzAction(id=a)
_resource = (
resource
if isinstance(resource, AuthzResource)
else resource.to_authz_resource(**_dynamic_kwargs)
)
_context = AuthorizationContext(
user=AuthzUser(
id=request.state.user_identity.get_user_id()
if hasattr(request.state, "user_identity")
else "Anonymous",
tenant=request.state.user_identity.get_user_tenant()
if hasattr(request.state, "user_identity")
else DEFAULT_TENANT,
attributes=request.state.user_identity.get_user_attributes()
if hasattr(request.state, "user_identity")
else {},
),
resource=_resource,
action=_action,
)

if _provider:
a_authz_responses.append(_provider.authorize(_context))
if not any(a_authz_responses):
raise AuthorizationError("Unauthorized")
# In a multi-tenant environment, we may want to allow users to send
# requests without configuring a tenant and DB. If so, they can set
# the request tenant and DB however they like and we simply overwrite it.
if overwrite_singleton_tenant_database_access_from_auth:
desired_tenant = request.state.user_identity.get_user_tenant()
if desired_tenant and "tenant" in kwargs:
if isinstance(kwargs["tenant"], str):
kwargs["tenant"] = desired_tenant
elif isinstance(kwargs["tenant"], chromadb.server.fastapi.types.CreateTenant):
kwargs["tenant"].name = desired_tenant
databases = request.state.user_identity.get_user_databases()
if databases and len(databases) == 1 and "database" in kwargs:
desired_database = databases[0]
if isinstance(kwargs["database"], str):
kwargs["database"] = desired_database
elif isinstance(kwargs["database"], chromadb.server.fastapi.types.CreateDatabase):
kwargs["database"].name = desired_database
if not request:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only change to this file: remove a layer of indentation to make the code marginally cleaner

return f(*args, **kwargs)

_provider = authz_provider.get()
a_list: List[Union[str, AuthzAction]] = []
if not isinstance(action, list):
a_list = [action]
else:
a_list = cast(List[Union[str, AuthzAction]], action)
a_authz_responses = []
for a in a_list:
_action = a if isinstance(a, AuthzAction) else AuthzAction(id=a)
_resource = (
resource
if isinstance(resource, AuthzResource)
else resource.to_authz_resource(**_dynamic_kwargs)
)
_context = AuthorizationContext(
user=AuthzUser(
id=request.state.user_identity.get_user_id()
if hasattr(request.state, "user_identity")
else "Anonymous",
tenant=request.state.user_identity.get_user_tenant()
if hasattr(request.state, "user_identity")
else DEFAULT_TENANT,
attributes=request.state.user_identity.get_user_attributes()
if hasattr(request.state, "user_identity")
else {},
),
resource=_resource,
action=_action,
)

if _provider:
a_authz_responses.append(_provider.authorize(_context))
if not any(a_authz_responses):
raise AuthorizationError("Unauthorized")

# In a multi-tenant environment, we may want to allow users to send
# requests without configuring a tenant and DB. If so, they can set
# the request tenant and DB however they like and we simply overwrite it.
if overwrite_singleton_tenant_database_access_from_auth:
desired_tenant = request.state.user_identity.get_user_tenant()
# Only overwrite if the user didn't specify a tenant but the
# method requires one.
if desired_tenant and "tenant" in kwargs and not kwargs["tenant"]:
if isinstance(kwargs["tenant"], str):
kwargs["tenant"] = desired_tenant
databases = request.state.user_identity.get_user_databases()
# Only overwrite if the user didn't specify a database but the
# method requires one.
if databases and len(databases) == 1 and "database" in kwargs and not kwargs["database"]:
desired_database = databases[0]
if isinstance(kwargs["database"], str):
kwargs["database"] = desired_database

return f(*args, **kwargs)

Expand Down
39 changes: 1 addition & 38 deletions chromadb/auth/fastapi_utils.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,6 @@
from functools import partial
from typing import Any, Callable, Dict, Optional, Sequence, cast
from typing import Any, Callable, Dict, cast
from chromadb.api import ServerAPI
from chromadb.auth import AuthzResourceTypes


def find_key_with_value_of_type(
type: AuthzResourceTypes, **kwargs: Any
) -> Dict[str, Any]:
from chromadb.server.fastapi.types import (
CreateCollection,
CreateDatabase,
CreateTenant,
)

for key, value in kwargs.items():
if type == AuthzResourceTypes.DB and isinstance(value, CreateDatabase):
return dict(value)
elif type == AuthzResourceTypes.COLLECTION and isinstance(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this was never used in the AuthzResourceTypes.COLLECTION case

value, CreateCollection
):
return dict(value)
elif type == AuthzResourceTypes.TENANT and isinstance(value, CreateTenant):
return dict(value)
return {}


def attr_from_resource_object(
type: AuthzResourceTypes,
additional_attrs: Optional[Sequence[str]] = None,
**kwargs: Any,
) -> Callable[..., Dict[str, Any]]:
def _wrap(**wkwargs: Any) -> Dict[str, Any]:
obj = find_key_with_value_of_type(type, **wkwargs)
if additional_attrs:
obj.update({k: wkwargs["function_kwargs"][k]
for k in additional_attrs})
return obj

return partial(_wrap, **kwargs)


def attr_from_collection_lookup(
Expand Down
26 changes: 13 additions & 13 deletions chromadb/server/fastapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
)
from chromadb.auth.fastapi_utils import (
attr_from_collection_lookup,
attr_from_resource_object,
)
from chromadb.config import DEFAULT_DATABASE, DEFAULT_TENANT, Settings, System
import chromadb.api
Expand All @@ -38,8 +37,6 @@
)
from chromadb.server.fastapi.types import (
AddEmbedding,
CreateDatabase,
CreateTenant,
DeleteEmbedding,
GetEmbedding,
QueryEmbedding,
Expand Down Expand Up @@ -180,7 +177,7 @@ def __init__(self, settings: Settings):
)

self.router.add_api_route(
"/api/v1/databases/{database}",
"/api/v1/databases",
self.get_database,
methods=["GET"],
response_model=None,
Expand All @@ -194,7 +191,7 @@ def __init__(self, settings: Settings):
)

self.router.add_api_route(
"/api/v1/tenants/{tenant}",
"/api/v1/tenants",
self.get_tenant,
methods=["GET"],
response_model=None,
Expand Down Expand Up @@ -303,15 +300,15 @@ def version(self) -> str:
action=AuthzResourceActions.CREATE_DATABASE,
resource=DynamicAuthzResource(
type=AuthzResourceTypes.DB,
attributes=attr_from_resource_object(
type=AuthzResourceTypes.DB, additional_attrs=["tenant"]
attributes=AuthzDynamicParams.dict_from_function_kwargs(
arg_names=["tenant", "database"]
),
),
)
def create_database(
self, database: CreateDatabase, tenant: str = DEFAULT_TENANT
self, database: str = DEFAULT_DATABASE, tenant: str = DEFAULT_TENANT
) -> None:
return self._api.create_database(database.name, tenant)
return self._api.create_database(database, tenant)

@trace_method("FastAPI.get_database", OpenTelemetryGranularity.OPERATION)
@authz_context(
Expand All @@ -324,7 +321,7 @@ def create_database(
),
),
)
def get_database(self, database: str, tenant: str = DEFAULT_TENANT) -> Database:
def get_database(self, database: str = DEFAULT_DATABASE, tenant: str = DEFAULT_TENANT) -> Database:
return self._api.get_database(database, tenant)

@trace_method("FastAPI.create_tenant", OpenTelemetryGranularity.OPERATION)
Expand All @@ -334,18 +331,21 @@ def get_database(self, database: str, tenant: str = DEFAULT_TENANT) -> Database:
type=AuthzResourceTypes.TENANT,
),
)
def create_tenant(self, tenant: CreateTenant) -> None:
return self._api.create_tenant(tenant.name)
def create_tenant(self, tenant: str = DEFAULT_TENANT) -> None:
return self._api.create_tenant(tenant)

@trace_method("FastAPI.get_tenant", OpenTelemetryGranularity.OPERATION)
@authz_context(
action=AuthzResourceActions.GET_TENANT,
resource=DynamicAuthzResource(
id="*",
type=AuthzResourceTypes.TENANT,
attributes=AuthzDynamicParams.dict_from_function_kwargs(
arg_names=["tenant"]
),
),
)
def get_tenant(self, tenant: str) -> Tenant:
def get_tenant(self, tenant: str = DEFAULT_TENANT) -> Tenant:
return self._api.get_tenant(tenant)

@trace_method("FastAPI.list_collections", OpenTelemetryGranularity.OPERATION)
Expand Down
8 changes: 0 additions & 8 deletions chromadb/server/fastapi/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,3 @@ class CreateCollection(BaseModel):
class UpdateCollection(BaseModel):
new_name: Optional[str] = None
new_metadata: Optional[CollectionMetadata] = None


class CreateDatabase(BaseModel):
name: str


class CreateTenant(BaseModel):
name: str
7 changes: 6 additions & 1 deletion clients/js/genapi.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/bin/bash

# curl -s http://localhost:8000/openapi.json | jq > openapi.json
curl -s http://localhost:8000/openapi.json | python -c "import sys, json; print(json.dumps(json.load(sys.stdin), indent=2))" > openapi.json
Expand All @@ -17,6 +17,11 @@ fi

openapi-generator-plus -c config.yml

if [ $? -ne 0 ]; then
echo "Generation failed"
exit 1
fi

if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' -e '/import "whatwg-fetch";/d' -e 's/window.fetch/fetch/g' src/generated/runtime.ts
else
Expand Down
Loading
Loading