Skip to content

Commit 8f4b83c

Browse files
authored
H-4699: Support policy resource filter on the creator (#7406)
1 parent 90018a0 commit 8f4b83c

File tree

22 files changed

+580
-212
lines changed

22 files changed

+580
-212
lines changed

apps/hash-ai-worker-ts/src/activities/shared/get-llm-response.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { generateUuid } from "@local/hash-isomorphic-utils/generate-uuid";
1515
import { systemLinkEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids";
1616
import { stringifyError } from "@local/hash-isomorphic-utils/stringify-error";
1717
import type { IncurredIn } from "@local/hash-isomorphic-utils/system-types/usagerecord";
18-
import type { PrincipalConstraint } from "@rust/hash-graph-authorization/types";
1918
// import { StatusCode } from "@local/status";
2019
import { backOff } from "exponential-backoff";
2120

@@ -206,19 +205,6 @@ export const getLlmResponse = async <T extends LlmParams>(
206205
} satisfies OriginProvenance,
207206
};
208207

209-
const viewPrincipals: PrincipalConstraint[] = [
210-
{
211-
type: "actor",
212-
actorType: "user",
213-
id: userAccountId,
214-
},
215-
{
216-
type: "actor",
217-
actorType: "ai",
218-
id: aiAssistantAccountId,
219-
},
220-
];
221-
222208
const errors = await Promise.all(
223209
incurredInEntities.map(async ({ entityId }) => {
224210
try {
@@ -263,12 +249,6 @@ export const getLlmResponse = async <T extends LlmParams>(
263249
},
264250
},
265251
],
266-
policies: viewPrincipals.map((principal) => ({
267-
name: `usage-record-view-entity-${incurredInEntityUuid}`,
268-
effect: "permit",
269-
actions: ["viewEntity"],
270-
principal,
271-
})),
272252
},
273253
);
274254

apps/hash-api/src/graph/knowledge/system-types/user-secret.ts

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
} from "@local/hash-isomorphic-utils/ontology-type-ids";
2222
import type { UsesUserSecret } from "@local/hash-isomorphic-utils/system-types/google/shared";
2323
import type { UserSecret } from "@local/hash-isomorphic-utils/system-types/shared";
24-
import type { PrincipalConstraint } from "@rust/hash-graph-authorization/types";
2524
import type { Auth } from "googleapis";
2625

2726
import { createEntity } from "../primitive/entity";
@@ -181,19 +180,6 @@ export const createUserSecret = async <
181180
);
182181
}
183182

184-
const viewPrincipals: PrincipalConstraint[] = [
185-
{
186-
type: "actor",
187-
actorType: "user",
188-
id: userAccountId,
189-
},
190-
{
191-
type: "actor",
192-
actorType: "machine",
193-
id: managingBotAccountId,
194-
},
195-
];
196-
197183
const userSecretEntityUuid = generateUuid() as EntityUuid;
198184
const usesUserSecretEntityUuid = generateUuid() as EntityUuid;
199185

@@ -206,16 +192,18 @@ export const createUserSecret = async <
206192
entityUuid: userSecretEntityUuid,
207193
properties: secretMetadata,
208194
relationships: botEditorUserViewerOnly,
209-
policies: viewPrincipals.map((principal) => ({
210-
name: `user-secret-view-entity-${userSecretEntityUuid}`,
211-
principal,
212-
effect: "permit",
213-
actions: ["viewEntity"],
214-
resource: {
215-
type: "entity",
216-
id: userSecretEntityUuid,
195+
policies: [
196+
{
197+
name: `user-secret-view-entity-${userSecretEntityUuid}`,
198+
principal: {
199+
type: "actor",
200+
actorType: "machine",
201+
id: managingBotAccountId,
202+
},
203+
effect: "permit",
204+
actions: ["viewEntity"],
217205
},
218-
})),
206+
],
219207
},
220208
);
221209

@@ -233,16 +221,18 @@ export const createUserSecret = async <
233221
},
234222
entityTypeIds: [systemLinkEntityTypes.usesUserSecret.linkEntityTypeId],
235223
relationships: botEditorUserViewerOnly,
236-
policies: viewPrincipals.map((principal) => ({
237-
name: `user-secret-view-entity-${usesUserSecretEntityUuid}`,
238-
principal,
239-
effect: "permit",
240-
actions: ["viewEntity"],
241-
resource: {
242-
type: "entity",
243-
id: usesUserSecretEntityUuid,
224+
policies: [
225+
{
226+
name: `user-secret-view-entity-${userSecretEntityUuid}`,
227+
principal: {
228+
type: "actor",
229+
actorType: "machine",
230+
id: managingBotAccountId,
231+
},
232+
effect: "permit",
233+
actions: ["viewEntity"],
244234
},
245-
})),
235+
],
246236
},
247237
);
248238

libs/@local/graph/authorization/schemas/policies.cedarschema

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace HASH {
1010

1111
entity Entity in [Web] {
1212
entity_types: Set<EntityType>,
13+
created_by: ActorId,
1314
};
1415

1516
entity EntityType in [Web] {
@@ -18,9 +19,14 @@ namespace HASH {
1819
};
1920

2021
entity User, Machine, Ai in [HASH::Web::Role, HASH::Team::Role] {
22+
id: ActorId,
2123
};
2224

2325
entity Public {
26+
// This is required to add policies depending on `principal.id`
27+
// in Cedar policies. Effectively, we set this to a dummy value
28+
// `{"type": "public", "id": "00000000-0000-0000-0000-000000000000"}`.
29+
id: ActorId,
2430
};
2531

2632
action all;
@@ -49,6 +55,11 @@ namespace HASH {
4955
principal: [User, Machine, Ai],
5056
resource: [EntityType],
5157
};
58+
59+
type ActorId = {
60+
type: String,
61+
id: String,
62+
};
5263
}
5364

5465
namespace HASH::Team {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use alloc::collections::BTreeMap;
2+
use core::{error::Error, fmt};
3+
4+
use cedar_policy_core::ast;
5+
use error_stack::{Report, ResultExt as _};
6+
use smol_str::SmolStr;
7+
use type_system::principal::actor::{ActorEntityUuid, ActorId, AiId, MachineId, UserId};
8+
use uuid::Uuid;
9+
10+
use super::CedarExpressionVisitor;
11+
12+
#[derive(Debug, derive_more::Display)]
13+
pub(crate) enum ParseActorIdExpressionError {
14+
#[display("Unexpected fields: `{}`", _0.join(", "))]
15+
UnexpectedField(Vec<String>),
16+
#[display("Missing field: `{_0}`")]
17+
MissingField(String),
18+
#[display("Invalid type: `{_0}`")]
19+
InvalidTypeExpression(String),
20+
#[display("Invalid type: `{_0}`")]
21+
InvalidType(String),
22+
#[display("Invalid UUID expression: `{_0}`")]
23+
InvalidUuidExpression(String),
24+
#[display("Invalid UUID: `{_0}`")]
25+
InvalidUuid(String),
26+
}
27+
28+
impl Error for ParseActorIdExpressionError {}
29+
30+
pub(crate) struct ActorIdVisitor;
31+
32+
impl CedarExpressionVisitor for ActorIdVisitor {
33+
type Error = Report<ParseActorIdExpressionError>;
34+
type Value = Option<ActorId>;
35+
36+
fn expecting(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
37+
fmt.write_str("an actor id")
38+
}
39+
40+
fn visit_record(
41+
&self,
42+
record: &BTreeMap<SmolStr, ast::Expr>,
43+
) -> Option<Result<Self::Value, Self::Error>> {
44+
let Some(type_expr) = record.get("type") else {
45+
return Some(Err(Report::new(ParseActorIdExpressionError::MissingField(
46+
"type".to_owned(),
47+
))));
48+
};
49+
let Some(uuid_expr) = record.get("id") else {
50+
return Some(Err(Report::new(ParseActorIdExpressionError::MissingField(
51+
"id".to_owned(),
52+
))));
53+
};
54+
if record.len() != 2 {
55+
return Some(Err(Report::new(
56+
ParseActorIdExpressionError::UnexpectedField(
57+
record
58+
.keys()
59+
.filter(|&key| (key != "type" && key != "id"))
60+
.map(SmolStr::to_string)
61+
.collect(),
62+
),
63+
)));
64+
}
65+
66+
let ast::ExprKind::Lit(ast::Literal::String(type_string)) = type_expr.expr_kind() else {
67+
return Some(Err(Report::new(
68+
ParseActorIdExpressionError::InvalidTypeExpression(type_expr.to_string()),
69+
)));
70+
};
71+
72+
let ast::ExprKind::Lit(ast::Literal::String(uuid_string)) = uuid_expr.expr_kind() else {
73+
return Some(Err(Report::new(
74+
ParseActorIdExpressionError::InvalidUuidExpression(uuid_expr.to_string()),
75+
)));
76+
};
77+
78+
let actor_entity_uuid = match Uuid::parse_str(uuid_string)
79+
.change_context_lazy(|| {
80+
ParseActorIdExpressionError::InvalidUuid(uuid_string.to_string())
81+
})
82+
.attach_printable("Invalid UUID")
83+
{
84+
Ok(uuid) => ActorEntityUuid::new(uuid),
85+
Err(error) => {
86+
return Some(Err(error));
87+
}
88+
};
89+
90+
Some(Ok(match type_string.as_str() {
91+
"user" => Some(ActorId::User(UserId::new(actor_entity_uuid))),
92+
"machine" => Some(ActorId::Machine(MachineId::new(actor_entity_uuid))),
93+
"ai" => Some(ActorId::Ai(AiId::new(actor_entity_uuid))),
94+
"public" => None,
95+
_ => {
96+
return Some(Err(Report::new(ParseActorIdExpressionError::InvalidType(
97+
type_string.to_string(),
98+
))));
99+
}
100+
}))
101+
}
102+
}

libs/@local/graph/authorization/src/policies/cedar/expression_tree.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub enum PolicyExpressionTree {
2424
BaseUrl(BaseUrl),
2525
OntologyTypeVersion(OntologyTypeVersion),
2626
IsOfType(VersionedUrl),
27+
CreatedByPrincipal,
2728
}
2829

2930
#[derive(Debug, derive_more::Display)]
@@ -38,6 +39,8 @@ impl Error for ParseBinaryExpressionError {}
3839

3940
#[derive(Debug, derive_more::Display)]
4041
pub(crate) enum ParseGetAttrExpressionError {
42+
#[display("No principal id found")]
43+
NoPrincipalId,
4144
#[display("No resource variable found")]
4245
NoResourceVariable,
4346
#[display("Invalid attribute: `{_0}`")]
@@ -160,6 +163,21 @@ impl PolicyExpressionTree {
160163
}
161164
}
162165

166+
fn expect_principal_id(
167+
expr: &Arc<ast::Expr>,
168+
) -> Result<(), Report<ParseGetAttrExpressionError>> {
169+
match expr.expr_kind() {
170+
ast::ExprKind::GetAttr { expr, attr } => match (expr.expr_kind(), attr.as_str()) {
171+
(ast::ExprKind::Var(ast::Var::Principal), "id") => Ok(()),
172+
(ast::ExprKind::Unknown(unknown), "id") if unknown.name == "principal" => Ok(()),
173+
_ => Err(Report::new(ParseGetAttrExpressionError::NoPrincipalId)
174+
.attach_printable(Arc::clone(expr))),
175+
},
176+
_ => Err(Report::new(ParseGetAttrExpressionError::NoPrincipalId)
177+
.attach_printable(Arc::clone(expr))),
178+
}
179+
}
180+
163181
fn expect_resource_variable(
164182
expr: &Arc<ast::Expr>,
165183
) -> Result<(), Report<ParseGetAttrExpressionError>> {
@@ -197,13 +215,15 @@ impl PolicyExpressionTree {
197215
BaseUrl,
198216
OntologyTypeVersion,
199217
Resource,
218+
CreatedBy,
200219
}
201220

202221
let attribute_type = match lhs.expr_kind() {
203222
ast::ExprKind::GetAttr { expr, attr } => {
204223
let attr_type = match attr.as_str() {
205224
"base_url" => Ok(AttributeType::BaseUrl),
206225
"ontology_type_version" => Ok(AttributeType::OntologyTypeVersion),
226+
"created_by" => Ok(AttributeType::CreatedBy),
207227
_ => Err(Report::new(ParseGetAttrExpressionError::InvalidAttribute(
208228
attr.clone(),
209229
))),
@@ -237,6 +257,9 @@ impl PolicyExpressionTree {
237257
.change_context(ParseBinaryExpressionError::Right)
238258
.map(|version| Self::OntologyTypeVersion(OntologyTypeVersion::new(version)))
239259
}
260+
(AttributeType::CreatedBy, _) => Self::expect_principal_id(rhs)
261+
.change_context(ParseBinaryExpressionError::Right)
262+
.map(|()| Self::CreatedByPrincipal),
240263
(AttributeType::Resource, ast::ExprKind::Lit(ast::Literal::EntityUID(euid))) => {
241264
if *euid.entity_type() == **EntityTypeId::entity_type() {
242265
EntityTypeId::from_eid(euid.eid())

libs/@local/graph/authorization/src/policies/cedar/mod.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod actor;
12
mod entity;
23
mod expression_tree;
34
mod ontology;
@@ -12,6 +13,7 @@ use error_stack::{IntoReport, Report, ResultExt as _};
1213

1314
pub use self::expression_tree::PolicyExpressionTree;
1415
pub(crate) use self::{
16+
actor::ActorIdVisitor,
1517
entity::EntityUuidVisitor,
1618
ontology::{BaseUrlVisitor, EntityTypeIdVisitor, OntologyTypeVersionVisitor},
1719
visitor::{
@@ -22,8 +24,21 @@ pub(crate) use self::{
2224
};
2325
use crate::policies::error::FromCedarRefernceError;
2426

27+
pub(crate) trait ToCedarRestrictedExpr {
28+
fn to_cedar_restricted_expr(&self) -> ast::RestrictedExpr;
29+
}
30+
2531
pub(crate) trait ToCedarExpr {
26-
fn to_cedar(&self) -> ast::Expr;
32+
fn to_cedar_expr(&self) -> ast::Expr;
33+
}
34+
35+
impl<T> ToCedarExpr for T
36+
where
37+
T: ToCedarRestrictedExpr,
38+
{
39+
fn to_cedar_expr(&self) -> ast::Expr {
40+
self.to_cedar_restricted_expr().into()
41+
}
2742
}
2843

2944
pub(crate) trait FromCedarExpr: Sized {

libs/@local/graph/authorization/src/policies/components.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ where
134134
.build_principal_context(actor_id, &mut self.context)
135135
.await
136136
.change_context(ContextCreationError::BuildPrincipalContext { actor_id })?;
137+
} else {
138+
self.context.add_public_actor();
137139
}
138140

139141
let entity_resources;

libs/@local/graph/authorization/src/policies/context.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use type_system::principal::{Actor, ActorGroup, Role};
1111
use super::{
1212
PolicyValidator,
1313
cedar::ToCedarEntity as _,
14+
principal::actor::PublicActor,
1415
resource::{EntityResource, EntityTypeResource},
1516
};
1617

@@ -52,6 +53,14 @@ impl ContextBuilder {
5253
self.entities.push(actor.to_cedar_entity());
5354
}
5455

56+
/// Adds the public actor to the context for policy evaluation.
57+
///
58+
/// This allows the actor to be identified as a principal during authorization,
59+
/// making it available for matching against principal constraints in policies.
60+
pub fn add_public_actor(&mut self) {
61+
self.entities.push(PublicActor.to_cedar_entity());
62+
}
63+
5564
/// Adds an actor group to the context for policy evaluation.
5665
///
5766
/// This allows policies associated with the actor group to be considered during authorization,

0 commit comments

Comments
 (0)