- Updated python_coding_microproject.csv to use NVIDIA NIM Kimi K2 - Updated kali_linux_shell_simulator.csv to use NVIDIA NIM Kimi K2 - Made more general-purpose (flexible targets, expanded tools) - Added nemotron-mini-agent.csv for fast local inference via Ollama - Added nemotron-agent.csv for advanced reasoning via Ollama - Added wiki page: Projects for NVIDIA NIMs and Nemotron
451 lines
17 KiB
Python
451 lines
17 KiB
Python
"""
|
|
Access Controller Service for GT 2.0
|
|
|
|
Manages resource access control with capability-based security.
|
|
Ensures perfect tenant isolation and proper permission cascading.
|
|
"""
|
|
|
|
import os
|
|
import stat
|
|
from typing import List, Optional, Dict, Any, Tuple
|
|
from datetime import datetime
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from app.models.access_group import (
|
|
AccessGroup, TenantStructure, User, Resource,
|
|
ResourceCreate, ResourceUpdate, ResourceResponse
|
|
)
|
|
from app.core.security import verify_capability_token
|
|
from app.core.database import get_db_session
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AccessController:
|
|
"""
|
|
Centralized access control service
|
|
Manages permissions for all resources with tenant isolation
|
|
"""
|
|
|
|
def __init__(self, tenant_domain: str):
|
|
self.tenant_domain = tenant_domain
|
|
self.base_path = Path(f"/data/{tenant_domain}")
|
|
self._ensure_tenant_directory()
|
|
|
|
def _ensure_tenant_directory(self):
|
|
"""
|
|
Ensure tenant directory exists with proper permissions
|
|
OS User: gt-{tenant_domain}-{pod_id}
|
|
Permissions: 700 (owner only)
|
|
"""
|
|
if not self.base_path.exists():
|
|
self.base_path.mkdir(parents=True, exist_ok=True)
|
|
# Set strict permissions - owner only
|
|
os.chmod(self.base_path, stat.S_IRWXU) # 700
|
|
logger.info(f"Created tenant directory: {self.base_path} with 700 permissions")
|
|
|
|
async def check_permission(
|
|
self,
|
|
user_id: str,
|
|
resource: Resource,
|
|
action: str = "read"
|
|
) -> Tuple[bool, Optional[str]]:
|
|
"""
|
|
Check if user has permission for action on resource
|
|
|
|
Args:
|
|
user_id: User requesting access
|
|
resource: Resource being accessed
|
|
action: read, write, delete, share
|
|
|
|
Returns:
|
|
Tuple of (allowed, reason)
|
|
"""
|
|
# Verify tenant isolation
|
|
if resource.tenant_domain != self.tenant_domain:
|
|
logger.warning(f"Cross-tenant access attempt: {user_id} -> {resource.id}")
|
|
return False, "Cross-tenant access denied"
|
|
|
|
# Owner has all permissions
|
|
if resource.owner_id == user_id:
|
|
return True, "Owner access granted"
|
|
|
|
# Check action-specific permissions
|
|
if action == "read":
|
|
return self._check_read_permission(user_id, resource)
|
|
elif action == "write":
|
|
return self._check_write_permission(user_id, resource)
|
|
elif action == "delete":
|
|
return False, "Only owner can delete"
|
|
elif action == "share":
|
|
return False, "Only owner can share"
|
|
else:
|
|
return False, f"Unknown action: {action}"
|
|
|
|
def _check_read_permission(self, user_id: str, resource: Resource) -> Tuple[bool, str]:
|
|
"""Check read permission based on access group"""
|
|
if resource.access_group == AccessGroup.ORGANIZATION:
|
|
return True, "Organization-wide read access"
|
|
elif resource.access_group == AccessGroup.TEAM:
|
|
if user_id in resource.team_members:
|
|
return True, "Team member read access"
|
|
return False, "Not a team member"
|
|
else: # INDIVIDUAL
|
|
return False, "Private resource"
|
|
|
|
def _check_write_permission(self, user_id: str, resource: Resource) -> Tuple[bool, str]:
|
|
"""Check write permission - only owner can write"""
|
|
return False, "Only owner can modify"
|
|
|
|
async def create_resource(
|
|
self,
|
|
user_id: str,
|
|
resource_data: ResourceCreate,
|
|
capability_token: str
|
|
) -> Resource:
|
|
"""
|
|
Create a new resource with proper access control
|
|
|
|
Args:
|
|
user_id: User creating the resource
|
|
resource_data: Resource creation data
|
|
capability_token: JWT capability token
|
|
|
|
Returns:
|
|
Created resource
|
|
"""
|
|
# Verify capability token
|
|
token_data = verify_capability_token(capability_token)
|
|
if not token_data or token_data.get("tenant_id") != self.tenant_domain:
|
|
raise PermissionError("Invalid capability token")
|
|
|
|
# Create resource
|
|
resource = Resource(
|
|
id=self._generate_resource_id(),
|
|
name=resource_data.name,
|
|
resource_type=resource_data.resource_type,
|
|
owner_id=user_id,
|
|
tenant_domain=self.tenant_domain,
|
|
access_group=resource_data.access_group,
|
|
team_members=resource_data.team_members or [],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow(),
|
|
metadata=resource_data.metadata or {},
|
|
file_path=None
|
|
)
|
|
|
|
# Create file-based storage if needed
|
|
if self._requires_file_storage(resource.resource_type):
|
|
resource.file_path = await self._create_resource_file(resource)
|
|
|
|
# Audit log
|
|
logger.info(f"Resource created: {resource.id} by {user_id} in {self.tenant_domain}")
|
|
|
|
return resource
|
|
|
|
async def update_resource_access(
|
|
self,
|
|
user_id: str,
|
|
resource_id: str,
|
|
new_access_group: AccessGroup,
|
|
team_members: Optional[List[str]] = None
|
|
) -> Resource:
|
|
"""
|
|
Update resource access group
|
|
|
|
Args:
|
|
user_id: User requesting update
|
|
resource_id: Resource to update
|
|
new_access_group: New access level
|
|
team_members: Team members if team access
|
|
|
|
Returns:
|
|
Updated resource
|
|
"""
|
|
# Load resource
|
|
resource = await self._load_resource(resource_id)
|
|
|
|
# Check permission
|
|
allowed, reason = await self.check_permission(user_id, resource, "share")
|
|
if not allowed:
|
|
raise PermissionError(f"Access denied: {reason}")
|
|
|
|
# Update access
|
|
old_group = resource.access_group
|
|
resource.update_access_group(new_access_group, team_members)
|
|
|
|
# Update file permissions if needed
|
|
if resource.file_path:
|
|
await self._update_file_permissions(resource)
|
|
|
|
# Audit log
|
|
logger.info(
|
|
f"Access updated: {resource_id} from {old_group} to {new_access_group} "
|
|
f"by {user_id}"
|
|
)
|
|
|
|
return resource
|
|
|
|
async def list_accessible_resources(
|
|
self,
|
|
user_id: str,
|
|
resource_type: Optional[str] = None
|
|
) -> List[Resource]:
|
|
"""
|
|
List all resources accessible to user
|
|
|
|
Args:
|
|
user_id: User requesting list
|
|
resource_type: Filter by type
|
|
|
|
Returns:
|
|
List of accessible resources
|
|
"""
|
|
accessible = []
|
|
|
|
# Get all resources in tenant
|
|
all_resources = await self._list_tenant_resources(resource_type)
|
|
|
|
for resource in all_resources:
|
|
allowed, _ = await self.check_permission(user_id, resource, "read")
|
|
if allowed:
|
|
accessible.append(resource)
|
|
|
|
return accessible
|
|
|
|
async def get_resource_stats(self, user_id: str) -> Dict[str, Any]:
|
|
"""
|
|
Get resource statistics for user
|
|
|
|
Args:
|
|
user_id: User to get stats for
|
|
|
|
Returns:
|
|
Statistics dictionary
|
|
"""
|
|
all_resources = await self._list_tenant_resources()
|
|
|
|
owned = [r for r in all_resources if r.owner_id == user_id]
|
|
accessible = await self.list_accessible_resources(user_id)
|
|
|
|
stats = {
|
|
"owned_count": len(owned),
|
|
"accessible_count": len(accessible),
|
|
"by_type": {},
|
|
"by_access_group": {
|
|
AccessGroup.INDIVIDUAL: 0,
|
|
AccessGroup.TEAM: 0,
|
|
AccessGroup.ORGANIZATION: 0
|
|
}
|
|
}
|
|
|
|
for resource in owned:
|
|
# Count by type
|
|
if resource.resource_type not in stats["by_type"]:
|
|
stats["by_type"][resource.resource_type] = 0
|
|
stats["by_type"][resource.resource_type] += 1
|
|
|
|
# Count by access group
|
|
stats["by_access_group"][resource.access_group] += 1
|
|
|
|
return stats
|
|
|
|
def _generate_resource_id(self) -> str:
|
|
"""Generate unique resource ID"""
|
|
import uuid
|
|
return str(uuid.uuid4())
|
|
|
|
def _requires_file_storage(self, resource_type: str) -> bool:
|
|
"""Check if resource type requires file storage"""
|
|
file_based_types = [
|
|
"agent", "dataset", "document", "workflow",
|
|
"notebook", "model", "configuration"
|
|
]
|
|
return resource_type in file_based_types
|
|
|
|
async def _create_resource_file(self, resource: Resource) -> str:
|
|
"""
|
|
Create file for resource with proper permissions
|
|
|
|
Args:
|
|
resource: Resource to create file for
|
|
|
|
Returns:
|
|
File path
|
|
"""
|
|
# Determine path based on resource type
|
|
type_dir = self.base_path / resource.resource_type / resource.id
|
|
type_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create main file
|
|
file_path = type_dir / "data.json"
|
|
file_path.touch()
|
|
|
|
# Set strict permissions - 700 for directory, 600 for file
|
|
os.chmod(type_dir, stat.S_IRWXU) # 700
|
|
os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR) # 600
|
|
|
|
logger.info(f"Created resource file: {file_path} with secure permissions")
|
|
|
|
return str(file_path)
|
|
|
|
async def _update_file_permissions(self, resource: Resource):
|
|
"""Update file permissions (always 700/600 for security)"""
|
|
if not resource.file_path or not Path(resource.file_path).exists():
|
|
return
|
|
|
|
# Permissions don't change based on access group
|
|
# All files remain 700/600 for OS-level security
|
|
# Access control is handled at application level
|
|
pass
|
|
|
|
async def _load_resource(self, resource_id: str) -> Resource:
|
|
"""Load resource from storage"""
|
|
try:
|
|
# Search for resource in all resource type directories
|
|
for resource_type_dir in self.base_path.iterdir():
|
|
if not resource_type_dir.is_dir():
|
|
continue
|
|
|
|
resource_file = resource_type_dir / "data.json"
|
|
if resource_file.exists():
|
|
try:
|
|
import json
|
|
with open(resource_file, 'r') as f:
|
|
resources_data = json.load(f)
|
|
|
|
if not isinstance(resources_data, list):
|
|
resources_data = [resources_data]
|
|
|
|
for resource_data in resources_data:
|
|
if resource_data.get('id') == resource_id:
|
|
return Resource(
|
|
id=resource_data['id'],
|
|
name=resource_data['name'],
|
|
resource_type=resource_data['resource_type'],
|
|
owner_id=resource_data['owner_id'],
|
|
tenant_domain=resource_data['tenant_domain'],
|
|
access_group=AccessGroup(resource_data['access_group']),
|
|
team_members=resource_data.get('team_members', []),
|
|
created_at=datetime.fromisoformat(resource_data['created_at']),
|
|
updated_at=datetime.fromisoformat(resource_data['updated_at']),
|
|
metadata=resource_data.get('metadata', {}),
|
|
file_path=resource_data.get('file_path')
|
|
)
|
|
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
|
logger.warning(f"Failed to parse resource file {resource_file}: {e}")
|
|
continue
|
|
|
|
raise ValueError(f"Resource {resource_id} not found")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to load resource {resource_id}: {e}")
|
|
raise
|
|
|
|
async def _list_tenant_resources(
|
|
self,
|
|
resource_type: Optional[str] = None
|
|
) -> List[Resource]:
|
|
"""List all resources in tenant"""
|
|
try:
|
|
import json
|
|
resources = []
|
|
|
|
# If specific resource type requested, search only that directory
|
|
search_dirs = [self.base_path / resource_type] if resource_type else list(self.base_path.iterdir())
|
|
|
|
for resource_type_dir in search_dirs:
|
|
if not resource_type_dir.exists() or not resource_type_dir.is_dir():
|
|
continue
|
|
|
|
resource_file = resource_type_dir / "data.json"
|
|
if resource_file.exists():
|
|
try:
|
|
with open(resource_file, 'r') as f:
|
|
resources_data = json.load(f)
|
|
|
|
if not isinstance(resources_data, list):
|
|
resources_data = [resources_data]
|
|
|
|
for resource_data in resources_data:
|
|
try:
|
|
resource = Resource(
|
|
id=resource_data['id'],
|
|
name=resource_data['name'],
|
|
resource_type=resource_data['resource_type'],
|
|
owner_id=resource_data['owner_id'],
|
|
tenant_domain=resource_data['tenant_domain'],
|
|
access_group=AccessGroup(resource_data['access_group']),
|
|
team_members=resource_data.get('team_members', []),
|
|
created_at=datetime.fromisoformat(resource_data['created_at']),
|
|
updated_at=datetime.fromisoformat(resource_data['updated_at']),
|
|
metadata=resource_data.get('metadata', {}),
|
|
file_path=resource_data.get('file_path')
|
|
)
|
|
resources.append(resource)
|
|
except (KeyError, ValueError) as e:
|
|
logger.warning(f"Failed to parse resource data: {e}")
|
|
continue
|
|
|
|
except (json.JSONDecodeError, IOError) as e:
|
|
logger.warning(f"Failed to read resource file {resource_file}: {e}")
|
|
continue
|
|
|
|
return resources
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to list tenant resources: {e}")
|
|
raise
|
|
|
|
|
|
class AccessControlMiddleware:
|
|
"""
|
|
Middleware for enforcing access control on API requests
|
|
"""
|
|
|
|
def __init__(self, tenant_domain: str):
|
|
self.controller = AccessController(tenant_domain)
|
|
|
|
async def verify_request(
|
|
self,
|
|
user_id: str,
|
|
resource_id: str,
|
|
action: str,
|
|
capability_token: str
|
|
) -> bool:
|
|
"""
|
|
Verify request has proper permissions
|
|
|
|
Args:
|
|
user_id: User making request
|
|
resource_id: Resource being accessed
|
|
action: Action being performed
|
|
capability_token: JWT capability token
|
|
|
|
Returns:
|
|
True if allowed, raises PermissionError if not
|
|
"""
|
|
# Verify capability token
|
|
token_data = verify_capability_token(capability_token)
|
|
if not token_data:
|
|
raise PermissionError("Invalid capability token")
|
|
|
|
# Verify tenant match
|
|
if token_data.get("tenant_id") != self.controller.tenant_domain:
|
|
raise PermissionError("Tenant mismatch in capability token")
|
|
|
|
# Load resource and check permission
|
|
resource = await self.controller._load_resource(resource_id)
|
|
allowed, reason = await self.controller.check_permission(
|
|
user_id, resource, action
|
|
)
|
|
|
|
if not allowed:
|
|
logger.warning(
|
|
f"Access denied: {user_id} -> {resource_id} ({action}): {reason}"
|
|
)
|
|
raise PermissionError(f"Access denied: {reason}")
|
|
|
|
return True |