Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ opentelemetry-semantic-conventions = { version = "0.32.1", default-features = fa
opentelemetry_sdk = { version = "0.32.1", default-features = false }
owo-colors = { version = "4.2.3", default-features = false }
oxc = { version = "0.95.0", default-features = false }
percent-encoding = { version = "2.3.2", default-features = false }
pin-project = { version = "1.1.10", default-features = false }
pin-project-lite = { version = "0.2.16", default-features = false }
plist = { version = "1.8.0", default-features = false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ impl From<DraftId> for Uuid {
}
}

/// An entity's web ID and UUID joined by `~`, followed by `~` and a draft ID when it names a draft.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct EntityId {
pub web_id: WebId,
Expand Down
24 changes: 24 additions & 0 deletions libs/@blockprotocol/type-system/rust/src/ontology/id/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
//! );
//! ```

#[cfg(feature = "schemars")]
use alloc::borrow::Cow;
#[cfg(feature = "postgres")]
use core::error::Error;
use core::{
Expand All @@ -47,6 +49,8 @@ pub use error::{
use error_stack::Report;
#[cfg(feature = "postgres")]
use postgres_types::{FromSql, IsNull, ToSql, Type};
#[cfg(feature = "schemars")]
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use url::Url;
#[cfg(feature = "utoipa")]
Expand Down Expand Up @@ -775,6 +779,26 @@ impl ToSchema<'_> for VersionedUrl {
}
}

#[cfg(feature = "schemars")]
impl JsonSchema for VersionedUrl {
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("VersionedUrl")
}

fn json_schema(_: &mut SchemaGenerator) -> Schema {
json_schema!({
"type": "string",
"title": "Versioned URL",
"description": "The versioned URL of a SemType ontology type (the $id of the \
schema). It must be of the form `${baseUrl}v/${version}`, where \
`version` is a major version number.",
"format": "uri",
"maxLength": 2048,
"examples": ["https://hash.ai/@h/types/entity-type/user/v/1"],
})
}
}

/// An identifier for an ontology type record consisting of a base URL and version.
///
/// This type provides a structured representation of an ontology type identifier
Expand Down
2 changes: 2 additions & 0 deletions libs/@local/graph/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ include_dir = { workspace = true }
indexmap = { workspace = true }
md-5 = { workspace = true }
mime = { workspace = true }
percent-encoding = { workspace = true, features = ["alloc"] }
reqwest = { workspace = true }
scalar_api_reference = { workspace = true }
schemars = { workspace = true, features = ["uuid1"] }
Expand All @@ -92,6 +93,7 @@ hash-middleware = { workspace = true, features = ["test-utils"] }
insta = { workspace = true }
rstest = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "test-util"] }
tokio-postgres = { workspace = true }
tower = { workspace = true, features = ["util"] }
uuid = { workspace = true, features = ["v4"] }

Expand Down
42 changes: 27 additions & 15 deletions libs/@local/graph/api/src/rest/caller.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
use aide::{
axum::{ApiRouter, routing::get_with},
transform::TransformOperation,
};
use aide::transform::TransformOperation;
use schemars::JsonSchema;
use serde::Serialize;
use type_system::principal::actor::ActorId;

use super::{
credentials::{Actor, Credentials, MaybeActor},
credentials::{Actor, AdmitsAnonymous, Credentials, MaybeActor},
extract::Json,
routing::{self, Route, Routes},
};

/// The resolved caller.
Expand Down Expand Up @@ -51,18 +49,32 @@ fn document_authenticated_caller(operation: TransformOperation<'_>) -> Transform
.summary("Get the authenticated caller")
}

pub(super) fn routes<C: Credentials>() -> ApiRouter {
ApiRouter::new()
.api_route(
fn authenticated_caller_route<C: Credentials, T: Clone + Send + Sync + 'static>() -> Route<T> {
routing::get(
authenticated_caller::<Actor<C>>,
document_authenticated_caller,
)
}

pub(super) fn routes<C: AdmitsAnonymous, T: Clone + Send + Sync + 'static>() -> Routes<T> {
Routes::new()
.route(
"/caller",
get_with(caller::<MaybeActor<C>>, document_caller),
routing::get(caller::<MaybeActor<C>>, document_caller),
)
.api_route(
.route(
"/authenticated-caller",
authenticated_caller_route::<C, T>(),
)
.tag("Caller")
}

pub(super) fn authenticated_routes<C: Credentials, T: Clone + Send + Sync + 'static>() -> Routes<T>
{
Routes::new()
.route(
"/authenticated-caller",
get_with(
authenticated_caller::<Actor<C>>,
document_authenticated_caller,
),
authenticated_caller_route::<C, T>(),
)
.with_path_items(|path| path.tag("Caller"))
.tag("Caller")
}
17 changes: 15 additions & 2 deletions libs/@local/graph/api/src/rest/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ impl<C: Credentials> OperationInput for Actor<C> {
}
}

/// Credentials of an API that serves a request without a credential anonymously.
///
/// Only such an API reads its caller through [`MaybeActor`], which documents anonymous access that
/// [`Audience::Public`] refuses before a handler runs.
pub(super) trait AdmitsAnonymous: Credentials {}

pub(super) struct MaybeActor<C>(Option<ActorId>, PhantomData<fn() -> C>);

impl<C> From<MaybeActor<C>> for Option<ActorId> {
Expand All @@ -85,7 +91,7 @@ impl<C> From<MaybeActor<C>> for Option<ActorId> {
}
}

impl<S: Sync, C> FromRequestParts<S> for MaybeActor<C> {
impl<S: Sync, C: AdmitsAnonymous> FromRequestParts<S> for MaybeActor<C> {
type Rejection = AuthenticationRejection;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
Expand All @@ -95,8 +101,15 @@ impl<S: Sync, C> FromRequestParts<S> for MaybeActor<C> {
}
}

impl<C: Credentials> OperationInput for MaybeActor<C> {
impl<C: AdmitsAnonymous> OperationInput for MaybeActor<C> {
fn operation_input(ctx: &mut GenContext, operation: &mut Operation) {
const {
assert!(
matches!(C::AUDIENCE, Audience::Internal),
"`AdmitsAnonymous` should only be implemented for credentials of \
`Audience::Internal`"
);
}
let _: TransformOperation<'_> =
TransformOperation::new(operation).security_requirement_multi([]);
C::operation_input(ctx, operation);
Expand Down
1 change: 0 additions & 1 deletion libs/@local/graph/api/src/rest/extract/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@ impl<T: JsonSchema, const STATUS: u16> OperationOutput for Json<T, STATUS> {
) -> Option<openapi::Response> {
// Documents the answer to a body that fails to serialize on the operation itself.
Rejection::<JsonResponseProblem>::inferred_responses(ctx, operation);
MessagePart::ResponseBody.mark(operation);
axum::Json::<T>::operation_response(ctx, operation)
}

Expand Down
17 changes: 10 additions & 7 deletions libs/@local/graph/api/src/rest/extract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,25 @@ mod tests;
use aide::openapi::Operation;
use serde_json::Value;

pub(in crate::rest) use self::{json::Json, path::Path, query::Query};
#[cfg_attr(
not(test),
expect(unused_imports, reason = "only the tests read query parameters")
)]
pub(in crate::rest) use self::query::Query;
Comment thread
indietyp marked this conversation as resolved.
pub(in crate::rest) use self::{json::Json, path::Path};

/// The part of a request or response that [`Json`], [`Path`] or [`Query`] reads or writes.
/// The part of a request that [`Json`], [`Path`] or [`Query`] reads.
///
/// Each extractor marks the operations it documents with the parts it reads or writes, and
/// [`openapi::build`] requires the mark for every path parameter, query parameter, request body and
/// JSON response an operation documents.
/// Each extractor marks the operations it documents with the parts it reads, and
/// [`openapi::build`] requires the mark for every path parameter, query parameter and request body
/// an operation documents.
///
/// [`openapi::build`]: crate::rest::openapi::build
#[derive(Debug, Clone, Copy)]
pub(in crate::rest) enum MessagePart {
PathParameters,
QueryParameters,
RequestBody,
ResponseBody,
}

impl MessagePart {
Expand All @@ -44,7 +48,6 @@ impl MessagePart {
Self::PathParameters => "x-hash-path-parameters",
Self::QueryParameters => "x-hash-query-parameters",
Self::RequestBody => "x-hash-request-body",
Self::ResponseBody => "x-hash-response-body",
}
}

Expand Down
4 changes: 4 additions & 0 deletions libs/@local/graph/api/src/rest/extract/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ impl Expose<QueryProblem> for QueryRejection {
/// serde hands flattened fields their values as strings, which only a string field accepts.
///
/// [`openapi::build`]: crate::rest::openapi::build
#[cfg_attr(
Comment thread
indietyp marked this conversation as resolved.
not(test),
expect(dead_code, reason = "only the tests read query parameters")
)]
pub(in crate::rest) struct Query<T>(pub T);

impl<T, S> FromRequestParts<S> for Query<T>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 9 additions & 5 deletions libs/@local/graph/api/src/rest/extract/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::io;
use aide::{
axum::{
ApiRouter,
routing::{get, post, post_with},
routing::{get, post},
},
openapi::{Info, OpenApi, Operation, PathItem, Response, StatusCode},
};
Expand All @@ -17,7 +17,11 @@ use serde_json::{Value, json};
use tower::ServiceExt as _;

use super::{Json, Path, Query, json::content_status};
use crate::rest::{openapi, test_utils::NoCredentials};
use crate::rest::{
Api, openapi,
routing::{self, Routes},
test_utils::NoCredentials,
};

#[derive(Deserialize, JsonSchema)]
struct Subject {
Expand Down Expand Up @@ -500,17 +504,17 @@ async fn echo(
/// snapshot shows how its rejections join the responses of the middleware.
#[test]
fn echo_document() {
let api = openapi::build::<NoCredentials>(
let api: Api = openapi::build::<NoCredentials, _>(
"/example",
Info {
title: "Extractor example".to_owned(),
version: "1".to_owned(),
..Info::default()
},
|| {
ApiRouter::new().api_route(
Routes::new().route(
"/echo/{label}",
post_with(echo, |operation| {
routing::post(echo, |operation| {
operation.id("echo").summary("Echo a request").description(
"Returns the label from the path and the value from the body, repeated as \
often as the query parameter `repeat` asks.",
Expand Down
6 changes: 4 additions & 2 deletions libs/@local/graph/api/src/rest/internal/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ impl credentials::Credentials for Credentials {
}
}

impl credentials::AdmitsAnonymous for Credentials {}

impl OperationInput for Credentials {
fn operation_input(_ctx: &mut GenContext, operation: &mut Operation) {
let _: TransformOperation<'_> = TransformOperation::new(operation)
Expand All @@ -51,10 +53,10 @@ impl OperationInput for Credentials {
#[cfg(test)]
mod tests {
use super::Credentials;
use crate::rest::test_utils::assert_authentication;
use crate::rest::test_utils::assert_anonymous_authentication;

#[tokio::test]
async fn credentials_resolve_callers() {
assert_authentication::<Credentials>().await;
assert_anonymous_authentication::<Credentials>().await;
}
}
6 changes: 3 additions & 3 deletions libs/@local/graph/api/src/rest/internal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ use super::{Api, caller, documentation, openapi};
/// The path the internal API, its documentation and the legacy document are served under.
pub(super) const PREFIX: &str = "/_api";

pub(super) fn api() -> Api {
openapi::build::<Credentials>(
pub(super) fn api<T: Clone + Send + Sync + 'static>() -> Api<T> {
openapi::build::<Credentials, _>(
PREFIX,
Info {
title: "Internal".to_owned(),
version: "unversioned".to_owned(),
description: Some(documentation::OVERVIEW.to_owned()),
..Info::default()
},
caller::routes::<Credentials>,
caller::routes::<Credentials, T>,
document,
)
}
Expand Down
2 changes: 1 addition & 1 deletion libs/@local/graph/api/src/rest/legacy/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ where
query_logger.capture(Some(actor_id), OpenApiQuery::DiffEntity(&params));
}

let store = store_pool
let mut store = store_pool
.acquire(temporal_client.0)
.await
.map_err(report_to_response)?;
Expand Down
Loading
Loading