Split employee document policy from core document actions

Introduce dedicated employee-scoped IAM actions and update all
resolvers and frontend mutations accordingly.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-01 11:33:40 +02:00
parent bcfc693366
commit 1b811011eb
12 changed files with 1331 additions and 177 deletions

View File

@@ -46,7 +46,7 @@ import type { DocumentApprovePage_approveMutation } from "#/__generated__/core/D
import type { DocumentApprovePage_rejectMutation } from "#/__generated__/core/DocumentApprovePage_rejectMutation.graphql"; import type { DocumentApprovePage_rejectMutation } from "#/__generated__/core/DocumentApprovePage_rejectMutation.graphql";
import type { DocumentApprovePageDecisionFragment$key } from "#/__generated__/core/DocumentApprovePageDecisionFragment.graphql"; import type { DocumentApprovePageDecisionFragment$key } from "#/__generated__/core/DocumentApprovePageDecisionFragment.graphql";
import type { DocumentApprovePageDocumentFragment$key } from "#/__generated__/core/DocumentApprovePageDocumentFragment.graphql"; import type { DocumentApprovePageDocumentFragment$key } from "#/__generated__/core/DocumentApprovePageDocumentFragment.graphql";
import type { DocumentApprovePageExportPDFMutation } from "#/__generated__/core/DocumentApprovePageExportPDFMutation.graphql"; import type { DocumentApprovePageExportEmployeePDFMutation } from "#/__generated__/core/DocumentApprovePageExportEmployeePDFMutation.graphql";
import type { DocumentApprovePageQuery } from "#/__generated__/core/DocumentApprovePageQuery.graphql"; import type { DocumentApprovePageQuery } from "#/__generated__/core/DocumentApprovePageQuery.graphql";
import type { DocumentApprovePageVersionRowFragment$key } from "#/__generated__/core/DocumentApprovePageVersionRowFragment.graphql"; import type { DocumentApprovePageVersionRowFragment$key } from "#/__generated__/core/DocumentApprovePageVersionRowFragment.graphql";
import { PDFPreview } from "#/components/documents/PDFPreview"; import { PDFPreview } from "#/components/documents/PDFPreview";
@@ -100,9 +100,6 @@ const decisionFragment = graphql`
state state
canApprove: permission(action: "core:document-version:approve") canApprove: permission(action: "core:document-version:approve")
canReject: permission(action: "core:document-version:reject") canReject: permission(action: "core:document-version:reject")
documentVersion {
id
}
} }
`; `;
@@ -131,10 +128,10 @@ const rejectDocumentVersionMutation = graphql`
`; `;
const exportPDFMutation = graphql` const exportPDFMutation = graphql`
mutation DocumentApprovePageExportPDFMutation( mutation DocumentApprovePageExportEmployeePDFMutation(
$input: ExportDocumentVersionPDFInput! $input: ExportEmployeeDocumentVersionPDFInput!
) { ) {
exportDocumentVersionPDF(input: $input) { exportEmployeeDocumentVersionPDF(input: $input) {
data data
} }
} }
@@ -227,9 +224,10 @@ function VersionRow({
function ViewerDecision(props: { function ViewerDecision(props: {
fragmentRef: DocumentApprovePageDecisionFragment$key; fragmentRef: DocumentApprovePageDecisionFragment$key;
versionId: string;
onBack: () => void; onBack: () => void;
}) { }) {
const { fragmentRef, onBack } = props; const { fragmentRef, versionId, onBack } = props;
const { __ } = useTranslate(); const { __ } = useTranslate();
const decision = useFragment(decisionFragment, fragmentRef); const decision = useFragment(decisionFragment, fragmentRef);
const rejectDialogRef = useDialogRef(); const rejectDialogRef = useDialogRef();
@@ -311,7 +309,7 @@ function ViewerDecision(props: {
approveVersion({ approveVersion({
variables: { variables: {
input: { input: {
documentVersionId: decision.documentVersion.id, documentVersionId: versionId,
}, },
}, },
onCompleted(_, errors) { onCompleted(_, errors) {
@@ -372,7 +370,7 @@ function ViewerDecision(props: {
rejectVersion({ rejectVersion({
variables: { variables: {
input: { input: {
documentVersionId: decision.documentVersion.id, documentVersionId: versionId,
comment: rejectComment || undefined, comment: rejectComment || undefined,
}, },
}, },
@@ -434,7 +432,7 @@ function DocumentApproveContent({
usePageTitle(__("Review and Approve Document")); usePageTitle(__("Review and Approve Document"));
const [exportPDF] = useMutation<DocumentApprovePageExportPDFMutation>( const [exportPDF] = useMutation<DocumentApprovePageExportEmployeePDFMutation>(
exportPDFMutation, exportPDFMutation,
); );
@@ -448,8 +446,6 @@ function DocumentApproveContent({
variables: { variables: {
input: { input: {
documentVersionId: selectedVersion.id, documentVersionId: selectedVersion.id,
withWatermark: true,
withSignatures: false,
}, },
}, },
onCompleted: (data, errors): void => { onCompleted: (data, errors): void => {
@@ -464,8 +460,8 @@ function DocumentApproveContent({
}); });
return; return;
} }
if (data.exportDocumentVersionPDF?.data) { if (data.exportEmployeeDocumentVersionPDF?.data) {
const dataUrl = data.exportDocumentVersionPDF.data; const dataUrl = data.exportEmployeeDocumentVersionPDF.data;
pdfUrlRef.current = dataUrl; pdfUrlRef.current = dataUrl;
setPdfUrl(dataUrl); setPdfUrl(dataUrl);
} }
@@ -522,6 +518,7 @@ function DocumentApproveContent({
? ( ? (
<ViewerDecision <ViewerDecision
fragmentRef={decision} fragmentRef={decision}
versionId={selectedVersion.id}
onBack={() => onBack={() =>
void navigate(`/organizations/${organizationId}/employee/approvals`)} void navigate(`/organizations/${organizationId}/employee/approvals`)}
/> />

View File

@@ -28,7 +28,7 @@ import { useNavigate } from "react-router";
import { useWindowSize } from "usehooks-ts"; import { useWindowSize } from "usehooks-ts";
import type { EmployeeDocumentSignaturePageDocumentFragment$key } from "#/__generated__/core/EmployeeDocumentSignaturePageDocumentFragment.graphql"; import type { EmployeeDocumentSignaturePageDocumentFragment$key } from "#/__generated__/core/EmployeeDocumentSignaturePageDocumentFragment.graphql";
import type { EmployeeDocumentSignaturePageExportSignablePDFMutation } from "#/__generated__/core/EmployeeDocumentSignaturePageExportSignablePDFMutation.graphql"; import type { EmployeeDocumentSignaturePageExportEmployeePDFMutation } from "#/__generated__/core/EmployeeDocumentSignaturePageExportEmployeePDFMutation.graphql";
import type { EmployeeDocumentSignaturePageQuery } from "#/__generated__/core/EmployeeDocumentSignaturePageQuery.graphql"; import type { EmployeeDocumentSignaturePageQuery } from "#/__generated__/core/EmployeeDocumentSignaturePageQuery.graphql";
import type { EmployeeDocumentSignaturePageSignMutation } from "#/__generated__/core/EmployeeDocumentSignaturePageSignMutation.graphql"; import type { EmployeeDocumentSignaturePageSignMutation } from "#/__generated__/core/EmployeeDocumentSignaturePageSignMutation.graphql";
import { PDFPreview } from "#/components/documents/PDFPreview"; import { PDFPreview } from "#/components/documents/PDFPreview";
@@ -80,11 +80,11 @@ const signDocumentMutation = graphql`
} }
`; `;
const exportSignableVersionDocumentPDFMutation = graphql` const exportEmployeeDocumentVersionPDFMutation = graphql`
mutation EmployeeDocumentSignaturePageExportSignablePDFMutation( mutation EmployeeDocumentSignaturePageExportEmployeePDFMutation(
$input: ExportSignableDocumentVersionPDFInput! $input: ExportEmployeeDocumentVersionPDFInput!
) { ) {
exportSignableVersionDocumentPDF(input: $input) { exportEmployeeDocumentVersionPDF(input: $input) {
data data
} }
} }
@@ -145,9 +145,9 @@ function DocumentSignatureContent({
signDocumentMutation, signDocumentMutation,
); );
const [exportSignableVersionDocumentPDF] const [exportEmployeeDocumentVersionPDF]
= useMutation<EmployeeDocumentSignaturePageExportSignablePDFMutation>( = useMutation<EmployeeDocumentSignaturePageExportEmployeePDFMutation>(
exportSignableVersionDocumentPDFMutation, exportEmployeeDocumentVersionPDFMutation,
); );
const [pdfUrl, setPdfUrl] = useState<string | null>(null); const [pdfUrl, setPdfUrl] = useState<string | null>(null);
@@ -191,7 +191,7 @@ function DocumentSignatureContent({
useEffect(() => { useEffect(() => {
if (!selectedVersion?.id) return; if (!selectedVersion?.id) return;
exportSignableVersionDocumentPDF({ exportEmployeeDocumentVersionPDF({
variables: { variables: {
input: { input: {
documentVersionId: selectedVersion.id, documentVersionId: selectedVersion.id,
@@ -209,8 +209,8 @@ function DocumentSignatureContent({
}); });
return; return;
} }
if (data.exportSignableVersionDocumentPDF?.data) { if (data.exportEmployeeDocumentVersionPDF?.data) {
const dataUrl = data.exportSignableVersionDocumentPDF.data; const dataUrl = data.exportEmployeeDocumentVersionPDF.data;
pdfUrlRef.current = dataUrl; pdfUrlRef.current = dataUrl;
setPdfUrl(dataUrl); setPdfUrl(dataUrl);
} }
@@ -230,7 +230,7 @@ function DocumentSignatureContent({
return () => { return () => {
pdfUrlRef.current = null; pdfUrlRef.current = null;
}; };
}, [selectedVersion?.id, exportSignableVersionDocumentPDF, toast, __]); }, [selectedVersion?.id, exportEmployeeDocumentVersionPDF, toast, __]);
return ( return (
<div <div

View File

@@ -23,47 +23,10 @@ import (
"go.probo.inc/probo/e2e/internal/testutil" "go.probo.inc/probo/e2e/internal/testutil"
) )
// getOwnerProfileID queries the organization profiles and returns the first one (the owner's).
func getOwnerProfileID(t *testing.T, owner *testutil.Client) string { func getOwnerProfileID(t *testing.T, owner *testutil.Client) string {
t.Helper() t.Helper()
query := ` return owner.GetProfileID().String()
query GetProfiles($orgId: ID!) {
node(id: $orgId) {
... on Organization {
profiles(first: 1) {
edges {
node {
id
}
}
}
}
}
}
`
var result struct {
Node struct {
Profiles struct {
Edges []struct {
Node struct {
ID string `json:"id"`
} `json:"node"`
} `json:"edges"`
} `json:"profiles"`
} `json:"node"`
}
err := owner.Execute(
query,
map[string]any{"orgId": owner.GetOrganizationID().String()},
&result,
)
require.NoError(t, err)
require.NotEmpty(t, result.Node.Profiles.Edges)
return result.Node.Profiles.Edges[0].Node.ID
} }
// createTestDocument creates a document and returns its ID and the document version ID // createTestDocument creates a document and returns its ID and the document version ID

File diff suppressed because it is too large Load Diff

View File

@@ -38,9 +38,11 @@ func generateUniqueID() string {
type TestRole string type TestRole string
const ( const (
RoleOwner TestRole = "OWNER" RoleOwner TestRole = "OWNER"
RoleAdmin TestRole = "ADMIN" RoleAdmin TestRole = "ADMIN"
RoleViewer TestRole = "VIEWER" RoleViewer TestRole = "VIEWER"
RoleEmployee TestRole = "EMPLOYEE"
RoleAuditor TestRole = "AUDITOR"
) )
type Client struct { type Client struct {
@@ -50,6 +52,7 @@ type Client struct {
mailpitBaseURL string mailpitBaseURL string
role TestRole role TestRole
userID gid.GID userID gid.GID
profileID gid.GID
organizationID gid.GID organizationID gid.GID
} }
@@ -129,6 +132,7 @@ func (c *Client) SetupTestUserInOrg(ownerClient *Client) {
// Owner invites user to organization // Owner invites user to organization
profileID, identityID := ownerClient.createUser(email, fullName, coredata.MembershipRole(c.role)) profileID, identityID := ownerClient.createUser(email, fullName, coredata.MembershipRole(c.role))
c.userID = identityID c.userID = identityID
c.profileID = profileID
ownerClient.inviteUser(profileID) ownerClient.inviteUser(profileID)
token := c.getActivationToken(email) token := c.getActivationToken(email)
passwordToken := c.activateUser(token) passwordToken := c.activateUser(token)
@@ -212,6 +216,9 @@ func (c *Client) createOrganization(name string) gid.GID {
Organization struct { Organization struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"organization"` } `json:"organization"`
Profile struct {
ID string `json:"id"`
} `json:"profile"`
} `json:"createOrganization"` } `json:"createOrganization"`
} }
@@ -223,6 +230,11 @@ func (c *Client) createOrganization(name string) gid.GID {
orgID, err := gid.ParseGID(result.CreateOrganization.Organization.ID) orgID, err := gid.ParseGID(result.CreateOrganization.Organization.ID)
require.NoError(c.T, err, "cannot parse organization ID") require.NoError(c.T, err, "cannot parse organization ID")
profileID, err := gid.ParseGID(result.CreateOrganization.Profile.ID)
require.NoError(c.T, err, "cannot parse profile ID")
c.profileID = profileID
return orgID return orgID
} }
@@ -483,6 +495,10 @@ func (c *Client) GetUserID() gid.GID {
return c.userID return c.userID
} }
func (c *Client) GetProfileID() gid.GID {
return c.profileID
}
func (c *Client) GetOrganizationID() gid.GID { func (c *Client) GetOrganizationID() gid.GID {
return c.organizationID return c.organizationID
} }

View File

@@ -17,7 +17,6 @@ package coredata
import ( import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
) )
type ( type (
@@ -25,8 +24,8 @@ type (
query *string query *string
trustCenterVisibilities []TrustCenterVisibility trustCenterVisibilities []TrustCenterVisibility
published *bool published *bool
userEmail *mail.Addr employeeIdentityID *gid.GID
approverIdentityID *gid.GID employeeFilterModes []EmployeeFilterMode
documentTypes []DocumentType documentTypes []DocumentType
classifications []DocumentClassification classifications []DocumentClassification
status []DocumentStatus status []DocumentStatus
@@ -56,13 +55,9 @@ func (f *DocumentFilter) WithPublished(published *bool) *DocumentFilter {
return f return f
} }
func (f *DocumentFilter) WithUserEmail(userEmail *mail.Addr) *DocumentFilter { func (f *DocumentFilter) WithEmployeeIdentityID(identityID *gid.GID, modes ...EmployeeFilterMode) *DocumentFilter {
f.userEmail = userEmail f.employeeIdentityID = identityID
return f f.employeeFilterModes = modes
}
func (f *DocumentFilter) WithApproverIdentityID(identityID *gid.GID) *DocumentFilter {
f.approverIdentityID = identityID
return f return f
} }
@@ -114,12 +109,17 @@ func (f *DocumentFilter) SQLArguments() pgx.NamedArgs {
} }
} }
var employeeFilterModes []string
for _, m := range f.employeeFilterModes {
employeeFilterModes = append(employeeFilterModes, string(m))
}
return pgx.NamedArgs{ return pgx.NamedArgs{
"query": f.query, "query": f.query,
"trust_center_visibilities": visibilities, "trust_center_visibilities": visibilities,
"published": f.published, "published": f.published,
"user_email": f.userEmail, "employee_identity_id": f.employeeIdentityID,
"approver_identity_id": f.approverIdentityID, "employee_filter_modes": employeeFilterModes,
"document_types": documentTypes, "document_types": documentTypes,
"classifications": classifications, "classifications": classifications,
"document_status": status, "document_status": status,
@@ -151,30 +151,30 @@ func (f *DocumentFilter) SQLFragment() string {
END END
AND AND
CASE CASE
WHEN @user_email::text IS NULL THEN TRUE WHEN @employee_identity_id::text IS NULL THEN TRUE
ELSE EXISTS ( ELSE (
SELECT 1 (
FROM document_versions dv 'signature' = ANY(@employee_filter_modes::text[]) AND EXISTS (
INNER JOIN document_version_signatures dvs ON dv.id = dvs.document_version_id SELECT 1
INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id FROM document_versions dv
INNER JOIN identities i ON p.identity_id = i.id INNER JOIN document_version_signatures dvs ON dv.id = dvs.document_version_id
WHERE dv.document_id = documents.id INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id
AND dv.status = 'PUBLISHED' WHERE dv.document_id = documents.id
AND i.email_address = @user_email::CITEXT AND p.identity_id = @employee_identity_id::text
AND dvs.state IN ('REQUESTED', 'SIGNED') AND dvs.state IN ('REQUESTED', 'SIGNED')
) )
END )
AND OR (
CASE 'approval' = ANY(@employee_filter_modes::text[]) AND EXISTS (
WHEN @approver_identity_id::text IS NULL THEN TRUE SELECT 1
ELSE EXISTS ( FROM document_versions dv
SELECT 1 INNER JOIN document_version_approval_quorums dvaq ON dvaq.version_id = dv.id
FROM document_versions dv INNER JOIN document_version_approval_decisions dvad ON dvad.quorum_id = dvaq.id
INNER JOIN document_version_approval_quorums dvaq ON dvaq.version_id = dv.id INNER JOIN iam_membership_profiles p ON dvad.approver_id = p.id
INNER JOIN document_version_approval_decisions dvad ON dvad.quorum_id = dvaq.id WHERE dv.document_id = documents.id
INNER JOIN iam_membership_profiles p ON dvad.approver_id = p.id AND p.identity_id = @employee_identity_id::text
WHERE dv.document_id = documents.id )
AND p.identity_id = @approver_identity_id::text )
) )
END END
AND AND

View File

@@ -17,14 +17,20 @@ package coredata
import ( import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail" )
type EmployeeFilterMode string
const (
EmployeeFilterModeSignature EmployeeFilterMode = "signature"
EmployeeFilterModeApproval EmployeeFilterMode = "approval"
) )
type ( type (
DocumentVersionFilter struct { DocumentVersionFilter struct {
statuses []DocumentVersionStatus statuses []DocumentVersionStatus
userEmail *mail.Addr employeeIdentityID *gid.GID
approverIdentityID *gid.GID employeeFilterModes []EmployeeFilterMode
} }
) )
@@ -37,13 +43,9 @@ func (f *DocumentVersionFilter) WithStatuses(statuses ...DocumentVersionStatus)
return f return f
} }
func (f *DocumentVersionFilter) WithUserEmail(userEmail *mail.Addr) *DocumentVersionFilter { func (f *DocumentVersionFilter) WithEmployeeIdentityID(identityID *gid.GID, modes ...EmployeeFilterMode) *DocumentVersionFilter {
f.userEmail = userEmail f.employeeIdentityID = identityID
return f f.employeeFilterModes = modes
}
func (f *DocumentVersionFilter) WithApproverIdentityID(identityID *gid.GID) *DocumentVersionFilter {
f.approverIdentityID = identityID
return f return f
} }
@@ -53,10 +55,15 @@ func (f *DocumentVersionFilter) SQLArguments() pgx.StrictNamedArgs {
filterStatuses = append(filterStatuses, s.String()) filterStatuses = append(filterStatuses, s.String())
} }
var employeeFilterModes []string
for _, m := range f.employeeFilterModes {
employeeFilterModes = append(employeeFilterModes, string(m))
}
return pgx.StrictNamedArgs{ return pgx.StrictNamedArgs{
"filter_statuses": filterStatuses, "filter_statuses": filterStatuses,
"user_email": f.userEmail, "employee_identity_id": f.employeeIdentityID,
"approver_identity_id": f.approverIdentityID, "employee_filter_modes": employeeFilterModes,
} }
} }
@@ -69,27 +76,26 @@ func (f *DocumentVersionFilter) SQLFragment() string {
) )
AND AND
( (
@user_email::text IS NULL @employee_identity_id::text IS NULL
OR EXISTS ( OR (
SELECT 1 'signature' = ANY(@employee_filter_modes::text[]) AND EXISTS (
FROM document_version_signatures dvs SELECT 1
INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id FROM document_version_signatures dvs
INNER JOIN identities i ON p.identity_id = i.id INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id
WHERE dvs.document_version_id = document_versions.id WHERE dvs.document_version_id = document_versions.id
AND i.email_address = @user_email::CITEXT AND p.identity_id = @employee_identity_id::text
AND dvs.state IN ('REQUESTED', 'SIGNED') AND dvs.state IN ('REQUESTED', 'SIGNED')
)
) )
) OR (
AND 'approval' = ANY(@employee_filter_modes::text[]) AND EXISTS (
( SELECT 1
@approver_identity_id::text IS NULL FROM document_version_approval_quorums dvaq
OR EXISTS ( INNER JOIN document_version_approval_decisions dvad ON dvad.quorum_id = dvaq.id
SELECT 1 INNER JOIN iam_membership_profiles p ON dvad.approver_id = p.id
FROM document_version_approval_quorums dvaq WHERE dvaq.version_id = document_versions.id
INNER JOIN document_version_approval_decisions dvad ON dvad.quorum_id = dvaq.id AND p.identity_id = @employee_identity_id::text
INNER JOIN iam_membership_profiles p ON dvad.approver_id = p.id )
WHERE dvaq.version_id = document_versions.id
AND p.identity_id = @approver_identity_id::text
) )
) )
)` )`

View File

@@ -190,7 +190,6 @@ const (
ActionDocumentVersionGet = "core:document-version:get" ActionDocumentVersionGet = "core:document-version:get"
ActionDocumentVersionList = "core:document-version:list" ActionDocumentVersionList = "core:document-version:list"
ActionDocumentVersionExportPDF = "core:document-version:export-pdf" ActionDocumentVersionExportPDF = "core:document-version:export-pdf"
ActionDocumentVersionExportSignable = "core:document-version:export-signable-pdf"
ActionDocumentVersionSign = "core:document-version:sign" ActionDocumentVersionSign = "core:document-version:sign"
ActionDocumentVersionUpdate = "core:document-version:update" ActionDocumentVersionUpdate = "core:document-version:update"
ActionDocumentVersionDeleteDraft = "core:document-version:delete-draft" ActionDocumentVersionDeleteDraft = "core:document-version:delete-draft"
@@ -203,6 +202,11 @@ const (
ActionDocumentVersionPublish = "core:document-version:publish" ActionDocumentVersionPublish = "core:document-version:publish"
ActionDocumentVersionExport = "core:document-version:export" ActionDocumentVersionExport = "core:document-version:export"
// EmployeeDocument actions
ActionEmployeeDocumentGet = "core:employee-document:get"
ActionEmployeeDocumentList = "core:employee-document:list"
ActionEmployeeDocumentVersionExportPDF = "core:employee-document-version:export-pdf"
// DocumentVersionSignature actions // DocumentVersionSignature actions
ActionDocumentVersionSignatureRequest = "core:document-version-signature:request" ActionDocumentVersionSignatureRequest = "core:document-version-signature:request"
ActionDocumentVersionCancelSignature = "core:document-version-signature:cancel" ActionDocumentVersionCancelSignature = "core:document-version-signature:cancel"

View File

@@ -162,13 +162,18 @@ var ViewerPolicy = policy.NewPolicy(
policy.Allow(ActionCustomDomainGet).WithSID("custom-domain-read").When(organizationCondition), policy.Allow(ActionCustomDomainGet).WithSID("custom-domain-read").When(organizationCondition),
policy.Allow(ActionOrganizationContextGet).WithSID("organization-context-read").When(organizationCondition), policy.Allow(ActionOrganizationContextGet).WithSID("organization-context-read").When(organizationCondition),
policy.Allow( policy.Allow(
ActionDocumentVersionExportPDF, ActionDocumentVersionExportSignable, ActionDocumentVersionSign, ActionDocumentVersionExportPDF, ActionDocumentVersionSign,
).WithSID("document-signing").When(organizationCondition), ).WithSID("document-signing").When(organizationCondition),
policy.Allow( policy.Allow(
ActionDocumentVersionApprove, ActionDocumentVersionReject, ActionDocumentVersionApprove, ActionDocumentVersionReject,
).WithSID("document-approval").When(organizationCondition), ).WithSID("document-approval").When(organizationCondition),
policy.Allow(
ActionEmployeeDocumentGet, ActionEmployeeDocumentList,
ActionEmployeeDocumentVersionExportPDF,
).WithSID("employee-document-access").When(organizationCondition),
policy.Allow( policy.Allow(
ActionProcessingActivityExport, ActionProcessingActivityExport,
ActionDataProtectionImpactAssessmentExport, ActionDataProtectionImpactAssessmentExport,
@@ -219,9 +224,14 @@ var AuditorPolicy = policy.NewPolicy(
).WithSID("entity-read-access").When(organizationCondition), ).WithSID("entity-read-access").When(organizationCondition),
policy.Allow( policy.Allow(
ActionDocumentVersionExportPDF, ActionDocumentVersionExportSignable, ActionDocumentVersionSign, ActionDocumentVersionExportPDF, ActionDocumentVersionSign,
).WithSID("document-signing").When(organizationCondition), ).WithSID("document-signing").When(organizationCondition),
policy.Allow(
ActionEmployeeDocumentGet, ActionEmployeeDocumentList,
ActionEmployeeDocumentVersionExportPDF,
).WithSID("employee-document-access").When(organizationCondition),
policy.Allow( policy.Allow(
ActionStateOfApplicabilityExport, ActionStateOfApplicabilityExport,
).WithSID("soa-export").When(organizationCondition), ).WithSID("soa-export").When(organizationCondition),
@@ -238,20 +248,19 @@ var EmployeePolicy = policy.NewPolicy(
).WithSID("org-basic-access").When(organizationCondition), ).WithSID("org-basic-access").When(organizationCondition),
policy.Allow( policy.Allow(
ActionDocumentGet, ActionDocumentList, ActionEmployeeDocumentGet, ActionEmployeeDocumentList,
).WithSID("document-signing-access").When(organizationCondition), ).WithSID("employee-document-access").When(organizationCondition),
policy.Allow( policy.Allow(
ActionDocumentVersionGet, ActionDocumentVersionList,
ActionDocumentVersionSign, ActionDocumentVersionSign,
ActionDocumentVersionExportSignable, ActionEmployeeDocumentVersionExportPDF,
).WithSID("document-version-signing").When(organizationCondition), ).WithSID("document-version-signing").When(organizationCondition),
policy.Allow( policy.Allow(
ActionDocumentVersionApprovalList, ActionDocumentVersionApprovalList,
ActionDocumentVersionApprove, ActionDocumentVersionApprove,
ActionDocumentVersionReject, ActionDocumentVersionReject,
ActionDocumentVersionExportPDF, ActionEmployeeDocumentVersionExportPDF,
).WithSID("document-version-approval").When(organizationCondition), ).WithSID("document-version-approval").When(organizationCondition),
).WithDescription("Employee access - can sign documents, approve documents, and view internal content") ).WithDescription("Employee access - can sign documents, approve documents, and view internal content")

View File

@@ -3860,9 +3860,9 @@ type Mutation {
exportDocumentVersionPDF( exportDocumentVersionPDF(
input: ExportDocumentVersionPDFInput! input: ExportDocumentVersionPDFInput!
): ExportDocumentVersionPDFPayload! ): ExportDocumentVersionPDFPayload!
exportSignableVersionDocumentPDF( exportEmployeeDocumentVersionPDF(
input: ExportSignableDocumentVersionPDFInput! input: ExportEmployeeDocumentVersionPDFInput!
): ExportSignableDocumentVersionPDFPayload! ): ExportEmployeeDocumentVersionPDFPayload!
exportProcessingActivitiesPDF( exportProcessingActivitiesPDF(
input: ExportProcessingActivitiesPDFInput! input: ExportProcessingActivitiesPDFInput!
): ExportProcessingActivitiesPDFPayload! ): ExportProcessingActivitiesPDFPayload!
@@ -4492,7 +4492,7 @@ input ExportDocumentVersionPDFInput {
withSignatures: Boolean! withSignatures: Boolean!
} }
input ExportSignableDocumentVersionPDFInput { input ExportEmployeeDocumentVersionPDFInput {
documentVersionId: ID! documentVersionId: ID!
} }
@@ -5222,7 +5222,7 @@ type ExportDocumentVersionPDFPayload {
data: String! data: String!
} }
type ExportSignableDocumentVersionPDFPayload { type ExportEmployeeDocumentVersionPDFPayload {
data: String! data: String!
} }

View File

@@ -23,11 +23,11 @@ import (
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
) )
type EmployeeDocumentFilterMode int type EmployeeDocumentFilterMode string
const ( const (
EmployeeDocumentFilterModeSignature EmployeeDocumentFilterMode = iota EmployeeDocumentFilterModeSignature EmployeeDocumentFilterMode = "SIGNATURE"
EmployeeDocumentFilterModeApproval EmployeeDocumentFilterModeApproval EmployeeDocumentFilterMode = "APPROVAL"
) )
type ( type (
@@ -64,6 +64,7 @@ type (
EmployeeDocumentVersion struct { EmployeeDocumentVersion struct {
ID gid.GID ID gid.GID
DocumentID gid.GID
OrganizationID gid.GID OrganizationID gid.GID
Major int Major int
Minor int Minor int

View File

@@ -1564,7 +1564,7 @@ func (r *electronicSignatureResolver) Events(ctx context.Context, obj *types.Ele
// Signed is the resolver for the signed field. // Signed is the resolver for the signed field.
func (r *employeeDocumentResolver) Signed(ctx context.Context, obj *types.EmployeeDocument) (*bool, error) { func (r *employeeDocumentResolver) Signed(ctx context.Context, obj *types.EmployeeDocument) (*bool, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentGet); err != nil { if err := r.authorize(ctx, obj.ID, probo.ActionEmployeeDocumentGet); err != nil {
return nil, err return nil, err
} }
@@ -1586,7 +1586,7 @@ func (r *employeeDocumentResolver) Signed(ctx context.Context, obj *types.Employ
// ApprovalState is the resolver for the approvalState field. // ApprovalState is the resolver for the approvalState field.
func (r *employeeDocumentResolver) ApprovalState(ctx context.Context, obj *types.EmployeeDocument) (*coredata.DocumentVersionApprovalDecisionState, error) { func (r *employeeDocumentResolver) ApprovalState(ctx context.Context, obj *types.EmployeeDocument) (*coredata.DocumentVersionApprovalDecisionState, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentGet); err != nil { if err := r.authorize(ctx, obj.ID, probo.ActionEmployeeDocumentGet); err != nil {
return nil, err return nil, err
} }
@@ -1608,7 +1608,7 @@ func (r *employeeDocumentResolver) ApprovalState(ctx context.Context, obj *types
// Versions is the resolver for the versions field. // Versions is the resolver for the versions field.
func (r *employeeDocumentResolver) Versions(ctx context.Context, obj *types.EmployeeDocument, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionOrderBy) (*types.EmployeeDocumentVersionConnection, error) { func (r *employeeDocumentResolver) Versions(ctx context.Context, obj *types.EmployeeDocument, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionOrderBy) (*types.EmployeeDocumentVersionConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionList); err != nil { if err := r.authorize(ctx, obj.ID, probo.ActionEmployeeDocumentGet); err != nil {
return nil, err return nil, err
} }
@@ -1629,14 +1629,20 @@ func (r *employeeDocumentResolver) Versions(ctx context.Context, obj *types.Empl
identity := authn.IdentityFromContext(ctx) identity := authn.IdentityFromContext(ctx)
versionFilter := coredata.NewDocumentVersionFilter() var filterMode coredata.EmployeeFilterMode
switch obj.FilterMode { switch obj.FilterMode {
case types.EmployeeDocumentFilterModeSignature: case types.EmployeeDocumentFilterModeSignature:
versionFilter = versionFilter.WithUserEmail(&identity.EmailAddress) filterMode = coredata.EmployeeFilterModeSignature
case types.EmployeeDocumentFilterModeApproval: case types.EmployeeDocumentFilterModeApproval:
versionFilter = versionFilter.WithApproverIdentityID(&identity.ID) filterMode = coredata.EmployeeFilterModeApproval
default:
r.logger.ErrorCtx(ctx, "unsupported employee document filter mode", log.String("filter_mode", string(obj.FilterMode)))
return nil, gqlutils.Internal(ctx)
} }
versionFilter := coredata.NewDocumentVersionFilter().
WithEmployeeIdentityID(&identity.ID, filterMode)
versionsPage, err := prb.Documents.ListVersions(ctx, obj.ID, cursor, versionFilter) versionsPage, err := prb.Documents.ListVersions(ctx, obj.ID, cursor, versionFilter)
if err != nil { if err != nil {
r.logger.ErrorCtx(ctx, "cannot list employee document versions", log.Error(err)) r.logger.ErrorCtx(ctx, "cannot list employee document versions", log.Error(err))
@@ -1647,6 +1653,7 @@ func (r *employeeDocumentResolver) Versions(ctx context.Context, obj *types.Empl
for i, v := range versionsPage.Data { for i, v := range versionsPage.Data {
employeeVersions[i] = &types.EmployeeDocumentVersion{ employeeVersions[i] = &types.EmployeeDocumentVersion{
ID: v.ID, ID: v.ID,
DocumentID: obj.ID,
OrganizationID: v.OrganizationID, OrganizationID: v.OrganizationID,
Major: v.Major, Major: v.Major,
Minor: v.Minor, Minor: v.Minor,
@@ -1665,7 +1672,7 @@ func (r *employeeDocumentResolver) Versions(ctx context.Context, obj *types.Empl
// Signed is the resolver for the signed field. // Signed is the resolver for the signed field.
func (r *employeeDocumentVersionResolver) Signed(ctx context.Context, obj *types.EmployeeDocumentVersion) (bool, error) { func (r *employeeDocumentVersionResolver) Signed(ctx context.Context, obj *types.EmployeeDocumentVersion) (bool, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionGet); err != nil { if err := r.authorize(ctx, obj.DocumentID, probo.ActionEmployeeDocumentGet); err != nil {
return false, err return false, err
} }
@@ -1684,7 +1691,7 @@ func (r *employeeDocumentVersionResolver) Signed(ctx context.Context, obj *types
// ApprovalDecision is the resolver for the approvalDecision field. // ApprovalDecision is the resolver for the approvalDecision field.
func (r *employeeDocumentVersionResolver) ApprovalDecision(ctx context.Context, obj *types.EmployeeDocumentVersion) (*types.DocumentVersionApprovalDecision, error) { func (r *employeeDocumentVersionResolver) ApprovalDecision(ctx context.Context, obj *types.EmployeeDocumentVersion) (*types.DocumentVersionApprovalDecision, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionApprovalList); err != nil { if err := r.authorize(ctx, obj.DocumentID, probo.ActionEmployeeDocumentGet); err != nil {
return nil, err return nil, err
} }
@@ -5708,9 +5715,9 @@ func (r *mutationResolver) ExportDocumentVersionPDF(ctx context.Context, input t
}, nil }, nil
} }
// ExportSignableVersionDocumentPDF is the resolver for the exportSignableVersionDocumentPDF field. // ExportEmployeeDocumentVersionPDF is the resolver for the exportEmployeeDocumentVersionPDF field.
func (r *mutationResolver) ExportSignableVersionDocumentPDF(ctx context.Context, input types.ExportSignableDocumentVersionPDFInput) (*types.ExportSignableDocumentVersionPDFPayload, error) { func (r *mutationResolver) ExportEmployeeDocumentVersionPDF(ctx context.Context, input types.ExportEmployeeDocumentVersionPDFInput) (*types.ExportEmployeeDocumentVersionPDFPayload, error) {
if err := r.authorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionExportSignable); err != nil { if err := r.authorize(ctx, input.DocumentVersionID, probo.ActionEmployeeDocumentVersionExportPDF); err != nil {
return nil, err return nil, err
} }
@@ -5723,7 +5730,11 @@ func (r *mutationResolver) ExportSignableVersionDocumentPDF(ctx context.Context,
} }
identity := authn.IdentityFromContext(ctx) identity := authn.IdentityFromContext(ctx)
documentFilter := coredata.NewDocumentFilter(nil).WithUserEmail(&identity.EmailAddress) documentFilter := coredata.NewDocumentFilter(nil).WithEmployeeIdentityID(
&identity.ID,
coredata.EmployeeFilterModeSignature,
coredata.EmployeeFilterModeApproval,
)
_, err = prb.Documents.GetWithFilter(ctx, documentVersion.DocumentID, documentFilter) _, err = prb.Documents.GetWithFilter(ctx, documentVersion.DocumentID, documentFilter)
if err != nil { if err != nil {
@@ -5731,7 +5742,7 @@ func (r *mutationResolver) ExportSignableVersionDocumentPDF(ctx context.Context,
return nil, gqlutils.NotFound(ctx, err) return nil, gqlutils.NotFound(ctx, err)
} }
r.logger.ErrorCtx(ctx, "cannot get signable document", log.Error(err)) r.logger.ErrorCtx(ctx, "cannot get employee document", log.Error(err))
return nil, gqlutils.Internal(ctx) return nil, gqlutils.Internal(ctx)
} }
@@ -5743,11 +5754,11 @@ func (r *mutationResolver) ExportSignableVersionDocumentPDF(ctx context.Context,
pdf, err := prb.Documents.ExportPDF(ctx, input.DocumentVersionID, options) pdf, err := prb.Documents.ExportPDF(ctx, input.DocumentVersionID, options)
if err != nil { if err != nil {
r.logger.ErrorCtx(ctx, "cannot export signable document PDF", log.Error(err)) r.logger.ErrorCtx(ctx, "cannot export employee document PDF", log.Error(err))
return nil, gqlutils.Internal(ctx) return nil, gqlutils.Internal(ctx)
} }
return &types.ExportSignableDocumentVersionPDFPayload{ return &types.ExportEmployeeDocumentVersionPDFPayload{
Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)), Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)),
}, nil }, nil
} }
@@ -10201,7 +10212,7 @@ func (r *vendorServiceResolver) Permission(ctx context.Context, obj *types.Vendo
// SignableDocuments is the resolver for the signableDocuments field. // SignableDocuments is the resolver for the signableDocuments field.
func (r *viewerResolver) SignableDocuments(ctx context.Context, obj *types.Viewer, organizationID gid.GID, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy) (*types.EmployeeDocumentConnection, error) { func (r *viewerResolver) SignableDocuments(ctx context.Context, obj *types.Viewer, organizationID gid.GID, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy) (*types.EmployeeDocumentConnection, error) {
if err := r.authorize(ctx, organizationID, probo.ActionDocumentList); err != nil { if err := r.authorize(ctx, organizationID, probo.ActionEmployeeDocumentList); err != nil {
return nil, err return nil, err
} }
@@ -10222,7 +10233,7 @@ func (r *viewerResolver) SignableDocuments(ctx context.Context, obj *types.Viewe
identity := authn.IdentityFromContext(ctx) identity := authn.IdentityFromContext(ctx)
documentFilter := coredata.NewDocumentFilter(nil).WithUserEmail(&identity.EmailAddress) documentFilter := coredata.NewDocumentFilter(nil).WithEmployeeIdentityID(&identity.ID, coredata.EmployeeFilterModeSignature)
documentsPage, err := prb.Documents.ListByOrganizationID(ctx, organizationID, cursor, documentFilter) documentsPage, err := prb.Documents.ListByOrganizationID(ctx, organizationID, cursor, documentFilter)
if err != nil { if err != nil {
@@ -10249,7 +10260,7 @@ func (r *viewerResolver) SignableDocuments(ctx context.Context, obj *types.Viewe
// SignableDocument is the resolver for the signableDocument field. // SignableDocument is the resolver for the signableDocument field.
func (r *viewerResolver) SignableDocument(ctx context.Context, obj *types.Viewer, id gid.GID) (*types.EmployeeDocument, error) { func (r *viewerResolver) SignableDocument(ctx context.Context, obj *types.Viewer, id gid.GID) (*types.EmployeeDocument, error) {
if err := r.authorize(ctx, id, probo.ActionDocumentGet); err != nil { if err := r.authorize(ctx, id, probo.ActionEmployeeDocumentGet); err != nil {
return nil, err return nil, err
} }
@@ -10257,7 +10268,7 @@ func (r *viewerResolver) SignableDocument(ctx context.Context, obj *types.Viewer
identity := authn.IdentityFromContext(ctx) identity := authn.IdentityFromContext(ctx)
documentFilter := coredata.NewDocumentFilter(nil).WithUserEmail(&identity.EmailAddress) documentFilter := coredata.NewDocumentFilter(nil).WithEmployeeIdentityID(&identity.ID, coredata.EmployeeFilterModeSignature)
document, err := prb.Documents.GetWithFilter(ctx, id, documentFilter) document, err := prb.Documents.GetWithFilter(ctx, id, documentFilter)
if err != nil { if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) { if errors.Is(err, coredata.ErrResourceNotFound) {
@@ -10280,7 +10291,7 @@ func (r *viewerResolver) SignableDocument(ctx context.Context, obj *types.Viewer
// ApprovableDocuments is the resolver for the approvableDocuments field. // ApprovableDocuments is the resolver for the approvableDocuments field.
func (r *viewerResolver) ApprovableDocuments(ctx context.Context, obj *types.Viewer, organizationID gid.GID, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy) (*types.EmployeeDocumentConnection, error) { func (r *viewerResolver) ApprovableDocuments(ctx context.Context, obj *types.Viewer, organizationID gid.GID, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy) (*types.EmployeeDocumentConnection, error) {
if err := r.authorize(ctx, organizationID, probo.ActionDocumentVersionApprovalList); err != nil { if err := r.authorize(ctx, organizationID, probo.ActionEmployeeDocumentList); err != nil {
return nil, err return nil, err
} }
@@ -10301,7 +10312,7 @@ func (r *viewerResolver) ApprovableDocuments(ctx context.Context, obj *types.Vie
identity := authn.IdentityFromContext(ctx) identity := authn.IdentityFromContext(ctx)
documentFilter := coredata.NewDocumentFilter(nil).WithApproverIdentityID(&identity.ID) documentFilter := coredata.NewDocumentFilter(nil).WithEmployeeIdentityID(&identity.ID, coredata.EmployeeFilterModeApproval)
documentsPage, err := prb.Documents.ListByOrganizationID(ctx, organizationID, cursor, documentFilter) documentsPage, err := prb.Documents.ListByOrganizationID(ctx, organizationID, cursor, documentFilter)
if err != nil { if err != nil {
@@ -10328,7 +10339,7 @@ func (r *viewerResolver) ApprovableDocuments(ctx context.Context, obj *types.Vie
// ApprovableDocument is the resolver for the approvableDocument field. // ApprovableDocument is the resolver for the approvableDocument field.
func (r *viewerResolver) ApprovableDocument(ctx context.Context, obj *types.Viewer, id gid.GID) (*types.EmployeeDocument, error) { func (r *viewerResolver) ApprovableDocument(ctx context.Context, obj *types.Viewer, id gid.GID) (*types.EmployeeDocument, error) {
if err := r.authorize(ctx, id, probo.ActionDocumentGet); err != nil { if err := r.authorize(ctx, id, probo.ActionEmployeeDocumentGet); err != nil {
return nil, err return nil, err
} }
@@ -10336,7 +10347,7 @@ func (r *viewerResolver) ApprovableDocument(ctx context.Context, obj *types.View
identity := authn.IdentityFromContext(ctx) identity := authn.IdentityFromContext(ctx)
documentFilter := coredata.NewDocumentFilter(nil).WithApproverIdentityID(&identity.ID) documentFilter := coredata.NewDocumentFilter(nil).WithEmployeeIdentityID(&identity.ID, coredata.EmployeeFilterModeApproval)
document, err := prb.Documents.GetWithFilter(ctx, id, documentFilter) document, err := prb.Documents.GetWithFilter(ctx, id, documentFilter)
if err != nil { if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) { if errors.Is(err, coredata.ErrResourceNotFound) {