Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/authsome/server/routes/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from authsome.server.analytics import capture_event
from authsome.server.credential_service import CredentialService
from authsome.server.routes._deps import (
build_auth_service,
get_daemon_or_browser_auth_service,
get_protected_auth_service,
get_vault_registry,
Expand All @@ -32,6 +33,13 @@ async def _connection_detail(
) -> ConnectionDetailResponse:
definition = await auth.get_provider(provider)
record = await auth.get_connection(provider, connection)
global_pointer = await auth.global_connections.get(provider)
is_global = (
global_pointer is not None
and global_pointer.owner_principal_id == record.principal_id
and global_pointer.owner_vault_id == record.vault_id
and global_pointer.connection_name == record.connection_name
)
return ConnectionDetailResponse(
provider=record.provider,
provider_display_name=definition.display_name,
Expand All @@ -56,15 +64,41 @@ async def _connection_detail(
),
can_set_default=can_set_default,
can_set_global=can_set_global,
is_global=is_global,
)


async def _provider_connection_counts(request: Request, auth: CredentialService) -> dict[str, int]:
if auth.principal_role != PrincipalRole.ADMIN:
return {}

counts: dict[str, int] = {}
for principal in await request.app.state.store.principals.list_all():
resolved = await request.app.state.ownership_resolver.resolve_for_principal(principal_id=principal.principal_id)
if resolved is None:
continue
principal_auth = build_auth_service(
request,
identity=None,
principal_id=principal.principal_id,
principal_role=principal.role,
vault_id=resolved.vault_id,
)
for group in await principal_auth.list_connections():
counts[group["name"]] = counts.get(group["name"], 0) + len(group["connections"])
return counts


@router.get("/connections")
async def list_connections(auth: CredentialService = Depends(get_daemon_or_browser_auth_service)):
async def list_connections(
request: Request,
auth: CredentialService = Depends(get_daemon_or_browser_auth_service),
):
by_source = await auth.list_providers_by_source()
return {
"connections": await auth.list_connections(),
"global_connections": [row.model_dump(mode="json") for row in await auth.list_global_connection_summaries()],
"provider_connection_counts": await _provider_connection_counts(request, auth),
"by_source": {
source: [provider.model_dump(mode="json") for provider in providers]
for source, providers in by_source.items()
Expand Down
1 change: 1 addition & 0 deletions src/authsome/server/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,4 @@ class ConnectionDetailResponse(BaseModel):
secrets: ConnectionSecretsResponse = Field(default_factory=ConnectionSecretsResponse)
can_set_default: bool = False
can_set_global: bool = False
is_global: bool = False
30 changes: 30 additions & 0 deletions tests/server/test_connection_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,36 @@ def test_admin_can_make_own_connection_global_from_detail(monkeypatch, tmp_path:
assert response.json()["can_set_global"] is True


def test_connection_detail_marks_current_global_connection(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path))
with create_server_test_client() as client:
admin = _claim_identity(client, tmp_path, "admin-ready-boldly-0001", email="admin@example.com")
admin_ctx = asyncio.run(client.app.state.ownership_resolver.resolve(identity=admin.handle))
_put_connection(client, admin_ctx.principal_id, admin_ctx.vault_id, "secondary")
_put_connection(client, admin_ctx.principal_id, admin_ctx.vault_id, "default")
assert (
client.post(
"/api/connections/github/default/global",
headers=_auth_header(tmp_path, "POST", "/api/connections/github/default/global", handle=admin.handle),
).status_code
== status.HTTP_200_OK
)

global_response = client.get(
"/api/connections/github/default/detail",
headers=_auth_header(tmp_path, "GET", "/api/connections/github/default/detail", handle=admin.handle),
)
secondary_response = client.get(
"/api/connections/github/secondary/detail",
headers=_auth_header(tmp_path, "GET", "/api/connections/github/secondary/detail", handle=admin.handle),
)

assert global_response.status_code == status.HTTP_200_OK
assert global_response.json()["is_global"] is True
assert secondary_response.status_code == status.HTTP_200_OK
assert secondary_response.json()["is_global"] is False


def test_admin_logout_targets_other_principal_connection(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path))
with create_server_test_client() as client:
Expand Down
26 changes: 26 additions & 0 deletions tests/server/test_global_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,32 @@ def test_admin_can_make_connection_global_and_user_sees_redacted_summary(monkeyp
assert raw_response.status_code == status.HTTP_404_NOT_FOUND


def test_admin_connections_include_provider_counts_across_principals(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path))
with create_server_test_client() as client:
admin = _claim_identity(client, tmp_path, "admin-ready-boldly-0001", email="admin@example.com")
_claim_identity(client, tmp_path, "steady-wisely-boldly-0042", email="user@example.com")
_claim_identity(client, tmp_path, "calmly-simply-boldly-0043", email="second@example.com")
user_ctx = asyncio.run(client.app.state.ownership_resolver.resolve(identity="steady-wisely-boldly-0042"))
second_ctx = asyncio.run(client.app.state.ownership_resolver.resolve(identity="calmly-simply-boldly-0043"))
_put_connection(client, user_ctx.principal_id, user_ctx.vault_id, "default")
_put_connection(client, second_ctx.principal_id, second_ctx.vault_id, "team")

admin_response = client.get(
"/api/connections",
headers=_auth_header(tmp_path, "GET", "/api/connections", handle=admin.handle),
)
user_response = client.get(
"/api/connections",
headers=_auth_header(tmp_path, "GET", "/api/connections", handle="steady-wisely-boldly-0042"),
)

assert admin_response.status_code == status.HTTP_200_OK
assert admin_response.json()["provider_connection_counts"] == {"github": 2}
assert user_response.status_code == status.HTTP_200_OK
assert user_response.json()["provider_connection_counts"] == {}


def test_global_connection_resolves_for_explicit_default_and_proxy_routes(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path))
with create_server_test_client() as client:
Expand Down
2 changes: 0 additions & 2 deletions ui/src/app/(authenticated)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ function pathToView(pathname: string): View {
connections: "connections",
agents: "agents",
principal: "principals",
vault: "vault",
audit: "audit",
settings: "settings",
};
Expand All @@ -57,7 +56,6 @@ function buildBreadcrumbs(
connections: "Connections",
agents: "Agents",
principal: "Principals",
vault: "Vault",
audit: "Audit Log",
settings: "Settings",
};
Expand Down
12 changes: 0 additions & 12 deletions ui/src/app/(authenticated)/vault/page.tsx

This file was deleted.

8 changes: 8 additions & 0 deletions ui/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,12 @@
textarea {
letter-spacing: 0;
}
button:not(:disabled),
[role="button"]:not([aria-disabled="true"]) {
cursor: pointer;
}
button:disabled,
[role="button"][aria-disabled="true"] {
cursor: not-allowed;
}
}
27 changes: 27 additions & 0 deletions ui/src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Link from "next/link";
import { ArrowLeft, CircleAlert } from "lucide-react";

import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

export default function NotFound() {
return (
<main className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
<Card className="w-full max-w-md border-border/70 shadow-none">
<CardHeader>
<div className="mb-3 flex size-10 items-center justify-center rounded-lg border border-amber-800 bg-amber-950/40 text-amber-400">
<CircleAlert className="size-5" />
</div>
<CardTitle>Page not found</CardTitle>
<CardDescription>The dashboard route is missing or no longer available.</CardDescription>
</CardHeader>
<CardContent>
<Link className={buttonVariants()} href="/">
<ArrowLeft />
Back to dashboard
</Link>
</CardContent>
</Card>
</main>
);
}
Loading
Loading