Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b78bc20
feat: query for what has been requested
indietyp Jun 8, 2026
e965452
chore: checkpoint
indietyp Jun 9, 2026
72fae63
feat: checkpoint
indietyp Jun 16, 2026
fcd340f
chore: remove ice ice baby
indietyp Jun 16, 2026
2902f80
feat: checkpoint
indietyp Jun 16, 2026
aaff555
feat: checkpoint
indietyp Jun 16, 2026
28aa218
feat: checkpoint
indietyp Jun 16, 2026
be100d5
feat: do not skip parameters
indietyp Jun 17, 2026
f2dd3b1
feat: remove property masking in favour of lateral join
indietyp Jun 17, 2026
48a8d2d
feat: more policy into own module
indietyp Jun 17, 2026
7bcc002
feat: protection translation unit
indietyp Jun 17, 2026
059697c
feat: checkpoint
indietyp Jun 17, 2026
32cb479
feat: authorization code
indietyp Jun 17, 2026
c84cf9e
fix: lints
indietyp Jun 17, 2026
9ae64de
feat: checkpoint
indietyp Jun 17, 2026
3823080
feat: checkpoint
indietyp Jun 17, 2026
25a7780
feat: test infrastructure
indietyp Jun 18, 2026
da85a8a
feat: reinforced tests
indietyp Jun 18, 2026
20c6ca4
feat: integration tests
indietyp Jun 18, 2026
3180a4a
feat: checkpoint
indietyp Jun 18, 2026
0772247
feat: checkpoint
indietyp Jun 18, 2026
5b2e586
feat: hardening
indietyp Jun 18, 2026
edc8804
feat: checkpoint
indietyp Jun 18, 2026
b31f4ca
fix: inject auxiliary requests
indietyp Jun 18, 2026
23fee1a
fix: suggestions from bugbot
indietyp Jun 18, 2026
cd24aca
chore: remove stray file
indietyp Jun 18, 2026
4e12741
chore: add `Arc` where needed
indietyp Jun 18, 2026
96a73c4
chore: fix debug assertions
indietyp Jun 18, 2026
ef5732d
fix: turborepo
indietyp Jun 19, 2026
3307b1a
fix: openapi
indietyp Jun 19, 2026
e0e455a
fix: docs
indietyp Jun 19, 2026
bc78a3b
fix: merge conflicts
indietyp Jun 22, 2026
5a0812f
chore: fix imports
indietyp Jun 23, 2026
d74c496
fix: comment
indietyp Jun 23, 2026
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
18 changes: 9 additions & 9 deletions apps/hash-graph/src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use multiaddr::{Multiaddr, Protocol};
use regex::Regex;
use reqwest::{Client, Url};
use tokio::{io, net::TcpListener, signal, time::timeout};
use tokio_postgres::NoTls;
use tokio_postgres::{Client as PostgresClient, NoTls};
use tokio_util::{codec::FramedWrite, sync::CancellationToken};
use type_system::ontology::json_schema::DomainValidator;

Expand Down Expand Up @@ -383,15 +383,15 @@ where
/// Starts the main graph API server (REST + optional RPC).
async fn start_server<S>(
pool: S,
postgres: PostgresStorePool,
compiler: Arc<CompilerContext>,
config: ServerConfig,
query_logger: Option<QueryLogger>,
filter_protection: Arc<PropertyProtectionFilterConfig<'static>>,
lifecycle: &ServerLifecycle,
) -> Result<(), Report<GraphError>>
where
S: StorePool + Send + Sync + 'static,
for<'p> S::Store<'p>: RestApiStore + PrincipalStore + PolicyStore,
for<'p> S::Store<'p>: RestApiStore + PrincipalStore + PolicyStore + AsRef<PostgresClient>,
{
let store = Arc::new(pool);
let temporal_client = create_temporal_client(&config.temporal)
Expand All @@ -414,12 +414,12 @@ where

let router = rest_api_router(RestRouterDependencies {
store,
postgres,
temporal_client,
domain_regex: DomainValidator::new(config.allowed_url_domain),
query_logger,
api_config: config.api_config,
compiler,
filter_protection,
});
start_rest_server(router, config.http_address, lifecycle);

Expand Down Expand Up @@ -456,9 +456,9 @@ pub async fn server(mut args: ServerArgs) -> Result<(), Report<GraphError>> {
validate_links: !args.config.skip_link_validation,
skip_embedding_creation: args.config.skip_embedding_creation,
filter_protection: if args.config.skip_filter_protection {
PropertyProtectionFilterConfig::new()
Arc::new(PropertyProtectionFilterConfig::new())
} else {
PropertyProtectionFilterConfig::hash_default()
Arc::new(PropertyProtectionFilterConfig::hash_default())
},
},
)
Expand All @@ -478,8 +478,6 @@ pub async fn server(mut args: ServerArgs) -> Result<(), Report<GraphError>> {

let lifecycle = ServerLifecycle::new();

let postgres = pool.clone();

if args.embed_admin {
start_admin_server(pool.clone(), args.admin, &lifecycle);
}
Expand All @@ -488,6 +486,8 @@ pub async fn server(mut args: ServerArgs) -> Result<(), Report<GraphError>> {
start_type_fetcher(args.type_fetcher.clone(), &lifecycle);
}

let filter_protection = Arc::clone(&pool.settings.filter_protection);

let pool = FetchingPool::new(
pool,
(
Expand Down Expand Up @@ -523,10 +523,10 @@ pub async fn server(mut args: ServerArgs) -> Result<(), Report<GraphError>> {

if let Err(error) = start_server(
pool,
postgres,
compiler,
args.config,
query_logger,
filter_protection,
&lifecycle,
)
.await
Expand Down
9 changes: 9 additions & 0 deletions libs/@local/graph/api/openapi/openapi.json

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

24 changes: 22 additions & 2 deletions libs/@local/graph/api/src/rest/hashql/compile.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use hash_graph_authorization::policies::action::ActionName;
use hashql_ast::error::AstDiagnosticCategory;
use hashql_core::{
heap::{Heap, ResetAllocator as _, Scratch},
heap::{self, Heap, ResetAllocator as _, Scratch},
module::ModuleRegistry,
span::{SpanId, SpanTable},
symbol::sym,
Expand All @@ -16,7 +17,10 @@ use hashql_mir::{
body::Body,
def::{DefId, DefIdVec},
error::MirDiagnosticCategory,
pass::{LowerConfig, execution::ExecutionAnalysisResidual},
pass::{
LowerConfig,
execution::{ExecutionAnalysisResidual, VertexType},
},
};
use hashql_syntax_jexpr::span::Span;

Expand All @@ -29,6 +33,10 @@ pub(crate) struct CodeCompilationArtifact<'heap> {
pub postgres: PreparedQueries<'heap, &'heap Heap>,
}

pub(crate) struct CodeExecutionPermissions<'heap> {
pub actions: heap::Vec<'heap, ActionName>,
}

pub(crate) struct Compilation<'heap> {
pub heap: &'heap Heap,

Expand All @@ -39,6 +47,7 @@ pub(crate) struct Compilation<'heap> {

pub entrypoint: DefId,
pub artifact: CodeCompilationArtifact<'heap>,
pub permissions: CodeExecutionPermissions<'heap>,
}

impl<'heap> Compilation<'heap> {
Expand Down Expand Up @@ -158,6 +167,16 @@ impl<'heap> Compilation<'heap> {
let queries = postgres.compile();
scratch.reset();

let mut actions = heap::Vec::new_in(heap);
for query in queries.iter() {
let action = match query.vertex_type {
VertexType::Entity => ActionName::ViewEntity,
};
actions.push(action);
}

let permissions = CodeExecutionPermissions { actions };

context
.diagnostics
.into_status(())
Expand All @@ -176,6 +195,7 @@ impl<'heap> Compilation<'heap> {
interpreter: bodies,
postgres: queries,
},
permissions,
})
}

Expand Down
120 changes: 119 additions & 1 deletion libs/@local/graph/api/src/rest/hashql/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ use alloc::borrow::Cow;
use core::ops::Range;

use axum::response::{Html, IntoResponse as _};
use error_stack::Report;
use hash_graph_authorization::policies::store::error::{ContextCreationError, DetermineActorError};
use hashql_ast::error::AstDiagnosticCategory;
use hashql_core::span::{SpanId, SpanTable};
use hashql_diagnostics::{
DiagnosticCategory, Failure, Sources, Status, Success,
Diagnostic, DiagnosticCategory, Failure, Label, Message, Sources, Status, Success,
category::{TerminalDiagnosticCategory, canonical_category_id},
diagnostic::render::{Format, RenderOptions},
severity::Critical,
Expand Down Expand Up @@ -67,6 +69,122 @@ impl DiagnosticCategory for HashQlDiagnosticCategory {
}
}

pub(crate) fn store_acquire_diagnostic(
report: &Report<impl core::error::Error>,
root_span: SpanId,
) -> Diagnostic<HashQlDiagnosticCategory, SpanId, Critical> {
let mut diagnostic =
Diagnostic::new(HashQlDiagnosticCategory::Infrastructure, Critical::BUG).primary(
Label::new(root_span, "failed to acquire database connection"),
);

diagnostic.add_message(Message::note(
"the query compiled successfully but the server could not open a database connection to \
execute it",
));

log_report(
&mut diagnostic,
report,
"failed to acquire database connection",
);

diagnostic
}

pub(crate) fn authorization_context_diagnostic(
report: &Report<ContextCreationError>,
root_span: SpanId,
) -> Diagnostic<HashQlDiagnosticCategory, SpanId, Critical> {
match report.current_context() {
ContextCreationError::ActorNotFound { actor_id } => {
actor_not_found(report, root_span, actor_id)
}
ContextCreationError::DetermineActor { actor_id } => {
// DetermineActor wraps either ActorNotFound or StoreError.
// Only report "does not exist" when the actor was actually looked up
// and not found; a store error during lookup is infrastructure.
if report
.downcast_ref::<DetermineActorError>()
.is_some_and(|inner| matches!(inner, DetermineActorError::StoreError))
{
authorization_context_failed(report, root_span)
} else {
actor_not_found(report, root_span, actor_id)
}
}
ContextCreationError::BuildPrincipalContext { .. }
| ContextCreationError::BuildEntityTypeContext { .. }
| ContextCreationError::BuildPropertyTypeContext { .. }
| ContextCreationError::BuildDataTypeContext { .. }
| ContextCreationError::BuildEntityContext { .. }
| ContextCreationError::ResolveActorPolicies { .. }
| ContextCreationError::CreatePolicySet
| ContextCreationError::CreatePolicyContext
| ContextCreationError::StoreError => authorization_context_failed(report, root_span),
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

fn actor_not_found(
report: &Report<ContextCreationError>,
root_span: SpanId,
actor_id: &impl core::fmt::Display,
) -> Diagnostic<HashQlDiagnosticCategory, SpanId, Critical> {
let mut diagnostic =
Diagnostic::new(HashQlDiagnosticCategory::Infrastructure, Critical::ERROR).primary(
Label::new(root_span, format!("actor `{actor_id}` does not exist")),
);

diagnostic.add_message(Message::note(
"every request must be authenticated with a valid actor ID; the provided ID does not \
correspond to any known user or machine",
));

log_report(&mut diagnostic, report, "actor not found");

diagnostic
}

fn authorization_context_failed(
report: &Report<ContextCreationError>,
root_span: SpanId,
) -> Diagnostic<HashQlDiagnosticCategory, SpanId, Critical> {
let mut diagnostic =
Diagnostic::new(HashQlDiagnosticCategory::Infrastructure, Critical::BUG).primary(
Label::new(root_span, "failed to build authorization context"),
);

diagnostic.add_message(Message::note(format!(
"the authorization system reported: {}",
report.current_context()
)));

diagnostic.add_message(Message::help(
"the query compiled successfully but the server could not resolve the policies needed to \
authorize execution",
));

log_report(
&mut diagnostic,
report,
"failed to build authorization context",
);

diagnostic
}

fn log_report(
diagnostic: &mut Diagnostic<HashQlDiagnosticCategory, SpanId, Critical>,
report: &Report<impl core::error::Error>,
log_message: &str,
) {
if cfg!(debug_assertions) {
diagnostic.add_message(Message::note(format!("{report:?}")));
} else {
tracing::error!(?report, "{log_message}");
}
}

#[derive(Debug, serde::Serialize)]
struct PointerSpan {
pub range: Range<usize>,
Expand Down
Loading
Loading