|
import hypothesis.strategies as st |
|
import tempfile |
|
from typing import Any, Dict, List, Optional |
|
import yaml |
|
|
|
import string |
|
|
|
from chromadb import TokenTransportHeader |
|
from chromadb.test.property.strategies import collection_name |
|
|
|
|
|
@st.composite |
|
def many_unique_names(draw: st.DrawFn) -> List[str]: |
|
|
|
|
|
|
|
|
|
length = len(valid_action_space) * 10 * 10 * 2 * 2 |
|
names = draw( |
|
st.lists( |
|
collection_name(), |
|
min_size=length, |
|
max_size=length, |
|
) |
|
) |
|
seen = set() |
|
unseen = [] |
|
for name in names: |
|
if name in seen: |
|
continue |
|
seen.add(name) |
|
unseen.append(name) |
|
return unseen |
|
|
|
|
|
@st.composite |
|
def random_token(draw: st.DrawFn) -> str: |
|
return draw( |
|
st.text(alphabet=string.ascii_letters + string.digits, min_size=1, max_size=50) |
|
) |
|
|
|
|
|
@st.composite |
|
def random_token_transport_header(draw: st.DrawFn) -> Optional[str]: |
|
return draw( |
|
st.sampled_from( |
|
[ |
|
TokenTransportHeader.AUTHORIZATION, |
|
TokenTransportHeader.X_CHROMA_TOKEN, |
|
None, |
|
] |
|
) |
|
) |
|
|
|
|
|
@st.composite |
|
def random_user_name(draw: st.DrawFn) -> str: |
|
return draw(st.text(alphabet=string.ascii_letters, min_size=1, max_size=20)) |
|
|
|
|
|
@st.composite |
|
def random_users_with_tokens(draw: st.DrawFn) -> List[Dict[str, Any]]: |
|
users = draw( |
|
st.lists( |
|
st.fixed_dictionaries( |
|
{ |
|
"id": random_user_name(), |
|
"tokens": st.lists(random_token(), min_size=3, max_size=10), |
|
} |
|
), |
|
min_size=1, |
|
max_size=1, |
|
) |
|
) |
|
unseen_users = [] |
|
seen_users = set() |
|
seen_tokens = set() |
|
for user in users: |
|
if user["id"] in seen_users: |
|
break |
|
for token in user["tokens"]: |
|
if token in seen_tokens: |
|
break |
|
else: |
|
seen_users.add(user["id"]) |
|
for token in user["tokens"]: |
|
seen_tokens.add(token) |
|
|
|
unseen_users.append(user) |
|
return unseen_users |
|
|
|
|
|
@st.composite |
|
def token_test_conf(draw: st.DrawFn) -> Dict[str, Any]: |
|
users = draw(random_users_with_tokens()) |
|
filename = _dump_to_tmpfile({"users": users}) |
|
return {"users": users, "filename": filename} |
|
|
|
|
|
valid_action_space = [ |
|
"system:reset", |
|
"tenant:create_tenant", |
|
"tenant:get_tenant", |
|
"db:create_database", |
|
"db:get_database", |
|
"db:list_collections", |
|
"db:create_collection", |
|
"db:get_or_create_collection", |
|
"collection:get_collection", |
|
"collection:delete_collection", |
|
"collection:update_collection", |
|
"collection:add", |
|
"collection:delete", |
|
"collection:get", |
|
"collection:query", |
|
"collection:peek", |
|
"collection:count", |
|
"collection:update", |
|
"collection:upsert", |
|
] |
|
|
|
|
|
def unauthorized_actions(authorized_actions: List[str]) -> List[str]: |
|
return [action for action in valid_action_space if action not in authorized_actions] |
|
|
|
|
|
@st.composite |
|
def random_role_name(draw: st.DrawFn) -> str: |
|
return draw(st.text(alphabet=string.ascii_letters, min_size=1, max_size=20)) |
|
|
|
|
|
@st.composite |
|
def random_action(draw: st.DrawFn) -> str: |
|
return draw(st.sampled_from(valid_action_space)) |
|
|
|
|
|
@st.composite |
|
def random_allowed_actions_for_role(draw: st.DrawFn) -> List[str]: |
|
actions = draw(st.sets(random_action(), min_size=1, max_size=10)) |
|
|
|
if any( |
|
action in actions |
|
for action in [ |
|
"collection:add", |
|
"collection:delete", |
|
"collection:get", |
|
"collection:query", |
|
"collection:peek", |
|
"collection:update", |
|
"collection:upsert", |
|
"collection:count", |
|
"collection:update_collection", |
|
"collection:delete_collection", |
|
] |
|
): |
|
actions.add("collection:get_collection") |
|
|
|
if "collection:peek" in actions: |
|
actions.add("collection:get") |
|
|
|
actions.add("tenant:get_tenant") |
|
actions.add("db:get_database") |
|
return list(actions) |
|
|
|
|
|
@st.composite |
|
def random_roles(draw: st.DrawFn) -> List[Dict[str, Any]]: |
|
roles = draw( |
|
st.lists( |
|
st.fixed_dictionaries( |
|
{"id": random_role_name(), "actions": random_allowed_actions_for_role()} |
|
), |
|
min_size=1, |
|
max_size=10, |
|
), |
|
) |
|
unseen_roles = [] |
|
seen = set() |
|
for role in roles: |
|
if role["id"] in seen: |
|
continue |
|
seen.add(role["id"]) |
|
unseen_roles.append(role) |
|
return unseen_roles |
|
|
|
|
|
def _transform_roles_for_flush(roles: List[Dict[str, Any]]) -> Dict[str, Any]: |
|
roles_mapping = {} |
|
for role in roles: |
|
roles_mapping.update({role["id"]: {"actions": role["actions"]}}) |
|
return roles_mapping |
|
|
|
|
|
@st.composite |
|
def random_users_and_roles(draw: st.DrawFn) -> Dict[str, Any]: |
|
users = draw(random_users_with_tokens()) |
|
roles = draw(random_roles()) |
|
for user in users: |
|
role_index = draw(st.integers(min_value=0, max_value=len(roles) - 1)) |
|
user["role"] = roles[role_index]["id"] |
|
return {"users": users, "roles": roles} |
|
|
|
|
|
def _root_user_and_role() -> Dict[str, Any]: |
|
return { |
|
"users": [{"id": "__root__", "tokens": ["__root__"], "role": "__root__"}], |
|
"roles": [{"id": "__root__", "actions": valid_action_space}], |
|
} |
|
|
|
|
|
@st.composite |
|
def rbac_test_conf(draw: st.DrawFn) -> Dict[str, Any]: |
|
users_and_roles = draw(random_users_and_roles()) |
|
root_user_and_role = _root_user_and_role() |
|
users_and_roles["users"].extend(root_user_and_role["users"]) |
|
users_and_roles["roles"].extend(root_user_and_role["roles"]) |
|
|
|
filename = _dump_to_tmpfile( |
|
{ |
|
"users": users_and_roles["users"], |
|
"roles_mapping": _transform_roles_for_flush(users_and_roles["roles"]), |
|
} |
|
) |
|
return { |
|
"users": users_and_roles["users"], |
|
"roles": users_and_roles["roles"], |
|
"filename": filename, |
|
} |
|
|
|
|
|
def _dump_to_tmpfile(data: Any) -> str: |
|
tmp = tempfile.NamedTemporaryFile(delete=False) |
|
with open(tmp.name, "w") as f: |
|
yaml.dump(data, f) |
|
return tmp.name |
|
|