Skip to content

Commit c9614e3

Browse files
authored
feat(server): add created_by to provider entity (#1265)
Signed-off-by: Radek Ježek <radek.jezek@ibm.com>
1 parent 4505ef8 commit c9614e3

17 files changed

Lines changed: 260 additions & 80 deletions

File tree

apps/beeai-sdk/src/beeai_sdk/platform/provider.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import typing
66
from contextlib import asynccontextmanager
77
from datetime import timedelta
8+
from uuid import UUID
89

910
import pydantic
1011
from a2a.client import ClientConfig, ClientFactory
@@ -35,6 +36,7 @@ class Provider(pydantic.BaseModel):
3536
agent_card: AgentCard
3637
state: typing.Literal["missing", "starting", "ready", "running", "error"] = "missing"
3738
last_error: ProviderErrorMessage | None = None
39+
created_by: UUID
3840
missing_configuration: list[EnvVar] = pydantic.Field(default_factory=list)
3941

4042
@staticmethod

apps/beeai-server/src/beeai_server/api/auth/auth.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,36 @@
5454
),
5555
}
5656
ROLE_PERMISSIONS[UserRole.DEVELOPER] = ROLE_PERMISSIONS[UserRole.USER] | Permissions(
57-
providers={"read", "write"}, # TODO provider ownership
57+
providers={"read", "write"},
5858
provider_builds={"read", "write"},
5959
provider_variables={"read", "write"},
6060
mcp_providers={"read", "write"},
6161
)
6262

63+
"""
64+
global entities:
65+
- model_providers
66+
- system_configuration
67+
- mcp_providers
68+
- mcp_tools
69+
private entities (scoped to user):
70+
- files
71+
- vector_stores
72+
- variables
73+
- contexts
74+
- context_data
75+
- feedback
76+
semi-private entities:
77+
- providers
78+
- any user list and show detail about any provider
79+
- developers can create/delete and manage only their own providers
80+
- admins can create/delete and manage any provider
81+
- provider_builds
82+
- any user list and show detail about any build
83+
- developers can create/delete and manage only their own builds
84+
- admins can create/delete and manage any build
85+
"""
86+
6387

6488
class ParsedToken(BaseModel):
6589
global_permissions: Permissions

apps/beeai-server/src/beeai_server/api/routes/provider_builds.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
from uuid import UUID
66

77
import fastapi
8-
from fastapi import Depends
8+
from fastapi import Depends, Query
99
from starlette.responses import StreamingResponse
1010

1111
from beeai_server.api.dependencies import (
1212
ProviderBuildServiceDependency,
1313
RequiresPermissions,
1414
)
15-
from beeai_server.api.schema.provider_build import CreateProviderBuildRequest
15+
from beeai_server.api.schema.provider_build import CreateProviderBuildRequest, ProviderBuildListQuery
1616
from beeai_server.configuration import get_configuration
1717
from beeai_server.domain.models.common import PaginatedResult
1818
from beeai_server.domain.models.permissions import AuthorizedUser
@@ -40,25 +40,32 @@ async def get_provider_build(
4040
return await provider_build_service.get_build(provider_build_id=id)
4141

4242
@router.get("")
43-
async def list_provider_build(
44-
_: Annotated[AuthorizedUser, Depends(RequiresPermissions(provider_builds={"read"}))],
43+
async def list_provider_builds(
44+
user: Annotated[AuthorizedUser, Depends(RequiresPermissions(provider_builds={"read"}))],
4545
provider_build_service: ProviderBuildServiceDependency,
46+
query: Annotated[ProviderBuildListQuery, Query()],
4647
) -> PaginatedResult[ProviderBuild]:
47-
return await provider_build_service.list_builds()
48+
return await provider_build_service.list_builds(
49+
pagination=query,
50+
status=query.status,
51+
user=user.user if query.user_owned else None,
52+
)
4853

4954
@router.get("/{id}/logs")
5055
async def stream_logs(
51-
_: Annotated[AuthorizedUser, Depends(RequiresPermissions(provider_builds={"write"}))],
56+
user: Annotated[AuthorizedUser, Depends(RequiresPermissions(provider_builds={"write"}))],
5257
id: UUID,
5358
provider_build_service: ProviderBuildServiceDependency,
5459
) -> StreamingResponse:
55-
logs_iterator = await provider_build_service.stream_logs(provider_build_id=id)
60+
# admin can see logs from all builds, other users only logs of their build
61+
logs_iterator = await provider_build_service.stream_logs(provider_build_id=id, user=user.user)
5662
return streaming_response(logs_iterator())
5763

5864
@router.delete("/{id}", status_code=fastapi.status.HTTP_204_NO_CONTENT)
5965
async def delete(
60-
_: Annotated[AuthorizedUser, Depends(RequiresPermissions(provider_builds={"write"}))],
66+
user: Annotated[AuthorizedUser, Depends(RequiresPermissions(provider_builds={"write"}))],
6167
id: UUID,
6268
provider_build_service: ProviderBuildServiceDependency,
6369
) -> None:
64-
await provider_build_service.delete_build(provider_build_id=id)
70+
# admin can delete all builds, other users only their build
71+
await provider_build_service.delete_build(provider_build_id=id, user=user.user)

apps/beeai-server/src/beeai_server/api/routes/providers.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
@router.post("")
2929
async def create_provider(
30-
_: Annotated[AuthorizedUser, Depends(RequiresPermissions(providers={"write"}))],
30+
user: Annotated[AuthorizedUser, Depends(RequiresPermissions(providers={"write"}))],
3131
request: CreateProviderRequest,
3232
provider_service: ProviderServiceDependency,
3333
configuration: ConfigurationDependency,
@@ -36,6 +36,7 @@ async def create_provider(
3636
if auto_remove and not configuration.provider.auto_remove_enabled:
3737
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Auto remove functionality is disabled")
3838
return await provider_service.create_provider(
39+
user=user.user,
3940
location=request.location,
4041
agent_card=request.agent_card,
4142
auto_remove=auto_remove,
@@ -47,7 +48,7 @@ async def create_provider(
4748
async def preview_provider(
4849
request: CreateProviderRequest,
4950
provider_service: ProviderServiceDependency,
50-
_: Annotated[AuthorizedUser, Depends(RequiresPermissions())],
51+
_: Annotated[AuthorizedUser, Depends(RequiresPermissions(providers={"write"}))],
5152
) -> ProviderWithState:
5253
return await provider_service.preview_provider(location=request.location, agent_card=request.agent_card)
5354

@@ -56,10 +57,11 @@ async def preview_provider(
5657
async def list_providers(
5758
provider_service: ProviderServiceDependency,
5859
request: Request,
59-
_: Annotated[AuthorizedUser, Depends(RequiresPermissions(providers={"read"}), use_cache=False)],
60+
user: Annotated[AuthorizedUser, Depends(RequiresPermissions(providers={"read"}), use_cache=False)],
61+
user_owned: Annotated[bool, Query()] = False,
6062
) -> PaginatedResult[ProviderWithState]:
6163
providers = []
62-
for provider in await provider_service.list_providers():
64+
for provider in await provider_service.list_providers(user=user.user if user_owned else None):
6365
new_provider = provider.model_copy(
6466
update={
6567
"agent_card": create_proxy_agent_card(provider.agent_card, provider_id=provider.id, request=request)
@@ -87,18 +89,20 @@ async def get_provider(
8789
async def delete_provider(
8890
id: UUID,
8991
provider_service: ProviderServiceDependency,
90-
_: Annotated[AuthorizedUser, Depends(RequiresPermissions(providers={"write"}))],
92+
user: Annotated[AuthorizedUser, Depends(RequiresPermissions(providers={"write"}))],
9193
) -> None:
92-
await provider_service.delete_provider(provider_id=id)
94+
# admin can delete any provider, other users only their providers
95+
await provider_service.delete_provider(provider_id=id, user=user.user)
9396

9497

9598
@router.get("/{id}/logs")
9699
async def stream_logs(
97-
_: Annotated[AuthorizedUser, Depends(RequiresPermissions(providers={"write"}))],
100+
user: Annotated[AuthorizedUser, Depends(RequiresPermissions(providers={"write"}))],
98101
id: UUID,
99102
provider_service: ProviderServiceDependency,
100103
) -> StreamingResponse:
101-
logs_iterator = await provider_service.stream_logs(provider_id=id)
104+
# admin can see logs from all providers, other users only logs of their provider
105+
logs_iterator = await provider_service.stream_logs(provider_id=id, user=user.user)
102106
return streaming_response(logs_iterator())
103107

104108

@@ -107,15 +111,17 @@ async def update_provider_variables(
107111
id: UUID,
108112
request: UpdateVariablesRequest,
109113
provider_service: ProviderServiceDependency,
110-
_: Annotated[AuthorizedUser, Depends(RequiresPermissions(provider_variables={"write"}))],
114+
user: Annotated[AuthorizedUser, Depends(RequiresPermissions(provider_variables={"write"}))],
111115
) -> None:
112-
await provider_service.update_provider_env(provider_id=id, env=request.variables)
116+
# admin can update all variables, other users only variables of their provider
117+
await provider_service.update_provider_env(provider_id=id, env=request.variables, user=user.user)
113118

114119

115120
@router.get("/{id}/variables")
116121
async def list_provider_variables(
117122
id: UUID,
118123
provider_service: ProviderServiceDependency,
119-
_: Annotated[AuthorizedUser, Depends(RequiresPermissions(provider_variables={"read"}))],
124+
user: Annotated[AuthorizedUser, Depends(RequiresPermissions(provider_variables={"read"}))],
120125
) -> ListVariablesSchema:
121-
return ListVariablesSchema(variables=await provider_service.list_provider_env(provider_id=id))
126+
# admin can see all variables, other users only variables of their provider
127+
return ListVariablesSchema(variables=await provider_service.list_provider_env(provider_id=id, user=user.user))

apps/beeai-server/src/beeai_server/api/schema/provider_build.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33

44
from pydantic import BaseModel
55

6+
from beeai_server.api.schema.common import PaginationQuery
7+
from beeai_server.domain.models.provider_build import BuildState
68
from beeai_server.utils.github import GithubUrl
79

810

911
class CreateProviderBuildRequest(BaseModel):
1012
location: GithubUrl
13+
14+
15+
class ProviderBuildListQuery(PaginationQuery):
16+
status: BuildState | None = None
17+
user_owned: bool = False

apps/beeai-server/src/beeai_server/domain/models/provider.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class Provider(BaseModel):
113113
registry: RegistryLocation | None = None
114114
auto_remove: bool = False
115115
created_at: AwareDatetime = Field(default_factory=utc_now)
116+
created_by: UUID
116117
last_active_at: AwareDatetime = Field(default_factory=utc_now)
117118
agent_card: AgentCard
118119

apps/beeai-server/src/beeai_server/domain/repositories/provider.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010

1111
@runtime_checkable
1212
class IProviderRepository(Protocol):
13-
async def list(self, *, auto_remove_filter: bool | None = None) -> AsyncIterator[Provider]:
13+
async def list(
14+
self, *, auto_remove_filter: bool | None = None, user_id: UUID | None = None
15+
) -> AsyncIterator[Provider]:
1416
yield ... # type: ignore
1517

1618
async def create(self, *, provider: Provider) -> None: ...
1719
async def update(self, *, provider: Provider) -> None: ...
1820

19-
async def get(self, *, provider_id: UUID) -> Provider: ...
20-
async def delete(self, *, provider_id: UUID) -> int: ...
21+
async def get(self, *, provider_id: UUID, user_id: UUID | None = None) -> Provider: ...
22+
async def delete(self, *, provider_id: UUID, user_id: UUID | None = None) -> int: ...
2123
async def update_last_accessed(self, *, provider_id: UUID) -> None: ...

apps/beeai-server/src/beeai_server/domain/repositories/provider_build.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
@runtime_checkable
1313
class IProviderBuildRepository(Protocol):
14-
async def list(self, *, status: BuildState | None = None) -> AsyncIterator[ProviderBuild]:
14+
async def list(
15+
self, *, status: BuildState | None = None, user_id: UUID | None = None
16+
) -> AsyncIterator[ProviderBuild]:
1517
yield ... # type: ignore
1618

1719
async def list_paginated(
@@ -22,9 +24,10 @@ async def list_paginated(
2224
order: str = "desc",
2325
order_by: str = "created_at",
2426
status: BuildState | None = None,
27+
user_id: UUID | None = None,
2528
) -> PaginatedResult[ProviderBuild]: ...
2629

2730
async def create(self, *, provider_build: ProviderBuild) -> None: ...
2831
async def update(self, *, provider_build: ProviderBuild) -> None: ...
29-
async def get(self, *, provider_build_id: UUID) -> ProviderBuild: ...
30-
async def delete(self, *, provider_build_id: UUID) -> int: ...
32+
async def get(self, *, provider_build_id: UUID, user_id: UUID | None = None) -> ProviderBuild: ...
33+
async def delete(self, *, provider_build_id: UUID, user_id: UUID | None = None) -> int: ...
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""add user ownership to providers
5+
6+
Revision ID: 73e2d8596ada
7+
Revises: 613a5534625e
8+
Create Date: 2025-10-01 13:12:33.873906
9+
10+
"""
11+
12+
from collections.abc import Sequence
13+
14+
import sqlalchemy as sa
15+
from alembic import op
16+
17+
# revision identifiers, used by Alembic.
18+
revision: str = "73e2d8596ada"
19+
down_revision: str | None = "613a5534625e"
20+
branch_labels: str | Sequence[str] | None = None
21+
depends_on: str | Sequence[str] | None = None
22+
23+
24+
def upgrade() -> None:
25+
"""Upgrade schema."""
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.add_column("providers", sa.Column("created_by", sa.UUID(), nullable=True))
28+
# Assign existing providers to the admin user
29+
op.execute("UPDATE providers SET created_by = (SELECT id FROM users WHERE email = 'admin@beeai.dev')")
30+
op.alter_column("providers", "created_by", nullable=False)
31+
op.create_foreign_key(None, "providers", "users", ["created_by"], ["id"], ondelete="CASCADE")
32+
# ### end Alembic commands ###
33+
34+
35+
def downgrade() -> None:
36+
"""Downgrade schema."""
37+
# ### commands auto generated by Alembic - please adjust! ###
38+
op.drop_constraint("providers_created_by_fkey", "providers", type_="foreignkey")
39+
op.drop_column("providers", "created_by")
40+
# ### end Alembic commands ###

apps/beeai-server/src/beeai_server/infrastructure/persistence/repositories/provider.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import Any
77
from uuid import UUID
88

9-
from sqlalchemy import JSON, Boolean, Column, DateTime, Integer, Row, String, Table
9+
from sqlalchemy import JSON, Boolean, Column, DateTime, ForeignKey, Integer, Row, String, Table
1010
from sqlalchemy import UUID as SQL_UUID
1111
from sqlalchemy.exc import IntegrityError
1212
from sqlalchemy.ext.asyncio import AsyncConnection
@@ -27,6 +27,7 @@
2727
Column("auto_stop_timeout_sec", Integer, nullable=True),
2828
Column("auto_remove", Boolean, default=False, nullable=False),
2929
Column("created_at", DateTime(timezone=True), nullable=False),
30+
Column("created_by", ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
3031
Column("last_active_at", DateTime(timezone=True), nullable=False),
3132
Column("agent_card", JSON, nullable=False),
3233
)
@@ -60,6 +61,7 @@ def _to_row(self, provider: Provider) -> dict[str, Any]:
6061
"auto_remove": provider.auto_remove,
6162
"agent_card": provider.agent_card.model_dump(mode="json"),
6263
"created_at": provider.created_at,
64+
"created_by": provider.created_by,
6365
"last_active_at": provider.last_active_at,
6466
}
6567

@@ -75,12 +77,15 @@ def _to_provider(self, row: Row) -> Provider:
7577
"auto_remove": row.auto_remove,
7678
"last_active_at": row.last_active_at,
7779
"created_at": row.created_at,
80+
"created_by": row.created_by,
7881
"agent_card": row.agent_card,
7982
}
8083
)
8184

82-
async def get(self, *, provider_id: UUID) -> Provider:
85+
async def get(self, *, provider_id: UUID, user_id: UUID | None = None) -> Provider:
8386
query = select(providers_table).where(providers_table.c.id == provider_id)
87+
if user_id is not None:
88+
query = query.where(providers_table.c.created_by == user_id)
8489
result = await self.connection.execute(query)
8590
if not (row := result.fetchone()):
8691
raise EntityNotFoundError(entity="provider", id=provider_id)
@@ -91,15 +96,21 @@ async def update_last_accessed(self, *, provider_id: UUID) -> None:
9196
query = providers_table.update().where(providers_table.c.id == provider_id).values(last_active_at=utc_now())
9297
await self.connection.execute(query)
9398

94-
async def delete(self, *, provider_id: UUID) -> int:
99+
async def delete(self, *, provider_id: UUID, user_id: UUID | None = None) -> int:
95100
query = delete(providers_table).where(providers_table.c.id == provider_id)
101+
if user_id is not None:
102+
query = query.where(providers_table.c.created_by == user_id)
96103
result = await self.connection.execute(query)
97104
if not result.rowcount:
98105
raise EntityNotFoundError(entity="provider", id=provider_id)
99106
return result.rowcount
100107

101-
async def list(self, *, auto_remove_filter: bool | None = None) -> AsyncIterator[Provider]:
108+
async def list(
109+
self, *, auto_remove_filter: bool | None = None, user_id: UUID | None = None
110+
) -> AsyncIterator[Provider]:
102111
query = providers_table.select()
112+
if user_id is not None:
113+
query = query.where(providers_table.c.created_by == user_id)
103114
if auto_remove_filter is not None:
104115
query = query.where(providers_table.c.auto_remove == auto_remove_filter)
105116
async for row in await self.connection.stream(query):

0 commit comments

Comments
 (0)