Skip to main content

Hub Architecture

Hub is the orchestration platform for team deployments. It manages user authentication, access control, workflow storage, and execution scheduling.

Technology Stack

LayerTechnologyWhy
BackendFastAPIAsync, OpenAPI docs, dependency injection
DatabasePostgreSQLACID compliance, JSON support, proven
ORMSQLAlchemy 2.0Async support, type hints, migrations
FrontendReact + ViteFast dev experience, modern tooling
StateZustandSimple, no boilerplate, TypeScript-first

Directory Structure

hub/
├── backend/
│ ├── app/
│ │ ├── api/
│ │ │ ├── routes/ # HTTP endpoints
│ │ │ │ ├── auth.py # Login, register, sessions
│ │ │ │ ├── users.py # User CRUD
│ │ │ │ ├── groups.py # User group management
│ │ │ │ ├── grant_groups.py # Grant group management
│ │ │ │ ├── connections.py # Connection CRUD
│ │ │ │ └── servers.py # Server registration
│ │ │ └── dependencies.py # Auth dependencies
│ │ ├── db/
│ │ │ ├── models.py # SQLAlchemy models
│ │ │ ├── database.py # Session management
│ │ │ └── migrations/ # Alembic migrations
│ │ └── domain/
│ │ ├── auth/ # Authentication logic
│ │ └── connections/ # Credential encryption
│ └── tests/
│ ├── test_grant_group_routes.py
│ ├── test_grant_group_permissions.py
│ └── test_connections/
├── frontend/
│ └── src/
│ ├── api/client.ts # API client
│ ├── components/ # Shared components
│ ├── pages/admin/ # Admin UI
│ └── state/ # Zustand stores
└── docker-compose.yml

Permission Hierarchy

Hub implements a unidirectional permission hierarchy. Access flows in one direction only:

Users → User Groups → Grant Groups → Connections

Data Model

Database Tables

TablePurpose
usersUser accounts with roles
groupsUser groups for organizing users
user_groupsJunction: users ↔ groups
grant_groupsPermission bundles containing connections
user_group_grant_groupsJunction: groups ↔ grant groups
grant_group_connectionsJunction: grant groups ↔ connections

Why This Design?

Single audit path: To answer "why does user X have access to connection Y?", trace:

User → UserGroup membership → GrantGroup assignment → Connection assignment

No cycles possible: Grant Groups cannot reference other Grant Groups or Users directly. This prevents circular permission dependencies.

Reusable permission bundles: A single Grant Group (e.g., "Production Databases") can be assigned to multiple User Groups without duplicating connection assignments.

Clear separation of concerns:

  • User Groups = organizational structure (teams, departments)
  • Grant Groups = permission bundles (what resources)
  • Connections = actual resources

Access Control Implementation

Checking User Access to Connections

# hub/backend/app/api/routes/connections.py

async def list_connections(user: SessionInfo, db: AsyncSession):
# Admins/auditors see all
if user.role in ("owner", "admin", "auditor"):
return await store.list_connections()

# Users: trace through the hierarchy
# 1. Get user's groups
user_group_ids = await get_user_group_ids(db, user.user_id)

# 2. Get grant groups assigned to those user groups
grant_group_ids = await db.execute(
select(UserGroupGrantGroup.grant_group_id)
.where(UserGroupGrantGroup.user_group_id.in_(user_group_ids))
)

# 3. Get connections in those grant groups
accessible_ids = await db.execute(
select(GrantGroupConnection.connection_id)
.where(GrantGroupConnection.grant_group_id.in_(grant_group_ids))
)

return [c for c in connections if c.id in accessible_ids]

Route Dependencies

# Require specific roles
from app.api.dependencies import RequireAdmin, RequireAuditor

@router.post("/grant-groups")
async def create_grant_group(
request: GrantGroupCreate,
_admin: SessionInfo = RequireAdmin, # 403 if not admin
db: AsyncSession = Depends(get_db),
):
...

@router.get("/grant-groups")
async def list_grant_groups(
_user: SessionInfo = RequireAuditor, # Admin or auditor
db: AsyncSession = Depends(get_db),
):
...

API Endpoints

Grant Groups

MethodEndpointRoleDescription
GET/grant-groupsAuditor+List all grant groups
POST/grant-groupsAdminCreate grant group
GET/grant-groups/{id}Auditor+Get grant group with connections and user groups
PATCH/grant-groups/{id}AdminUpdate grant group
DELETE/grant-groups/{id}AdminDelete grant group
POST/grant-groups/{id}/connections/{conn_id}AdminAdd connection to grant group
DELETE/grant-groups/{id}/connections/{conn_id}AdminRemove connection from grant group
GET/grant-groups/{id}/user-groupsAuditor+List user groups with this grant group (read-only)

User Groups (Grant Group Assignment)

MethodEndpointRoleDescription
GET/groups/{id}/grant-groupsAuditor+List grant groups assigned to user group
POST/groups/{id}/grant-groups/{gg_id}AdminAssign grant group to user group
DELETE/groups/{id}/grant-groups/{gg_id}AdminRemove grant group from user group

Design Decision: Unidirectional Assignment

Grant groups are assigned from the User Groups side, not from the Grant Groups side. This enforces the parent → child hierarchy:

✓ POST /groups/{group_id}/grant-groups/{grant_group_id}   # Correct
✗ POST /grant-groups/{grant_group_id}/user-groups/{group_id} # Not implemented

The Grant Groups tab shows which user groups reference it (read-only). To modify assignments, use the User Groups tab.

Testing

Test Structure

hub/backend/tests/
├── test_grant_group_routes.py # CRUD and assignment operations
├── test_grant_group_permissions.py # Access control verification
└── test_connections/
├── test_user_connections.py # User role access tests
├── test_admin_connections.py # Admin role tests
└── test_auditor_connections.py # Auditor role tests

Key Test Patterns

# Test that users can only see connections via grant groups
async def test_user_can_list_connections_via_grant_group(client, db):
# Setup: user → user_group → grant_group → connection
user = await create_user(db, role="user")
group = await create_group(db)
await add_user_to_group(db, user.id, group.id)

grant_group = await create_grant_group(db)
await assign_grant_group_to_group(db, group.id, grant_group.id)

connection = await create_connection(db)
await add_connection_to_grant_group(db, grant_group.id, connection.id)

# Test: user can see the connection
response = await client.get("/connections", headers=auth_headers(user))
assert response.status_code == 200
assert connection.id in [c["id"] for c in response.json()["connections"]]

Running Tests

cd hub/backend

# All grant group tests
uv run pytest tests/test_grant_group_routes.py tests/test_grant_group_permissions.py -v

# Connection access tests
uv run pytest tests/test_connections/ -v

# Full test suite
uv run pytest -v

Frontend Implementation

State Management

The frontend uses Zustand stores with separate concerns:

// src/state/authStore.ts - Authentication state
// src/state/settingsStore.ts - UI preferences

API Client

// src/api/client.ts

// Grant Groups
export async function listGrantGroups(): Promise<GrantGroupListResponse>;
export async function createGrantGroup(data: GrantGroupCreate): Promise<GrantGroupSummary>;
export async function getGrantGroup(id: string): Promise<GrantGroupDetail>;
export async function addConnectionToGrantGroup(grantGroupId: string, connectionId: string): Promise<void>;

// User Group → Grant Group assignment
export async function listGroupGrantGroups(groupId: string): Promise<GroupGrantGroupsResponse>;
export async function assignGrantGroupToGroup(groupId: string, grantGroupId: string): Promise<void>;
export async function removeGrantGroupFromGroup(groupId: string, grantGroupId: string): Promise<void>;

UI Components

ComponentLocationPurpose
GroupsTabpages/admin/GroupsTab.tsxManage user groups, assign grant groups
GrantGroupsTabpages/admin/GrantGroupsTab.tsxManage grant groups, assign connections
ConnectionsTabpages/admin/ConnectionsTab.tsxManage connections

The sidebar follows the hierarchy: Users → User Groups → Grant Groups → Connections