Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions Cargo.lock

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

5 changes: 3 additions & 2 deletions libs/@local/graph/authorization/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ authors.workspace = true

[dependencies]
# Public workspace dependencies
error-stack = { workspace = true, public = true, features = ["unstable"] }
type-system = { workspace = true, public = true }
error-stack = { workspace = true, public = true, features = ["unstable"] }
hash-graph-temporal-versioning = { workspace = true, public = true }
type-system = { workspace = true, public = true }

# Public third-party dependencies
postgres-types = { workspace = true, public = true, features = ["derive", "with-uuid-1"], optional = true }
Expand Down
3 changes: 2 additions & 1 deletion libs/@local/graph/authorization/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"dependencies": {
"@blockprotocol/type-system-rs": "workspace:*",
"@rust/error-stack": "workspace:*",
"@rust/hash-codec": "workspace:*"
"@rust/hash-codec": "workspace:*",
"@rust/hash-graph-temporal-versioning": "workspace:*"
},
"devDependencies": {
"@local/tsconfig": "workspace:*",
Expand Down
75 changes: 68 additions & 7 deletions libs/@local/graph/authorization/src/policies/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use std::collections::{HashMap, HashSet};

use error_stack::{Report, ResultExt as _};
use hash_graph_temporal_versioning::Timestamp;
use type_system::{
knowledge::entity::{
EntityId,
Expand Down Expand Up @@ -65,6 +66,7 @@
#[derive(Debug)]
pub struct PolicyComponents {
actor_id: Option<ActorId>,
timestamp: Timestamp<()>,
is_instance_admin: bool,
policies: Vec<ResolvedPolicy>,
tracked_actions: HashMap<ActionName, Option<OptimizationData>>,
Expand All @@ -86,6 +88,16 @@
self.actor_id
}

/// Returns the store's clock reading captured while building these components.
///
/// This value is the time authority for the operation these components were built for: it
/// should be used to resolve temporal axes and to derive written timestamps, so that all
/// timestamps within one operation agree with each other and with the store's clock.
#[must_use]
pub const fn timestamp(&self) -> Timestamp<()> {
self.timestamp
}

/// Returns `true` if the actor is an instance admin.
///
/// Instance admins have elevated privileges, such as bypassing filter protection
Expand Down Expand Up @@ -319,6 +331,7 @@
pub struct PolicyComponentsBuilder<'a, S> {
store: &'a S,
actor: AuthenticatedActor,
timestamp: Option<Timestamp<()>>,
context: ContextBuilder,
entity_type_ids: HashSet<Cow<'a, VersionedUrl>>,
property_type_ids: HashSet<Cow<'a, VersionedUrl>>,
Expand All @@ -334,6 +347,7 @@
Self {
store,
actor: AuthenticatedActor::Uuid(ActorEntityUuid::public_actor()),
timestamp: None,
context: ContextBuilder::default(),
entity_type_ids: HashSet::new(),
property_type_ids: HashSet::new(),
Expand All @@ -353,6 +367,27 @@
self
}

/// Provides the store's clock reading captured for the surrounding operation.
///
/// Callers which already hold a clock reading from the store β€” e.g. because an earlier
/// statement of the same operation returned one β€” should pass it here so the components share
/// the operation's timestamp. When absent, a reading is captured while building the
/// components.
pub fn set_timestamp(&mut self, timestamp: Timestamp<()>) {
self.timestamp = Some(timestamp);
}

Check warning

Code scanning / clippy

this could be a const fn Warning

this could be a const fn
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

/// Provides the store's clock reading captured for the surrounding operation.
///
/// See [`set_timestamp`] for details.
///
/// [`set_timestamp`]: Self::set_timestamp
#[must_use]
pub fn with_timestamp(mut self, timestamp: Timestamp<()>) -> Self {
self.set_timestamp(timestamp);
self
}

pub fn add_entity_type_id(&mut self, entity_type: &'a VersionedUrl) {
self.entity_type_ids.insert(Cow::Borrowed(entity_type));
}
Expand Down Expand Up @@ -584,15 +619,36 @@
#[tracing::instrument(level = "info", skip(self))]
fn into_future(mut self) -> Self::IntoFuture {
async move {
let actor_id = match self.actor {
AuthenticatedActor::Id(actor_id) => Some(actor_id),
AuthenticatedActor::Uuid(actor_uuid) => self
let (actor_id, timestamp) = match (self.actor, self.timestamp) {
(AuthenticatedActor::Id(actor_id), timestamp) => (Some(actor_id), timestamp),
(AuthenticatedActor::Uuid(actor_uuid), timestamp @ Some(_)) => (
self.store
.determine_actor(actor_uuid)
.await
.change_context(ContextCreationError::DetermineActor {
actor_id: actor_uuid,
})?,
timestamp,
),
(AuthenticatedActor::Uuid(actor_uuid), None) => {
let (actor_id, timestamp) = self
.store
.determine_actor_with_timestamp(actor_uuid)
.await
.change_context(ContextCreationError::DetermineActor {
actor_id: actor_uuid,
})?;
(actor_id, Some(timestamp))
}
};

let timestamp = match timestamp {
Some(timestamp) => timestamp,
None => self
.store
.determine_actor(actor_uuid)
.current_timestamp()
.await
.change_context(ContextCreationError::DetermineActor {
actor_id: actor_uuid,
})?,
.change_context(ContextCreationError::StoreError)?,
};

if let Some(actor_id) = actor_id {
Expand Down Expand Up @@ -707,6 +763,7 @@

let mut policy_components = PolicyComponents {
actor_id,
timestamp,
is_instance_admin: self.context.is_instance_admin(),
policies,
tracked_actions: actions.iter().map(|action| (*action, None)).collect(),
Expand Down Expand Up @@ -735,6 +792,7 @@
mod tests {
use std::collections::{HashMap, HashSet};

use hash_graph_temporal_versioning::Timestamp;
use type_system::{knowledge::entity::id::EntityUuid, principal::actor::ActorId};
use uuid::Uuid;

Expand Down Expand Up @@ -772,6 +830,7 @@

let mut policy_components = PolicyComponents {
actor_id: None,
timestamp: Timestamp::UNIX_EPOCH,
is_instance_admin: false,
policies,
tracked_actions: HashMap::from([(ActionName::View, None)]),
Expand Down Expand Up @@ -826,6 +885,7 @@

let policy_components_without_optimization = PolicyComponents {
actor_id: None,
timestamp: Timestamp::UNIX_EPOCH,
is_instance_admin: false,
policies: policies_without_optimization,
tracked_actions: HashMap::from([(ActionName::View, None)]),
Expand Down Expand Up @@ -854,6 +914,7 @@

let mut policy_components_with_optimization = PolicyComponents {
actor_id: None,
timestamp: Timestamp::UNIX_EPOCH,
is_instance_admin: false,
policies: policies_with_optimization,
tracked_actions: HashMap::new(),
Expand Down
6 changes: 6 additions & 0 deletions libs/@local/graph/authorization/src/policies/store/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ pub enum DetermineActorError {

impl Error for DetermineActorError {}

#[derive(Debug, derive_more::Display)]
#[display("Could not read the current timestamp from the store")]
pub struct CurrentTimestampError;

impl Error for CurrentTimestampError {}

#[derive(Debug, derive_more::Display)]
#[display("Could not build principal context for actor with ID `{actor_id}`")]
pub enum BuildPrincipalContextError {
Expand Down
41 changes: 37 additions & 4 deletions libs/@local/graph/authorization/src/policies/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::collections::{
};

use error_stack::{Report, bail, ensure};
use hash_graph_temporal_versioning::Timestamp;
use type_system::{
knowledge::{entity::id::EntityEditionId, property::PropertyObjectWithMetadata},
ontology::VersionedUrl,
Expand All @@ -22,10 +23,10 @@ use uuid::Uuid;
use self::error::{
ActorCreationError, BuildDataTypeContextError, BuildEntityContextError,
BuildEntityTypeContextError, BuildPrincipalContextError, BuildPropertyTypeContextError,
ContextCreationError, CreatePolicyError, DetermineActorError, EnsureSystemPoliciesError,
GetPoliciesError, GetSystemAccountError, PolicyStoreError, RemovePolicyError,
RoleAssignmentError, TeamCreationError, TeamRoleCreationError, TeamRoleError,
UpdatePolicyError, WebCreationError, WebRoleCreationError, WebRoleError,
ContextCreationError, CreatePolicyError, CurrentTimestampError, DetermineActorError,
EnsureSystemPoliciesError, GetPoliciesError, GetSystemAccountError, PolicyStoreError,
RemovePolicyError, RoleAssignmentError, TeamCreationError, TeamRoleCreationError,
TeamRoleError, UpdatePolicyError, WebCreationError, WebRoleCreationError, WebRoleError,
};
use super::{
ContextBuilder, Effect, Policy, PolicyId, ResolvedPolicy,
Expand Down Expand Up @@ -534,6 +535,38 @@ pub trait PrincipalStore {
actor_entity_uuid: ActorEntityUuid,
) -> Result<Option<ActorId>, Report<DetermineActorError>>;

/// Determines the type of an actor by its ID, additionally reading the store's clock.
///
/// The timestamp is captured by the same statement that looks up the actor, so both are
/// obtained in a single round trip. For the public actor no lookup is required and `None` is
/// returned as the actor; the clock is then read on its own.
///
/// # Errors
///
/// - [`StoreError`] if a database error occurs
/// - [`ActorNotFound`] if the actor with the given ID doesn't exist
///
/// [`StoreError`]: DetermineActorError::StoreError
/// [`ActorNotFound`]: DetermineActorError::ActorNotFound
async fn determine_actor_with_timestamp(
&self,
actor_entity_uuid: ActorEntityUuid,
) -> Result<(Option<ActorId>, Timestamp<()>), Report<DetermineActorError>>;

/// Reads the current timestamp from the store's clock.
///
/// The store's clock is the single time authority for all query-relevant timestamps, so
/// callers which need a timestamp β€” e.g. to resolve temporal axes or to stamp written
/// records β€” must use this (or a value derived from another statement's clock reading, such
/// as [`determine_actor_with_timestamp`]) rather than the host's clock.
///
/// [`determine_actor_with_timestamp`]: Self::determine_actor_with_timestamp
///
/// # Errors
///
/// - [`CurrentTimestampError`] if a database error occurs
async fn current_timestamp(&self) -> Result<Timestamp<()>, Report<CurrentTimestampError>>;

/// Builds a context used to evaluate policies for an actor.
///
/// # Errors
Expand Down
6 changes: 6 additions & 0 deletions libs/@local/graph/postgres-store/src/snapshot/entity/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::HashMap;

use error_stack::{Report, ResultExt as _, ensure};
use futures::{StreamExt as _, TryStreamExt as _, stream};
use hash_graph_authorization::policies::store::PrincipalStore as _;
use hash_graph_store::{
entity::{EntityValidationReport, ValidateEntityComponents},
error::InsertionError,
Expand Down Expand Up @@ -257,10 +258,15 @@ where
.await
.change_context(InsertionError)?;

let timestamp = postgres_client
.current_timestamp()
.await
.change_context(InsertionError)?;
let validator_provider = StoreProvider {
store: postgres_client,
cache: Box::new(StoreCache::default()),
policy_components: None,
timestamp,
};

let mut edition_ids_updates = Vec::new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet, hash_map::Entry};

use error_stack::{Report, ResultExt as _};
use futures::TryStreamExt as _;
use hash_graph_authorization::policies::store::PrincipalStore as _;
use hash_graph_store::{
entity::{
DeleteEntitiesParams, DeletionScope, DeletionSummary, EntityQueryPath, LinkDeletionBehavior,
Expand Down Expand Up @@ -901,7 +902,11 @@ where
actor_id: ActorEntityUuid,
params: DeleteEntitiesParams<'_>,
) -> Result<DeletionSummary, Report<DeletionError>> {
let transaction_time = Timestamp::<TransactionTime>::now();
let transaction_time = Timestamp::<TransactionTime>::from_anonymous(
self.current_timestamp()
.await
.change_context(DeletionError::Store)?,
);
let decision_time = params
.decision_time
.unwrap_or_else(|| transaction_time.cast());
Expand Down
Loading
Loading