Skip to content

Commit

Permalink
H-1587, H-1949, H-2103: Better type permission checks, hide warning w…
Browse files Browse the repository at this point in the history
…hen creating link types (#3947)

Co-authored-by: Ben Werner <[email protected]>
  • Loading branch information
CiaranMn and benwerner01 authored Feb 2, 2024
1 parent 6aa150c commit 1debe5c
Show file tree
Hide file tree
Showing 24 changed files with 316 additions and 112 deletions.
38 changes: 37 additions & 1 deletion apps/hash-api/src/graph/ontology/primitive/entity-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
zeroedGraphResolveDepths,
} from "@local/hash-isomorphic-utils/graph-queries";
import { generateTypeId } from "@local/hash-isomorphic-utils/ontology-types";
import { ConstructEntityTypeParams } from "@local/hash-isomorphic-utils/types";
import {
ConstructEntityTypeParams,
UserPermissionsOnEntityType,
} from "@local/hash-isomorphic-utils/types";
import {
EntityTypeAuthorizationRelationship,
EntityTypeMetadata,
Expand All @@ -35,6 +38,7 @@ import {
mapGraphApiSubgraphToSubgraph,
} from "@local/hash-subgraph/stdlib";

import { publicUserAccountId } from "../../../auth/public-user-account-id";
import { ImpureGraphFunction } from "../../context-types";
import { getWebShortname, isExternalTypeId } from "./util";

Expand Down Expand Up @@ -79,6 +83,38 @@ export const checkEntityTypePermission: ImpureGraphFunction<
.checkEntityTypePermission(actorId, params.entityTypeId, params.permission)
.then(({ data }) => data.has_permission);

export const checkPermissionsOnEntityType: ImpureGraphFunction<
{ entityTypeId: VersionedUrl },
Promise<UserPermissionsOnEntityType>
> = async (graphContext, { actorId }, params) => {
const { entityTypeId } = params;

const isPublicUser = actorId === publicUserAccountId;

const [canUpdate, canInstantiateEntities] = await Promise.all([
isPublicUser
? false
: await checkEntityTypePermission(
graphContext,
{ actorId },
{ entityTypeId, permission: "update" },
),
isPublicUser
? false
: await checkEntityTypePermission(
graphContext,
{ actorId },
{ entityTypeId, permission: "instantiate" },
),
]);

return {
edit: canUpdate,
instantiate: canInstantiateEntities,
view: true,
};
};

/**
* Create an entity type.
*
Expand Down
2 changes: 2 additions & 0 deletions apps/hash-api/src/graphql/resolvers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { loggedInAndSignedUpMiddleware } from "./middlewares/logged-in-and-signe
import { getDataType, queryDataTypes } from "./ontology/data-type";
import {
archiveEntityTypeResolver,
checkUserPermissionsOnEntityTypeResolver,
createEntityTypeResolver,
getEntityTypeResolver,
queryEntityTypesResolver,
Expand Down Expand Up @@ -122,6 +123,7 @@ export const resolvers: Omit<Resolvers, "Query" | "Mutation"> & {
),
checkUserPermissionsOnEntity: (_, { metadata }, context, info) =>
checkUserPermissionsOnEntity({ metadata }, _, context, info),
checkUserPermissionsOnEntityType: checkUserPermissionsOnEntityTypeResolver,
hasAccessToHash: loggedInMiddleware(hasAccessToHashResolver),
},

Expand Down
11 changes: 11 additions & 0 deletions apps/hash-api/src/graphql/resolvers/ontology/entity-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
fullTransactionTimeAxis,
zeroedGraphResolveDepths,
} from "@local/hash-isomorphic-utils/graph-queries";
import { UserPermissionsOnEntityType } from "@local/hash-isomorphic-utils/types";
import {
EntityTypeRootType,
EntityTypeWithMetadata,
Expand All @@ -14,6 +15,7 @@ import { mapGraphApiSubgraphToSubgraph } from "@local/hash-subgraph/stdlib";

import {
archiveEntityType,
checkPermissionsOnEntityType,
createEntityType,
getEntityTypeSubgraphById,
unarchiveEntityType,
Expand All @@ -24,6 +26,7 @@ import {
MutationCreateEntityTypeArgs,
MutationUnarchiveEntityTypeArgs,
MutationUpdateEntityTypeArgs,
QueryCheckUserPermissionsOnEntityTypeArgs,
QueryGetEntityTypeArgs,
QueryQueryEntityTypesArgs,
ResolverFn,
Expand Down Expand Up @@ -197,6 +200,14 @@ export const updateEntityTypeResolver: ResolverFn<
},
);

export const checkUserPermissionsOnEntityTypeResolver: ResolverFn<
Promise<UserPermissionsOnEntityType>,
Record<string, never>,
LoggedInGraphQLContext,
QueryCheckUserPermissionsOnEntityTypeArgs
> = async (_, params, { dataSources, authentication }) =>
checkPermissionsOnEntityType(dataSources, authentication, params);

export const archiveEntityTypeResolver: ResolverFn<
Promise<OntologyTemporalMetadata>,
Record<string, never>,
Expand Down
54 changes: 29 additions & 25 deletions apps/hash-frontend/src/components/grid/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,22 @@ export type GridProps<T extends Row & { rowId: string }> = Omit<
| "rows"
| "onCellEdited"
> & {
columns: SizedGridColumn[];
columnFilters?: ColumnFilter<string, T>[];
columns: SizedGridColumn[];
createGetCellContent: (rows: T[]) => (cell: Item) => GridCell;
createOnCellEdited?: (rows: T[]) => DataEditorProps["onCellEdited"];
currentlyDisplayedRowsRef?: MutableRefObject<T[] | null>;
dataLoading: boolean;
enableCheckboxSelection?: boolean;
selectedRows?: T[];
onSelectedRowsChange?: (selectedRows: T[]) => void;
rows?: T[];
resizable?: boolean;
sortable?: boolean;
initialSortedColumnKey?: string;
firstColumnLeftPadding?: number;
gridRef?: Ref<DataEditorRef>;
currentlyDisplayedRowsRef?: MutableRefObject<T[] | null>;
createGetCellContent: (rows: T[]) => (cell: Item) => GridCell;
createOnCellEdited?: (rows: T[]) => DataEditorProps["onCellEdited"];
initialSortedColumnKey?: string;
onSelectedRowsChange?: (selectedRows: T[]) => void;
resizable?: boolean;
rows?: T[];
selectedRows?: T[];
sortRows?: (rows: T[], sort: ColumnSort<string>) => T[];
sortable?: boolean;
};

const gridHeaderHeight = 42;
Expand All @@ -80,24 +81,25 @@ export const gridRowHeight = 42;
export const gridHorizontalScrollbarHeight = 17;

export const Grid = <T extends Row & { rowId: string }>({
createGetCellContent,
createOnCellEdited,
columnFilters,
columns,
currentlyDisplayedRowsRef,
customRenderers,
onVisibleRegionChanged,
dataLoading,
drawHeader,
columns,
rows,
firstColumnLeftPadding,
enableCheckboxSelection = false,
firstColumnLeftPadding,
gridRef,
initialSortedColumnKey,
onSelectedRowsChange,
onVisibleRegionChanged,
resizable = true,
rows,
selectedRows,
sortable = true,
columnFilters,
initialSortedColumnKey,
createGetCellContent,
sortRows,
gridRef,
createOnCellEdited,
selectedRows,
currentlyDisplayedRowsRef,
onSelectedRowsChange,
...rest
}: GridProps<T>) => {
useRenderGridPortal();
Expand Down Expand Up @@ -363,18 +365,20 @@ export const Grid = <T extends Row & { rowId: string }>({
});
}, [columns, columnSizes]);

const emptyStateText = dataLoading ? "Loading..." : "No results";

const getSkeletonCellContent = useCallback(
([colIndex]: Item): TextCell => ({
kind: GridCellKind.Text,
displayData: colIndex === 0 ? "Loading..." : "",
data: colIndex === 0 ? "Loading..." : "",
displayData: colIndex === 0 ? emptyStateText : "",
data: colIndex === 0 ? emptyStateText : "",
allowOverlay: false,
themeOverride: {
cellHorizontalPadding: 15,
},
style: "faded",
}),
[],
[emptyStateText],
);

const wrapperRef = useRef<HTMLDivElement>(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,9 @@ export const unarchiveEntityTypeMutation = gql`
unarchiveEntityType(entityTypeId: $entityTypeId)
}
`;

export const checkUserPermissionsOnEntityTypeQuery = gql`
query checkUserPermissionsOnEntityType($entityTypeId: VersionedUrl!) {
checkUserPermissionsOnEntityType(entityTypeId: $entityTypeId)
}
`;
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { VersionedUrl } from "@blockprotocol/type-system";
import { AlertModal } from "@hashintel/design-system";
import { generateEntityLabel } from "@local/hash-isomorphic-utils/generate-entity-label";
import { blockProtocolEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids";
import {
EntityPropertiesObject,
extractEntityUuidFromEntityId,
} from "@local/hash-subgraph";
import { getRoots } from "@local/hash-subgraph/stdlib";
import { Typography } from "@mui/material";
import { useRouter } from "next/router";
import { useContext, useState } from "react";

import { useBlockProtocolCreateEntity } from "../../../../components/hooks/block-protocol-functions/knowledge/use-block-protocol-create-entity";
import { PageErrorState } from "../../../../components/page-error-state";
import { Link } from "../../../../shared/ui/link";
import { WorkspaceContext } from "../../../shared/workspace-context";
import { EditBar } from "../../shared/edit-bar";
import { EntityEditorPage } from "./entity-editor-page";
Expand All @@ -28,6 +31,8 @@ export const CreateEntityPage = ({ entityTypeId }: CreateEntityPageProps) => {
const router = useRouter();
const applyDraftLinkEntityChanges = useApplyDraftLinkEntityChanges();

const [errorMessage, setErrorMessage] = useState<string | null>(null);

const [
draftLinksToCreate,
setDraftLinksToCreate,
Expand Down Expand Up @@ -89,6 +94,8 @@ export const CreateEntityPage = ({ entityTypeId }: CreateEntityPageProps) => {
);

void router.push(`/@${activeWorkspace.shortname}/entities/${entityId}`);
} catch (err) {
setErrorMessage((err as Error).message);
} finally {
setCreating(false);
}
Expand All @@ -108,39 +115,54 @@ export const CreateEntityPage = ({ entityTypeId }: CreateEntityPageProps) => {
entityTypeId === blockProtocolEntityTypes.query.entityTypeId;

return (
<EntityEditorPage
editBar={
<EditBar
label="- this entity has not been created yet"
visible
discardButtonProps={{
href: "/new/entity",
children: "Discard entity",
}}
confirmButtonProps={{
onClick: () => handleCreateEntity(),
loading: creating,
children: "Create entity",
}}
/>
}
entityLabel={entityLabel}
entityUuid="draft"
owner={`@${activeWorkspace?.shortname}`}
isQueryEntity={isQueryEntity}
isDirty
isDraft
handleSaveChanges={handleCreateEntity}
setEntity={(entity) => {
updateEntitySubgraphStateByEntity(entity, setDraftEntitySubgraph);
}}
draftLinksToCreate={draftLinksToCreate}
setDraftLinksToCreate={setDraftLinksToCreate}
draftLinksToArchive={draftLinksToArchive}
setDraftLinksToArchive={setDraftLinksToArchive}
entitySubgraph={draftEntitySubgraph}
readonly={false}
replaceWithLatestDbVersion={async () => {}}
/>
<>
{errorMessage && (
<AlertModal
calloutMessage={errorMessage}
close={() => setErrorMessage("")}
header="Couldn't create entity"
type="warning"
>
<Typography>
Please <Link href="https://hash.ai/contact">contact us</Link> and
tell us what entity you were trying to create when this happened
</Typography>
</AlertModal>
)}
<EntityEditorPage
editBar={
<EditBar
label="- this entity has not been created yet"
visible
discardButtonProps={{
href: "/new/entity",
children: "Discard entity",
}}
confirmButtonProps={{
onClick: () => handleCreateEntity(),
loading: creating,
children: "Create entity",
}}
/>
}
entityLabel={entityLabel}
entityUuid="draft"
owner={`@${activeWorkspace?.shortname}`}
isQueryEntity={isQueryEntity}
isDirty
isDraft
handleSaveChanges={handleCreateEntity}
setEntity={(entity) => {
updateEntitySubgraphStateByEntity(entity, setDraftEntitySubgraph);
}}
draftLinksToCreate={draftLinksToCreate}
setDraftLinksToCreate={setDraftLinksToCreate}
draftLinksToArchive={draftLinksToArchive}
setDraftLinksToArchive={setDraftLinksToArchive}
entitySubgraph={draftEntitySubgraph}
readonly={false}
replaceWithLatestDbVersion={async () => {}}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const LinkTable = ({ showSearch, onSearchClose }: LinkTableProps) => {
columns={linkGridColumns}
rows={rows}
createGetCellContent={createGetCellContent}
dataLoading={false}
drawCell={drawCell}
showSearch={showSearch}
onSearchClose={onSearchClose}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const PropertyTable = ({
columns={propertyGridColumns}
createGetCellContent={createGetCellContent}
createOnCellEdited={createOnCellEdited}
dataLoading={false}
rows={rows}
showSearch={showSearch}
onSearchClose={onSearchClose}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { validateEntityType } from "@blockprotocol/type-system";
import { EntityType } from "@blockprotocol/type-system/slim";
import { OwnedById } from "@local/hash-subgraph";
import { Buffer } from "buffer/";
import { useRouter } from "next/router";
import { useMemo } from "react";
Expand All @@ -9,7 +8,6 @@ import {
getLayoutWithSidebar,
NextPageWithLayout,
} from "../../../../shared/layout";
import { useIsReadonlyModeForType } from "../../../../shared/readonly-mode";
import { EntityTypePage } from "../../../shared/entity-type-page";
import { useRouteNamespace } from "../../shared/use-route-namespace";
import { getEntityTypeBaseUrl } from "./[...slug-maybe-version].page/get-entity-type-base-url";
Expand Down Expand Up @@ -54,10 +52,6 @@ const Page: NextPageWithLayout = () => {
? parseInt(requestedVersionString, 10)
: null;

const userUnauthorized = useIsReadonlyModeForType(
routeNamespace?.accountId as OwnedById,
);

if (!routeNamespace) {
if (loadingNamespace) {
return null;
Expand All @@ -72,7 +66,6 @@ const Page: NextPageWithLayout = () => {
draftEntityType={draftEntityType}
entityTypeBaseUrl={entityTypeBaseUrl}
requestedVersion={requestedVersion}
readonly={userUnauthorized}
/>
);
};
Expand Down
Loading

0 comments on commit 1debe5c

Please sign in to comment.