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
8 changes: 4 additions & 4 deletions crates/utopia-server/src/api/graph_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ pub struct OverviewQuery {

pub async fn overview(
State(state): State<AppState>,
AuthUser(user): AuthUser,
reader: SessionOrToken,
Path(kb_id): Path<Uuid>,
Query(q): Query<OverviewQuery>,
) -> ApiResult<Json<serde_json::Value>> {
require_kb(&state, &user, kb_id, Role::Viewer).await?;
require_reader_kb(&state, &reader, kb_id).await?;
let at = parse_instant("at", q.at.as_deref())?;
let as_of = parse_instant("as_of", q.as_of.as_deref())?;
// 画多少个是渲染的事,库里有多少是知识库的事——两个数都回,界面才说得出
Expand Down Expand Up @@ -105,11 +105,11 @@ pub struct NeighborhoodQuery {

pub async fn neighborhood(
State(state): State<AppState>,
AuthUser(user): AuthUser,
reader: SessionOrToken,
Path(kb_id): Path<Uuid>,
Query(q): Query<NeighborhoodQuery>,
) -> ApiResult<Json<serde_json::Value>> {
require_kb(&state, &user, kb_id, Role::Viewer).await?;
require_reader_kb(&state, &reader, kb_id).await?;
let at = parse_instant("at", q.at.as_deref())?;
let as_of = parse_instant("as_of", q.as_of.as_deref())?;
let (nodes, edges) = utopia_store::graph::neighborhood(
Expand Down
11 changes: 6 additions & 5 deletions crates/utopia-server/src/api/token_read_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! 个人令牌读实体(0014 决定 7):搜实体、实体详情、认知历史、事实证据四条读路由
//! 个人令牌读图(0014 决定 7):搜实体、实体详情、认知历史、事实证据、图总览、邻域六条读路由
//! 收任何一枚活着的令牌。角色照旧是上限,`kb_ids` 排除了的库答 404;改实体仍只认会话。
use crate::state::AppState;
use axum::body::Body;
Expand Down Expand Up @@ -81,13 +81,15 @@ impl Fixture {
.1
}

fn read_paths(&self) -> [String; 4] {
fn read_paths(&self) -> [String; 6] {
let (kb, entity, fact) = (self.kb, self.entity, self.fact);
[
format!("/kbs/{kb}/entities?q=Zhang"),
format!("/kbs/{kb}/entities/{entity}"),
format!("/kbs/{kb}/entities/{entity}/history"),
format!("/kbs/{kb}/facts/{fact}/evidence"),
format!("/kbs/{kb}/graph/overview"),
format!("/kbs/{kb}/graph/neighborhood?entity={entity}"),
]
}

Expand All @@ -109,8 +111,7 @@ impl Fixture {
}

#[tokio::test]
async fn a_read_token_of_a_base_viewer_reads_its_entities_facts_and_history() -> anyhow::Result<()>
{
async fn a_read_token_of_a_base_viewer_gets_200_on_every_graph_read() -> anyhow::Result<()> {
let Some(f) = Fixture::new().await? else {
return Ok(());
};
Expand All @@ -128,7 +129,7 @@ async fn a_read_token_of_a_base_viewer_reads_its_entities_facts_and_history() ->
}

#[tokio::test]
async fn a_token_limited_to_another_base_gets_404_on_every_entity_read() -> anyhow::Result<()> {
async fn a_token_limited_to_another_base_gets_404_on_every_graph_read() -> anyhow::Result<()> {
let Some(f) = Fixture::new().await? else {
return Ok(());
};
Expand Down
3 changes: 2 additions & 1 deletion crates/utopia-server/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ impl FromRequestParts<AppState> for AuthUser {
///
/// **不替换 `AuthUser`。** `AuthUser` 把每个 Bearer 都当 JWT 解,令牌在那几百条
/// 路由上一律是 401——那是有意的:哪条路由准令牌进来,要在 0014 里说出理由。
/// 用这个提取器的只有文档原件(0052)、「把一个库搭起来」那几条和读实体那几条(决定 7)。
/// 用这个提取器的只有文档原件(0052)、「把一个库搭起来」那几条和读图那几条:搜实体、
/// 实体详情、认知历史、事实证据、图总览、邻域(决定 7)。
///
/// 令牌只收窄:`pat` 在的时候,路由先问 [`Self::require_write`] 或
/// [`Self::reaches`],角色照旧由 `require_kb` 与工作区角色判。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,18 @@ push documents in.

7. **A token reads the graph** (added 2026-10-16, #1262). A service that mirrors into a base reads the
base back to check what landed, and a session JWT is the wrong credential for a backend
(decision 6). Four reads now also take any live token, acting as its owner: searching a base's
(decision 6). Six reads now also take any live token, acting as its owner: searching a base's
entities (`GET /kbs/{id}/entities`), an entity's detail (`GET /kbs/{id}/entities/{eid}`), its
record-axis history (`…/history`), and a fact's evidence (`GET /kbs/{id}/facts/{fid}/evidence`).
Search finds the entity by name; the detail gives its facts, names and derivations; the
history says when we changed our mind; the evidence ties a fact to its sentence. Read is the
lowest scope, so any token qualifies; the owner's role is still the ceiling and Viewer
suffices; a token whose `kb_ids` exclude the base gets 404, as for document content (0052).
Changing an entity on the same path (`PATCH`) still takes a session.
record-axis history (`…/history`), a fact's evidence (`GET /kbs/{id}/facts/{fid}/evidence`),
the graph overview (`GET /kbs/{id}/graph/overview`) and an entity's neighbourhood
(`GET /kbs/{id}/graph/neighborhood`). Search finds the entity by name; the detail gives its
facts, names and derivations; the history says when we changed our mind; the evidence ties a
fact to its sentence; the overview gives the most connected entities with the base's totals,
so a client sees the shape of what it pushed; the neighbourhood gives the entities around one,
so it can follow what a statement touches, and one started from another base's entity comes
back empty. Read is the lowest scope, so any token qualifies; the owner's role is still the
ceiling and Viewer suffices; a token whose `kb_ids` exclude the base gets 404, as for document
content (0052). Changing an entity on the same path (`PATCH`) still takes a session.

## Dead ends

Expand All @@ -121,7 +125,8 @@ push documents in.
push-only as well; a backend mirroring several kinds of documents pushes them through one. A
token now creates `api` sources too.
- 2026-10-16: decision 7 opens four graph reads to any token; decision 6's "only these routes
and document content" now also counts them.
and document content" now also counts them. 2026-10-19: the graph overview and neighbourhood
join them.

## Open questions

Expand Down
6 changes: 3 additions & 3 deletions docs/design/access-and-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ writes an audit row `mcp.tool_called`; the tokens page shows the plaintext once
snippet [0014]. A `write` token also sets up a base as its owner: creating a base and a `statements` or `api` source,
reading the source's push key, importing an ontology and deciding a kind word, with the owner's
role unchanged as the ceiling; any token lists a workspace's bases; a token limited by `kb_ids`
creates no base. These routes, document content and four graph reads (entity search, detail,
history, fact evidence; Viewer suffices, `kb_ids` answers 404) take `SessionOrToken`; apart from
MCP, every other route is session-only [0014 d6, d7]. `ingest_token` on a source is a different kind: plaintext, push-only [0014].
creates no base. These routes, document content and six graph reads (entity search, detail,
history, fact evidence, overview, neighbourhood; Viewer suffices, `kb_ids` answers 404) take
`SessionOrToken`; apart from MCP, every other route is session-only [0014 d6, d7]. `ingest_token` on a source is a different kind: plaintext, push-only [0014].

**Another app can trade its user's fresh ID token for a session.** `POST /auth/oidc/exchange`
accepts an ID token from the configured issuer whose audience is in
Expand Down
Loading