forked from phoenix-oss/llama-stack-mirror
This PR introduces a way to implement Attribute Based Access Control (ABAC) for the Llama Stack server. The rough design is: - https://github.com/meta-llama/llama-stack/pull/1626 added a way for the Llama Stack server to query an authenticator - We build upon that and expect "access attributes" as part of the response. These attributes indicate the scopes available for the request. - We use these attributes to perform access control for registered resources as well as for constructing the default access control policies for newly created resources. - By default, if you support authentication but don't return access attributes, we will add a unique namespace pointing to the API_KEY. That way, all resources by default will be scoped to API_KEYs. An important aspect of this design is that Llama Stack stays out of the business of credential management or the CRUD for attributes. How you manage your namespaces or projects is entirely up to you. The design only implements access control checks for the metadata / book-keeping information that the Stack tracks. ### Limitations - Currently, read vs. write vs. admin permissions aren't made explicit, but this can be easily extended by adding appropriate attributes to the `AccessAttributes` data structure. - This design does not apply to agent instances since they are not considered resources the Stack knows about. Agent instances are completely within the scope of the Agents API provider. ### Test Plan Added unit tests, existing integration tests
81 lines
3 KiB
Python
81 lines
3 KiB
Python
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
# All rights reserved.
|
|
#
|
|
# This source code is licensed under the terms described in the LICENSE file in
|
|
# the root directory of this source tree.
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
from llama_stack.distribution.datatypes import RoutableObjectWithProvider
|
|
from llama_stack.log import get_logger
|
|
|
|
logger = get_logger(__name__, category="core")
|
|
|
|
|
|
def check_access(obj: RoutableObjectWithProvider, user_attributes: Optional[Dict[str, Any]] = None) -> bool:
|
|
"""Check if the current user has access to the given object, based on access attributes.
|
|
|
|
Access control algorithm:
|
|
1. If the resource has no access_attributes, access is GRANTED to all authenticated users
|
|
2. If the user has no attributes, access is DENIED to any object with access_attributes defined
|
|
3. For each attribute category in the resource's access_attributes:
|
|
a. If the user lacks that category, access is DENIED
|
|
b. If the user has the category but none of the required values, access is DENIED
|
|
c. If the user has at least one matching value in each required category, access is GRANTED
|
|
|
|
Example:
|
|
# Resource requires:
|
|
access_attributes = AccessAttributes(
|
|
roles=["admin", "data-scientist"],
|
|
teams=["ml-team"]
|
|
)
|
|
|
|
# User has:
|
|
user_attributes = {
|
|
"roles": ["data-scientist", "engineer"],
|
|
"teams": ["ml-team", "infra-team"],
|
|
"projects": ["llama-3"]
|
|
}
|
|
|
|
# Result: Access GRANTED
|
|
# - User has the "data-scientist" role (matches one of the required roles)
|
|
# - AND user is part of the "ml-team" (matches the required team)
|
|
# - The extra "projects" attribute is ignored
|
|
|
|
Args:
|
|
obj: The resource object to check access for
|
|
|
|
Returns:
|
|
bool: True if access is granted, False if denied
|
|
"""
|
|
# If object has no access attributes, allow access by default
|
|
if not hasattr(obj, "access_attributes") or not obj.access_attributes:
|
|
return True
|
|
|
|
# If no user attributes, deny access to objects with access control
|
|
if not user_attributes:
|
|
return False
|
|
|
|
obj_attributes = obj.access_attributes.model_dump(exclude_none=True)
|
|
if not obj_attributes:
|
|
return True
|
|
|
|
# Check each attribute category (requires ALL categories to match)
|
|
for attr_key, required_values in obj_attributes.items():
|
|
user_values = user_attributes.get(attr_key, [])
|
|
|
|
if not user_values:
|
|
logger.debug(
|
|
f"Access denied to {obj.type} '{obj.identifier}': missing required attribute category '{attr_key}'"
|
|
)
|
|
return False
|
|
|
|
if not any(val in user_values for val in required_values):
|
|
logger.debug(
|
|
f"Access denied to {obj.type} '{obj.identifier}': "
|
|
f"no match for attribute '{attr_key}', required one of {required_values}"
|
|
)
|
|
return False
|
|
|
|
logger.debug(f"Access granted to {obj.type} '{obj.identifier}'")
|
|
return True
|