Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dc3f49d
BE-310: Add filter protection to prevent email enumeration attacks
TimDiekmann Jan 24, 2026
8e552b5
BE-310: Use Filter::In with TypeBaseUrls for multi-type entity support
TimDiekmann Jan 25, 2026
10fbd57
Update Rust formatting command in lefthook pre-commit hook
TimDiekmann Jan 26, 2026
5ef15ef
BE-310: Fix clippy warnings in filter protection code
TimDiekmann Jan 26, 2026
dbbff23
BE-310: Exclude email from embedding generation for User entities
TimDiekmann Jan 26, 2026
b6c0333
BE-310: Skip filter transformation when protection config is empty
TimDiekmann Jan 27, 2026
9bea7c9
BE-310: Add multi-property and multi-type filter protection tests
TimDiekmann Jan 27, 2026
d3eed40
BE-310: Add truth tables for multi-property and multi-type tests
TimDiekmann Jan 27, 2026
200ffc4
BE-310: Fix clippy warnings in filter protection tests
TimDiekmann Jan 27, 2026
06377a4
BE-310: Add ActorId support for self-exclusion bypass in filter prote…
TimDiekmann Jan 28, 2026
2ae379a
BE-310: Add property masking in SELECT statements
TimDiekmann Jan 28, 2026
9561f9d
WIP: BE-310 embedding filtering
TimDiekmann Jan 28, 2026
9d533e8
BE-310: Remove email masking from Node API and optimize getUser
TimDiekmann Jan 30, 2026
014d9a2
BE-310: Bypass filter protection for instance admins
TimDiekmann Jan 30, 2026
062b897
Remove excludedProperties handling from AI embedding generation
TimDiekmann Jan 30, 2026
7e92ee4
Remove unused imports from hash-ai-worker-ts activities and workflows
TimDiekmann Jan 30, 2026
d904dd1
Remove outdated doc comment and enhance property protection examples in
TimDiekmann Jan 30, 2026
d90e8c3
Fix error handling in getWebShortname to return null on failure
TimDiekmann Jan 30, 2026
7a7cc8a
Verify entity type in getUser to ensure it is a User
TimDiekmann Jan 30, 2026
a4e3d64
User `Parameter` over `Constant`s for filtering in properties
TimDiekmann Jan 31, 2026
cd65b3b
Fix syntax error in SELECT query parameter casting
TimDiekmann Jan 31, 2026
56cc104
Remove unused property filter compilation methods from Postgres query
TimDiekmann Feb 1, 2026
cc97347
Use snapshot settings when creating Postgres connection pool
TimDiekmann Feb 1, 2026
9c77148
Remove unused imports in Postgres query compilation code
TimDiekmann Feb 1, 2026
d90a8e1
Apply property masking to traversed entities in subgraph queries
TimDiekmann Feb 1, 2026
e32d415
Fix property masking order in subgraph traversal
TimDiekmann Feb 1, 2026
625afdd
Extract user entity type check into isUserEntity helper function
TimDiekmann Feb 1, 2026
4263f1b
Use type-only imports in workflows.ts for clarity
TimDiekmann Feb 1, 2026
2f45f6f
Remove private IP exceptions from Kratos dev config for webhooks
TimDiekmann Feb 1, 2026
9806505
Initialize email property metadata in getUser function
TimDiekmann Feb 4, 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
2 changes: 1 addition & 1 deletion .lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pre-commit:
rust:
tags: [backend, style]
glob: "*.rs"
run: cargo fmt -- {staged_files} || true
run: rustfmt {staged_files} || true
Comment thread
indietyp marked this conversation as resolved.
stage_fixed: true
toml:
tags: [backend, style]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1076,11 +1076,7 @@ describe("The dereferenceEntityType function", () => {
"https://hash.ai/@test/types/entity-type/property-values-demo/v/4",
subgraph: {
...testSubgraph,
vertices: mapGraphApiVerticesToVertices(
testSubgraph.vertices,
null,
true,
),
vertices: mapGraphApiVerticesToVertices(testSubgraph.vertices),
} as Subgraph,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ import type {
CosineDistanceFilter,
GraphApi,
} from "@local/hash-graph-client";
import { type HashEntity, queryEntities } from "@local/hash-graph-sdk/entity";
import { HashEntity, queryEntities } from "@local/hash-graph-sdk/entity";
import { queryEntityTypeSubgraph } from "@local/hash-graph-sdk/entity-type";
import { mapGraphApiEntityToEntity } from "@local/hash-graph-sdk/subgraph";
import type { ProposedEntity } from "@local/hash-isomorphic-utils/flows/types";
import {
almostFullOntologyResolveDepths,
Expand Down Expand Up @@ -213,9 +212,7 @@ export const findExistingEntity = async ({
includePermissions: false,
},
).then(({ entities }) =>
entities
.slice(0, 3)
.map((entity) => mapGraphApiEntityToEntity(entity, actorId)),
entities.slice(0, 3).map((entity) => new HashEntity(entity)),
);
}

Expand Down Expand Up @@ -251,9 +248,7 @@ export const findExistingEntity = async ({
includePermissions: false,
},
).then(({ entities }) =>
entities
.slice(0, 3)
.map((entity) => mapGraphApiEntityToEntity(entity, actorId)),
entities.slice(0, 3).map((entity) => new HashEntity(entity)),
);
}
}
Expand Down
12 changes: 5 additions & 7 deletions apps/hash-api/src/auth/create-auth-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ import type { Express, Request, RequestHandler } from "express";

import type { ImpureGraphContext } from "../graph/context-types";
import type { User } from "../graph/knowledge/system-types/user";
import {
createUser,
getUserByKratosIdentityId,
} from "../graph/knowledge/system-types/user";
import { createUser, getUser } from "../graph/knowledge/system-types/user";
import { systemAccountId } from "../graph/system-account";
import { hydraAdmin } from "./ory-hydra";
import type { KratosUserIdentity } from "./ory-kratos";
Expand Down Expand Up @@ -140,10 +137,11 @@ export const getUserAndSession = async ({
throw new Error("Could not find kratos identity for session");
}

const { id: kratosIdentityId } = identity;
const { id: kratosIdentityId, traits } = identity as KratosUserIdentity;

const user = await getUserByKratosIdentityId(context, authentication, {
const user = await getUser(context, authentication, {
kratosIdentityId,
emails: traits.emails,
});

if (!user) {
Expand Down Expand Up @@ -179,7 +177,7 @@ export const createAuthMiddleware = (params: {
token: accessOrSessionToken,
});
if (introspectionResult.data.active && introspectionResult.data.sub) {
const user = await getUserByKratosIdentityId(
const user = await getUser(
context,
{ actorId: publicUserAccountId },
{
Expand Down
2 changes: 1 addition & 1 deletion apps/hash-api/src/auth/ory-kratos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const kratosIdentityApi = new IdentityApi(

export type KratosUserIdentityTraits = {
shortname?: string;
emails: string[];
emails: [string, ...string[]];
};

export type KratosUserIdentity = Omit<Identity, "traits"> & {
Expand Down
30 changes: 20 additions & 10 deletions apps/hash-api/src/graph/knowledge/primitive/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ export const countEntities: ImpureGraphFunction<
> = async ({ graphApi }, { actorId }, params) =>
graphApi.countEntities(actorId, params).then(({ data }) => data);

type GetLatestEntityByIdFunction<
Properties extends
TypeIdsAndPropertiesForEntity = TypeIdsAndPropertiesForEntity,
> = ImpureGraphFunction<
{
entityId: EntityId;
},
Promise<HashEntity<Properties>>
>;

/**
* Get the latest edition of an entity by its entityId. See notes on params.
*
Expand All @@ -178,13 +188,13 @@ export const countEntities: ImpureGraphFunction<
* 2. if there is somehow more than one edition for the requested entityId at the current time, which is an internal
* fault
*/
export const getLatestEntityById: ImpureGraphFunction<
{
entityId: EntityId;
},
Promise<HashEntity>
> = async (context, authentication, params) => {
const { entityId } = params;
export const getLatestEntityById = async <
Properties extends
TypeIdsAndPropertiesForEntity = TypeIdsAndPropertiesForEntity,
>(
...args: Parameters<GetLatestEntityByIdFunction<Properties>>
): ReturnType<GetLatestEntityByIdFunction<Properties>> => {
const [context, authentication, { entityId }] = args;

const [webId, entityUuid, draftId] = splitEntityId(entityId);

Expand Down Expand Up @@ -223,7 +233,7 @@ export const getLatestEntityById: ImpureGraphFunction<

const {
entities: [entity, ...unexpectedEntities],
} = await queryEntities(context, authentication, {
} = await queryEntities<Properties>(context, authentication, {
filter: {
all: allFilter,
},
Expand Down Expand Up @@ -368,9 +378,9 @@ export const createEntityWithLinks = async <
* pages which may affect this.
*/
const entity = existingEntityId
? ((await getLatestEntityById(context, authentication, {
? await getLatestEntityById<Properties>(context, authentication, {
entityId: existingEntityId,
})) as HashEntity<Properties>)
})
: await createEntity<Properties>(context, authentication, {
...createParams,
properties: definition.entityProperties!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
getMentionedUsersInTextualContent,
getTextById,
} from "../../system-types/text";
import { getUserById } from "../../system-types/user";
import { getUser } from "../../system-types/user";
import { checkPermissionsOnEntity } from "../entity";
import type {
AfterCreateEntityHook,
Expand Down Expand Up @@ -79,12 +79,18 @@ const commentCreateHookCallback: AfterCreateEntityHookCallback = async ({
const pageAuthorAccountId =
occurredInEntity.entity.metadata.provenance.createdById;

const pageAuthor = await getUserById(context, authentication, {
entityId: entityIdFromComponents(
pageAuthorAccountId as WebId,
pageAuthorAccountId as string as EntityUuid,
),
const pageAuthorEntityId = entityIdFromComponents(
pageAuthorAccountId as WebId,
pageAuthorAccountId as string as EntityUuid,
);
const pageAuthor = await getUser(context, authentication, {
entityId: pageAuthorEntityId,
});
if (!pageAuthor) {
throw new Error(
`User with entityId ${pageAuthorEntityId} doesn't exist or cannot be accessed by requesting user.`,
);
}

const commentAuthor = await getCommentAuthor(context, authentication, {
commentEntityId: comment.entity.metadata.recordId.entityId,
Expand Down Expand Up @@ -216,12 +222,18 @@ const hasTextCreateHookCallback: AfterCreateEntityHookCallback = async ({
{ textualContent },
);

const triggeredByUser = await getUserById(context, authentication, {
entityId: entityIdFromComponents(
authentication.actorId as WebId,
authentication.actorId as string as EntityUuid,
),
const triggeredByUserEntityId = entityIdFromComponents(
authentication.actorId as WebId,
authentication.actorId as string as EntityUuid,
);
const triggeredByUser = await getUser(context, authentication, {
entityId: triggeredByUserEntityId,
});
if (!triggeredByUser) {
throw new Error(
`User with entityId ${triggeredByUserEntityId} doesn't exist or cannot be accessed by requesting user.`,
);
}

await Promise.all([
...mentionedUsers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
getMentionedUsersInTextualContent,
getTextFromEntity,
} from "../../../system-types/text";
import { getUserById } from "../../../system-types/user";
import { getUser } from "../../../system-types/user";
import { checkPermissionsOnEntity } from "../../entity";
import { getTextUpdateOccurredIn } from "../shared/mention-notification";
import type { AfterUpdateEntityHookCallback } from "../update-entity-hooks";
Expand Down Expand Up @@ -79,13 +79,21 @@ export const textAfterUpdateEntityHookCallback: AfterUpdateEntityHookCallback =
),
);

const triggeredByUser = await getUserById(context, authentication, {
entityId: entityIdFromComponents(
authentication.actorId as WebId,
authentication.actorId as string as EntityUuid,
),
const triggeredByUserEntityId = entityIdFromComponents(
authentication.actorId as WebId,
authentication.actorId as string as EntityUuid,
);

const triggeredByUser = await getUser(context, authentication, {
entityId: triggeredByUserEntityId,
});

if (!triggeredByUser) {
throw new Error(
`User with entityId ${triggeredByUserEntityId} doesn't exist or cannot be accessed by requesting user.`,
);
}

await Promise.all([
...removedMentionedUsers.map(async (removedMentionedUser) => {
const existingNotification = await getMentionNotification(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
PureGraphFunction,
} from "../../context-types";
import { getOrgByShortname } from "./org";
import { getUserByShortname } from "./user";
import { getUser } from "./user";

/** @todo: enable admins to expand upon restricted shortnames block list */
export const RESTRICTED_SHORTNAMES = [
Expand Down Expand Up @@ -124,7 +124,7 @@ export const shortnameIsTaken: ImpureGraphFunction<
* @see https://linear.app/hash/issue/H-2989
*/
return (
(await getUserByShortname(ctx, authentication, params)) !== null ||
(await getUser(ctx, authentication, params)) !== null ||
(await getOrgByShortname(ctx, authentication, params)) !== null
);
};
Expand Down
10 changes: 7 additions & 3 deletions apps/hash-api/src/graph/knowledge/system-types/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,13 @@ const generateCommonParameters = async (
}

if (fileEntityUpdateInput) {
const existingEntity = (await getLatestEntityById(ctx, authentication, {
entityId: fileEntityUpdateInput.existingFileEntityId,
})) as HashEntity<File>;
const existingEntity = await getLatestEntityById<File>(
ctx,
authentication,
{
entityId: fileEntityUpdateInput.existingFileEntityId,
},
);

return {
existingEntity,
Expand Down
15 changes: 11 additions & 4 deletions apps/hash-api/src/graph/knowledge/system-types/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { getCommentById } from "./comment";
import type { Page } from "./page";
import { getPageFromEntity } from "./page";
import type { User } from "./user";
import { getUserById } from "./user";
import { getUser } from "./user";

export type Text = {
textualContent: TextToken[];
Expand Down Expand Up @@ -291,9 +291,16 @@ export const getMentionedUsersInTextualContent: ImpureGraphFunction<
(mention, i, all) =>
all.findIndex(({ entityId }) => entityId === mention.entityId) === i,
)
.map(({ entityId }) =>
getUserById(context, authentication, { entityId }),
),
.map(async ({ entityId }) => {
const user = await getUser(context, authentication, { entityId });

if (!user) {
throw new Error(
`User with entityId ${entityId} doesn't exist or cannot be accessed by requesting user.`,
);
}
return user;
}),
);

return mentionedUsers;
Expand Down
Loading
Loading