Replace third-party owners with administrators
Some checks failed
github / Analyze (go) (push) Has been cancelled
github / Analyze (actions) (push) Has been cancelled
github / Analyze (javascript-typescript) (push) Has been cancelled
make / build-apps (push) Has been cancelled
make / probod binary (darwin/amd64) (push) Has been cancelled
make / probod binary (freebsd/amd64) (push) Has been cancelled
make / probod binary (linux/amd64) (push) Has been cancelled
make / probod binary (openbsd/amd64) (push) Has been cancelled
make / probod binary (windows/amd64) (push) Has been cancelled
make / probod binary (darwin/arm64) (push) Has been cancelled
make / probod binary (freebsd/arm64) (push) Has been cancelled
make / probod binary (linux/arm64) (push) Has been cancelled
make / probod binary (openbsd/arm64) (push) Has been cancelled
make / probo-agent (darwin/amd64) (push) Has been cancelled
make / probo-agent (freebsd/amd64) (push) Has been cancelled
make / probo-agent (linux/amd64) (push) Has been cancelled
make / probo-agent (windows/amd64) (push) Has been cancelled
make / probo-agent (darwin/arm64) (push) Has been cancelled
make / probo-agent (freebsd/arm64) (push) Has been cancelled
make / probo-agent (linux/arm64) (push) Has been cancelled
make / probo-agent (windows/arm64) (push) Has been cancelled
make / docker (amd64) (push) Has been cancelled
make / docker (arm64) (push) Has been cancelled
make / snapshot-scan (push) Has been cancelled
make / build-probod (push) Has been cancelled
make / build-probo-agent (push) Has been cancelled
make / lint-go (push) Has been cancelled
make / lint-js (push) Has been cancelled
make / lint-swift (push) Has been cancelled
make / lint-shell (push) Has been cancelled
make / test (push) Has been cancelled
make / test-e2e (push) Has been cancelled
trufflehog / scan (push) Has been cancelled
Some checks failed
github / Analyze (go) (push) Has been cancelled
github / Analyze (actions) (push) Has been cancelled
github / Analyze (javascript-typescript) (push) Has been cancelled
make / build-apps (push) Has been cancelled
make / probod binary (darwin/amd64) (push) Has been cancelled
make / probod binary (freebsd/amd64) (push) Has been cancelled
make / probod binary (linux/amd64) (push) Has been cancelled
make / probod binary (openbsd/amd64) (push) Has been cancelled
make / probod binary (windows/amd64) (push) Has been cancelled
make / probod binary (darwin/arm64) (push) Has been cancelled
make / probod binary (freebsd/arm64) (push) Has been cancelled
make / probod binary (linux/arm64) (push) Has been cancelled
make / probod binary (openbsd/arm64) (push) Has been cancelled
make / probo-agent (darwin/amd64) (push) Has been cancelled
make / probo-agent (freebsd/amd64) (push) Has been cancelled
make / probo-agent (linux/amd64) (push) Has been cancelled
make / probo-agent (windows/amd64) (push) Has been cancelled
make / probo-agent (darwin/arm64) (push) Has been cancelled
make / probo-agent (freebsd/arm64) (push) Has been cancelled
make / probo-agent (linux/arm64) (push) Has been cancelled
make / probo-agent (windows/arm64) (push) Has been cancelled
make / docker (amd64) (push) Has been cancelled
make / docker (arm64) (push) Has been cancelled
make / snapshot-scan (push) Has been cancelled
make / build-probod (push) Has been cancelled
make / build-probo-agent (push) Has been cancelled
make / lint-go (push) Has been cancelled
make / lint-js (push) Has been cancelled
make / lint-swift (push) Has been cancelled
make / lint-shell (push) Has been cancelled
make / test (push) Has been cancelled
make / test-e2e (push) Has been cancelled
trufflehog / scan (push) Has been cancelled
Migrate business and security owners into a shared administrators list across GraphQL, MCP, CLI, n8n, and the console. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
@@ -2462,9 +2462,9 @@
|
|||||||
},
|
},
|
||||||
"thirdPartyOverviewPage": {
|
"thirdPartyOverviewPage": {
|
||||||
"pageTitle": "{{name}} - Overview",
|
"pageTitle": "{{name}} - Overview",
|
||||||
"sections": { "details": "Third party details", "countries": "Countries", "ownership": "Ownership details", "links": "Links", "dataAgreements": "Data agreements" },
|
"sections": { "details": "Third party details", "countries": "Countries", "administrators": "Administrators", "links": "Links", "dataAgreements": "Data agreements" },
|
||||||
"fields": { "name": "Name", "description": "Description", "category": "Category", "legalName": "Legal name", "headquarterAddress": "Headquarter address", "websiteUrl": "Website URL", "businessOwner": "Business owner", "securityOwner": "Security owner" },
|
"fields": { "name": "Name", "description": "Description", "category": "Category", "legalName": "Legal name", "headquarterAddress": "Headquarter address", "websiteUrl": "Website URL", "administrators": "Administrators" },
|
||||||
"placeholders": { "category": "Select a category" },
|
"placeholders": { "category": "Select a category", "administrators": "Add administrators..." },
|
||||||
"categories": { "analytics": "Analytics", "cloudMonitoring": "Cloud Monitoring", "cloudProvider": "Cloud Provider", "collaboration": "Collaboration", "customerSupport": "Customer Support", "dataStorageAndProcessing": "Data Storage and Processing", "documentManagement": "Document Management", "employeeManagement": "Employee Management", "engineering": "Engineering", "finance": "Finance", "identityProvider": "Identity Provider", "it": "IT", "marketing": "Marketing", "officeOperations": "Office Operations", "other": "Other", "passwordManagement": "Password Management", "productAndDesign": "Product and Design", "professionalServices": "Professional Services", "recruiting": "Recruiting", "sales": "Sales", "security": "Security", "versionControl": "Version Control" },
|
"categories": { "analytics": "Analytics", "cloudMonitoring": "Cloud Monitoring", "cloudProvider": "Cloud Provider", "collaboration": "Collaboration", "customerSupport": "Customer Support", "dataStorageAndProcessing": "Data Storage and Processing", "documentManagement": "Document Management", "employeeManagement": "Employee Management", "engineering": "Engineering", "finance": "Finance", "identityProvider": "Identity Provider", "it": "IT", "marketing": "Marketing", "officeOperations": "Office Operations", "other": "Other", "passwordManagement": "Password Management", "productAndDesign": "Product and Design", "professionalServices": "Professional Services", "recruiting": "Recruiting", "sales": "Sales", "security": "Security", "versionControl": "Version Control" },
|
||||||
"urlLabels": { "statusPage": "Status page URL", "termsOfService": "Terms of service URL", "privacyPolicy": "Privacy document URL", "serviceLevelAgreement": "Service level agreement URL", "dataProcessingAgreement": "Data processing agreement URL", "securityPage": "Security page URL", "trustPage": "Trust page URL" },
|
"urlLabels": { "statusPage": "Status page URL", "termsOfService": "Terms of service URL", "privacyPolicy": "Privacy document URL", "serviceLevelAgreement": "Service level agreement URL", "dataProcessingAgreement": "Data processing agreement URL", "securityPage": "Security page URL", "trustPage": "Trust page URL" },
|
||||||
"agreements": { "businessAssociate": "Business Associate Agreement", "dataPrivacy": "Data Privacy Agreement", "noBusinessAssociate": "No business associate agreement available", "noDataPrivacy": "No data privacy agreement available", "validity": { "range": "Valid from {{from}} until {{until}}", "from": "Valid from {{date}}", "until": "Valid until {{date}}" } },
|
"agreements": { "businessAssociate": "Business Associate Agreement", "dataPrivacy": "Data Privacy Agreement", "noBusinessAssociate": "No business associate agreement available", "noDataPrivacy": "No data privacy agreement available", "validity": { "range": "Valid from {{from}} until {{until}}", "from": "Valid from {{date}}", "until": "Valid until {{date}}" } },
|
||||||
|
|||||||
@@ -5743,7 +5743,7 @@
|
|||||||
"sections": {
|
"sections": {
|
||||||
"details": "Détails du tiers",
|
"details": "Détails du tiers",
|
||||||
"countries": "Pays",
|
"countries": "Pays",
|
||||||
"ownership": "Détails de la propriété",
|
"administrators": "Administrateurs",
|
||||||
"links": "Liens",
|
"links": "Liens",
|
||||||
"dataAgreements": "Accords de données"
|
"dataAgreements": "Accords de données"
|
||||||
},
|
},
|
||||||
@@ -5754,11 +5754,11 @@
|
|||||||
"legalName": "Nom légal",
|
"legalName": "Nom légal",
|
||||||
"headquarterAddress": "Adresse du siège",
|
"headquarterAddress": "Adresse du siège",
|
||||||
"websiteUrl": "URL du site web",
|
"websiteUrl": "URL du site web",
|
||||||
"businessOwner": "Propriétaire métier",
|
"administrators": "Administrateurs"
|
||||||
"securityOwner": "Propriétaire sécurité"
|
|
||||||
},
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"category": "Sélectionner une catégorie"
|
"category": "Sélectionner une catégorie",
|
||||||
|
"administrators": "Ajouter des administrateurs..."
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"analytics": "Analytique",
|
"analytics": "Analytique",
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ const schema = z.object({
|
|||||||
countries: z.array(z.string()),
|
countries: z.array(z.string()),
|
||||||
securityPageUrl: z.string().optional().nullable(),
|
securityPageUrl: z.string().optional().nullable(),
|
||||||
trustPageUrl: z.string().optional().nullable(),
|
trustPageUrl: z.string().optional().nullable(),
|
||||||
businessOwnerId: z.string().nullish(),
|
administratorIds: z.array(z.string()),
|
||||||
securityOwnerId: z.string().nullish(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const thirdPartyFormFragment = graphql`
|
const thirdPartyFormFragment = graphql`
|
||||||
@@ -67,11 +66,10 @@ const thirdPartyFormFragment = graphql`
|
|||||||
countries
|
countries
|
||||||
securityPageUrl
|
securityPageUrl
|
||||||
trustPageUrl
|
trustPageUrl
|
||||||
businessOwner {
|
administrators {
|
||||||
id
|
|
||||||
}
|
|
||||||
securityOwner {
|
|
||||||
id
|
id
|
||||||
|
fullName
|
||||||
|
emailAddress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -112,8 +110,7 @@ export function useThirdPartyForm(thirdPartyKey: useThirdPartyFormFragment$key)
|
|||||||
countries: [...(thirdParty.countries ?? [])],
|
countries: [...(thirdParty.countries ?? [])],
|
||||||
securityPageUrl: thirdParty.securityPageUrl || null,
|
securityPageUrl: thirdParty.securityPageUrl || null,
|
||||||
trustPageUrl: thirdParty.trustPageUrl || null,
|
trustPageUrl: thirdParty.trustPageUrl || null,
|
||||||
businessOwnerId: thirdParty.businessOwner?.id,
|
administratorIds: thirdParty.administrators.map(a => a.id),
|
||||||
securityOwnerId: thirdParty.securityOwner?.id,
|
|
||||||
}),
|
}),
|
||||||
[thirdParty],
|
[thirdParty],
|
||||||
);
|
);
|
||||||
@@ -151,5 +148,6 @@ export function useThirdPartyForm(thirdPartyKey: useThirdPartyFormFragment$key)
|
|||||||
return {
|
return {
|
||||||
...form,
|
...form,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
administrators: thirdParty.administrators,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import type { ThirdPartyOverviewPageQuery } from "#/__generated__/core/ThirdPart
|
|||||||
import type { ThirdPartyCategory } from "#/__generated__/core/useThirdPartyFormFragment.graphql";
|
import type { ThirdPartyCategory } from "#/__generated__/core/useThirdPartyFormFragment.graphql";
|
||||||
import { ControlledField } from "#/components/form/ControlledField";
|
import { ControlledField } from "#/components/form/ControlledField";
|
||||||
import { CountriesField } from "#/components/form/CountriesField";
|
import { CountriesField } from "#/components/form/CountriesField";
|
||||||
import { PeopleSelectField } from "#/components/form/PeopleSelectField";
|
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
|
||||||
import { useThirdPartyForm } from "#/hooks/forms/useThirdPartyForm";
|
import { useThirdPartyForm } from "#/hooks/forms/useThirdPartyForm";
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||||
|
|
||||||
@@ -156,6 +156,7 @@ export default function ThirdPartyOverviewPage(props: ThirdPartyOverviewPageProp
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
|
administrators,
|
||||||
} = useThirdPartyForm(thirdParty);
|
} = useThirdPartyForm(thirdParty);
|
||||||
|
|
||||||
const thirdPartyWithBAA
|
const thirdPartyWithBAA
|
||||||
@@ -271,25 +272,21 @@ export default function ThirdPartyOverviewPage(props: ThirdPartyOverviewPageProp
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-base font-medium">{t("thirdPartyOverviewPage.sections.ownership")}</h2>
|
<h2 className="text-base font-medium">{t("thirdPartyOverviewPage.sections.administrators")}</h2>
|
||||||
<Card className="space-y-4" padded>
|
<Card className="space-y-4" padded>
|
||||||
<PeopleSelectField
|
<PeopleMultiSelectField
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
control={control}
|
control={control}
|
||||||
name="businessOwnerId"
|
name="administratorIds"
|
||||||
label={t("thirdPartyOverviewPage.fields.businessOwner")}
|
label={t("thirdPartyOverviewPage.fields.administrators")}
|
||||||
error={errors.businessOwnerId?.message}
|
error={errors.administratorIds?.message}
|
||||||
disabled={isFormDisabled}
|
disabled={isFormDisabled}
|
||||||
optional={true}
|
selectedPeople={administrators.map(a => ({
|
||||||
/>
|
id: a.id,
|
||||||
<PeopleSelectField
|
fullName: a.fullName,
|
||||||
organizationId={organizationId}
|
emailAddress: a.emailAddress,
|
||||||
control={control}
|
}))}
|
||||||
name="securityOwnerId"
|
placeholder={t("thirdPartyOverviewPage.placeholders.administrators")}
|
||||||
label={t("thirdPartyOverviewPage.fields.securityOwner")}
|
|
||||||
error={errors.securityOwnerId?.message}
|
|
||||||
disabled={isFormDisabled}
|
|
||||||
optional={true}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ Use the same shape across DB, services, GraphQL, and console pickers.
|
|||||||
### Resources that follow this pattern
|
### Resources that follow this pattern
|
||||||
|
|
||||||
- Assets, data (datum), risks, obligations, findings
|
- Assets, data (datum), risks, obligations, findings
|
||||||
- Third parties (`business_owner_profile_id`, `security_owner_profile_id`)
|
|
||||||
- Devices (ITAM) — aligned with compliance resources as of the devices table
|
- Devices (ITAM) — aligned with compliance resources as of the devices table
|
||||||
introduction
|
introduction
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ resource for the `owner` field (parent access is already established).
|
|||||||
|
|
||||||
Default for new work is a **nullable** owner (`OwnerID *gid.GID`, GraphQL
|
Default for new work is a **nullable** owner (`OwnerID *gid.GID`, GraphQL
|
||||||
`owner: Profile`). Set the embedded profile only when present so the resolver
|
`owner: Profile`). Set the embedded profile only when present so the resolver
|
||||||
nil guard is live (device, risk, finding, third-party owners):
|
nil guard is live (device, risk, finding):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// types — set only when present
|
// types — set only when present
|
||||||
|
|||||||
@@ -25,12 +25,11 @@
|
|||||||
// authorization shape (authorizing the parent obj.ID with the child's
|
// authorization shape (authorizing the parent obj.ID with the child's
|
||||||
// ActionMembershipProfileGet, then loading the child through the scope-by-key
|
// ActionMembershipProfileGet, then loading the child through the scope-by-key
|
||||||
// Profile dataloader) also existed on asset.owner, datum.owner, finding.owner,
|
// Profile dataloader) also existed on asset.owner, datum.owner, finding.owner,
|
||||||
// obligation.owner, risk.owner, task.assignedTo, thirdParty.businessOwner and
|
// obligation.owner, risk.owner, task.assignedTo, and
|
||||||
// thirdParty.securityOwner. Each of those write paths validates the owner FK
|
// thirdParty.administrators. Each of those write paths validates the owner FK
|
||||||
// today, so these tests use injectCrossTenantFK to plant a foreign profile id
|
// today, so these tests plant a foreign profile id directly -- proving the
|
||||||
// directly in the row -- proving the read resolver now authorizes the actual
|
// read resolver refuses cross-tenant PII independently of the write check
|
||||||
// child profile id and refuses cross-tenant PII independently of the write
|
// (a future write regression, migration bug, or direct DB access).
|
||||||
// check (a future write regression, migration bug, or direct DB access).
|
|
||||||
package console_test
|
package console_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -333,24 +332,24 @@ func TestSecurity_ReadGap_TaskAssignedTo(t *testing.T) {
|
|||||||
testutil.AssertNodeNotAccessible(t, err, readResult.Node.AssignedTo == nil, "cross-tenant profile PII via task.assignedTo")
|
testutil.AssertNodeNotAccessible(t, err, readResult.Node.AssignedTo == nil, "cross-tenant profile PII via task.assignedTo")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSecurity_ReadGap_ThirdPartyBusinessOwner(t *testing.T) {
|
func TestSecurity_ReadGap_ThirdPartyAdministrators(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
org1Owner := testutil.NewClient(t, testutil.RoleOwner)
|
org1Owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
org2Owner := testutil.NewClient(t, testutil.RoleOwner)
|
org2Owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
org2ProfileID := factory.CreateUser(org2Owner, factory.Attrs{"fullName": "Org2 Secret Business Owner (read-gap probe)"})
|
org2ProfileID := factory.CreateUser(org2Owner, factory.Attrs{"fullName": "Org2 Secret Administrator (read-gap probe)"})
|
||||||
|
|
||||||
thirdPartyID := factory.CreateThirdParty(org1Owner, factory.Attrs{"name": "Org1 ThirdParty for read-gap probe"})
|
thirdPartyID := factory.CreateThirdParty(org1Owner, factory.Attrs{"name": "Org1 ThirdParty for read-gap probe"})
|
||||||
|
|
||||||
injectCrossTenantFK(t, "third_parties", "business_owner_profile_id", thirdPartyID, org2ProfileID)
|
factory.InjectCrossTenantThirdPartyAdministrator(t, thirdPartyID, org2ProfileID)
|
||||||
|
|
||||||
var readResult struct {
|
var readResult struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
BusinessOwner *struct {
|
Administrators []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FullName string `json:"fullName"`
|
FullName string `json:"fullName"`
|
||||||
} `json:"businessOwner"`
|
} `json:"administrators"`
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,45 +357,20 @@ func TestSecurity_ReadGap_ThirdPartyBusinessOwner(t *testing.T) {
|
|||||||
query($id: ID!) {
|
query($id: ID!) {
|
||||||
node(id: $id) {
|
node(id: $id) {
|
||||||
... on ThirdParty {
|
... on ThirdParty {
|
||||||
businessOwner { id fullName }
|
administrators { id fullName }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, map[string]any{"id": thirdPartyID}, &readResult)
|
`, map[string]any{"id": thirdPartyID}, &readResult)
|
||||||
|
|
||||||
testutil.AssertNodeNotAccessible(t, err, readResult.Node.BusinessOwner == nil, "cross-tenant profile PII via thirdParty.businessOwner")
|
leaked := false
|
||||||
}
|
|
||||||
|
|
||||||
func TestSecurity_ReadGap_ThirdPartySecurityOwner(t *testing.T) {
|
for _, a := range readResult.Node.Administrators {
|
||||||
t.Parallel()
|
if a.ID == org2ProfileID || a.FullName != "" && a.ID == org2ProfileID {
|
||||||
|
leaked = true
|
||||||
org1Owner := testutil.NewClient(t, testutil.RoleOwner)
|
break
|
||||||
org2Owner := testutil.NewClient(t, testutil.RoleOwner)
|
}
|
||||||
|
|
||||||
org2ProfileID := factory.CreateUser(org2Owner, factory.Attrs{"fullName": "Org2 Secret Security Owner (read-gap probe)"})
|
|
||||||
|
|
||||||
thirdPartyID := factory.CreateThirdParty(org1Owner, factory.Attrs{"name": "Org1 ThirdParty for read-gap probe"})
|
|
||||||
|
|
||||||
injectCrossTenantFK(t, "third_parties", "security_owner_profile_id", thirdPartyID, org2ProfileID)
|
|
||||||
|
|
||||||
var readResult struct {
|
|
||||||
Node struct {
|
|
||||||
SecurityOwner *struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
FullName string `json:"fullName"`
|
|
||||||
} `json:"securityOwner"`
|
|
||||||
} `json:"node"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := org1Owner.Execute(`
|
testutil.AssertNodeNotAccessible(t, err, !leaked && len(readResult.Node.Administrators) == 0, "cross-tenant profile PII via thirdParty.administrators")
|
||||||
query($id: ID!) {
|
|
||||||
node(id: $id) {
|
|
||||||
... on ThirdParty {
|
|
||||||
securityOwner { id fullName }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, map[string]any{"id": thirdPartyID}, &readResult)
|
|
||||||
|
|
||||||
testutil.AssertNodeNotAccessible(t, err, readResult.Node.SecurityOwner == nil, "cross-tenant profile PII via thirdParty.securityOwner")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -506,13 +506,13 @@ func TestThirdParty_SubResolvers(t *testing.T) {
|
|||||||
assert.NotNil(t, result.Node.Services.Edges)
|
assert.NotNil(t, result.Node.Services.Edges)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("businessOwner sub-resolver (null)", func(t *testing.T) {
|
t.Run("administrators sub-resolver (empty)", func(t *testing.T) {
|
||||||
query := `
|
query := `
|
||||||
query($id: ID!) {
|
query($id: ID!) {
|
||||||
node(id: $id) {
|
node(id: $id) {
|
||||||
... on ThirdParty {
|
... on ThirdParty {
|
||||||
id
|
id
|
||||||
businessOwner {
|
administrators {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
}
|
}
|
||||||
@@ -523,47 +523,17 @@ func TestThirdParty_SubResolvers(t *testing.T) {
|
|||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
BusinessOwner *struct {
|
Administrators []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FullName string `json:"fullName"`
|
FullName string `json:"fullName"`
|
||||||
} `json:"businessOwner"`
|
} `json:"administrators"`
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{"id": thirdPartyID}, &result)
|
err := owner.Execute(query, map[string]any{"id": thirdPartyID}, &result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Nil(t, result.Node.BusinessOwner)
|
assert.Empty(t, result.Node.Administrators)
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("securityOwner sub-resolver (null)", func(t *testing.T) {
|
|
||||||
query := `
|
|
||||||
query($id: ID!) {
|
|
||||||
node(id: $id) {
|
|
||||||
... on ThirdParty {
|
|
||||||
id
|
|
||||||
securityOwner {
|
|
||||||
id
|
|
||||||
fullName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
SecurityOwner *struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
FullName string `json:"fullName"`
|
|
||||||
} `json:"securityOwner"`
|
|
||||||
} `json:"node"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{"id": thirdPartyID}, &result)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, result.Node.SecurityOwner)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,23 +726,22 @@ func TestThirdParty_OmittableDescription(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestThirdParty_OmittableBusinessOwner(t *testing.T) {
|
func TestThirdParty_Administrators(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
// Create a profile for owner assignment
|
|
||||||
profileID := factory.CreateUser(owner)
|
profileID := factory.CreateUser(owner)
|
||||||
thirdPartyID := factory.NewThirdParty(owner).
|
thirdPartyID := factory.NewThirdParty(owner).
|
||||||
WithName("BusinessOwner Test ThirdParty").
|
WithName("Administrators Test ThirdParty").
|
||||||
Create()
|
Create()
|
||||||
|
|
||||||
t.Run("set business owner", func(t *testing.T) {
|
t.Run("set administrators", func(t *testing.T) {
|
||||||
query := `
|
query := `
|
||||||
mutation UpdateThirdParty($input: UpdateThirdPartyInput!) {
|
mutation UpdateThirdParty($input: UpdateThirdPartyInput!) {
|
||||||
updateThirdParty(input: $input) {
|
updateThirdParty(input: $input) {
|
||||||
thirdParty {
|
thirdParty {
|
||||||
id
|
id
|
||||||
businessOwner {
|
administrators {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
}
|
}
|
||||||
@@ -784,32 +753,33 @@ func TestThirdParty_OmittableBusinessOwner(t *testing.T) {
|
|||||||
var result struct {
|
var result struct {
|
||||||
UpdateThirdParty struct {
|
UpdateThirdParty struct {
|
||||||
ThirdParty struct {
|
ThirdParty struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
BusinessOwner struct {
|
Administrators []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FullName string `json:"fullName"`
|
FullName string `json:"fullName"`
|
||||||
} `json:"businessOwner"`
|
} `json:"administrators"`
|
||||||
} `json:"thirdParty"`
|
} `json:"thirdParty"`
|
||||||
} `json:"updateThirdParty"`
|
} `json:"updateThirdParty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{
|
err := owner.Execute(query, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": thirdPartyID,
|
"id": thirdPartyID,
|
||||||
"businessOwnerId": profileID,
|
"administratorIds": []string{profileID},
|
||||||
},
|
},
|
||||||
}, &result)
|
}, &result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, profileID, result.UpdateThirdParty.ThirdParty.BusinessOwner.ID)
|
require.Len(t, result.UpdateThirdParty.ThirdParty.Administrators, 1)
|
||||||
|
assert.Equal(t, profileID, result.UpdateThirdParty.ThirdParty.Administrators[0].ID)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("clear business owner with null", func(t *testing.T) {
|
t.Run("clear administrators with empty list", func(t *testing.T) {
|
||||||
query := `
|
query := `
|
||||||
mutation UpdateThirdParty($input: UpdateThirdPartyInput!) {
|
mutation UpdateThirdParty($input: UpdateThirdPartyInput!) {
|
||||||
updateThirdParty(input: $input) {
|
updateThirdParty(input: $input) {
|
||||||
thirdParty {
|
thirdParty {
|
||||||
id
|
id
|
||||||
businessOwner {
|
administrators {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -820,103 +790,22 @@ func TestThirdParty_OmittableBusinessOwner(t *testing.T) {
|
|||||||
var result struct {
|
var result struct {
|
||||||
UpdateThirdParty struct {
|
UpdateThirdParty struct {
|
||||||
ThirdParty struct {
|
ThirdParty struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
BusinessOwner *struct {
|
Administrators []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
} `json:"businessOwner"`
|
} `json:"administrators"`
|
||||||
} `json:"thirdParty"`
|
} `json:"thirdParty"`
|
||||||
} `json:"updateThirdParty"`
|
} `json:"updateThirdParty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{
|
err := owner.Execute(query, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": thirdPartyID,
|
"id": thirdPartyID,
|
||||||
"businessOwnerId": nil,
|
"administratorIds": []string{},
|
||||||
},
|
},
|
||||||
}, &result)
|
}, &result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Nil(t, result.UpdateThirdParty.ThirdParty.BusinessOwner)
|
assert.Empty(t, result.UpdateThirdParty.ThirdParty.Administrators)
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestThirdParty_OmittableSecurityOwner(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
||||||
|
|
||||||
// Create a profile for owner assignment
|
|
||||||
profileID := factory.CreateUser(owner)
|
|
||||||
thirdPartyID := factory.NewThirdParty(owner).WithName("SecurityOwner Test ThirdParty").Create()
|
|
||||||
|
|
||||||
t.Run("set security owner", func(t *testing.T) {
|
|
||||||
query := `
|
|
||||||
mutation UpdateThirdParty($input: UpdateThirdPartyInput!) {
|
|
||||||
updateThirdParty(input: $input) {
|
|
||||||
thirdParty {
|
|
||||||
id
|
|
||||||
securityOwner {
|
|
||||||
id
|
|
||||||
fullName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
UpdateThirdParty struct {
|
|
||||||
ThirdParty struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
SecurityOwner struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
FullName string `json:"fullName"`
|
|
||||||
} `json:"securityOwner"`
|
|
||||||
} `json:"thirdParty"`
|
|
||||||
} `json:"updateThirdParty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"id": thirdPartyID,
|
|
||||||
"securityOwnerId": profileID,
|
|
||||||
},
|
|
||||||
}, &result)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, profileID, result.UpdateThirdParty.ThirdParty.SecurityOwner.ID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("clear security owner with null", func(t *testing.T) {
|
|
||||||
query := `
|
|
||||||
mutation UpdateThirdParty($input: UpdateThirdPartyInput!) {
|
|
||||||
updateThirdParty(input: $input) {
|
|
||||||
thirdParty {
|
|
||||||
id
|
|
||||||
securityOwner {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
UpdateThirdParty struct {
|
|
||||||
ThirdParty struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
SecurityOwner *struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"securityOwner"`
|
|
||||||
} `json:"thirdParty"`
|
|
||||||
} `json:"updateThirdParty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"id": thirdPartyID,
|
|
||||||
"securityOwnerId": nil,
|
|
||||||
},
|
|
||||||
}, &result)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, result.UpdateThirdParty.ThirdParty.SecurityOwner)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,7 +1067,7 @@ func TestThirdParty_TenantIsolation(t *testing.T) {
|
|||||||
require.Error(t, err, "Should not be able to delete thirdParty from another org")
|
require.Error(t, err, "Should not be able to delete thirdParty from another org")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("cannot create thirdParty referencing a business owner from another organization", func(t *testing.T) {
|
t.Run("cannot create thirdParty referencing an administrator from another organization", func(t *testing.T) {
|
||||||
org2ProfileID := factory.CreateUser(org2Owner)
|
org2ProfileID := factory.CreateUser(org2Owner)
|
||||||
|
|
||||||
_, err := org1Owner.Do(`
|
_, err := org1Owner.Do(`
|
||||||
@@ -1189,17 +1078,17 @@ func TestThirdParty_TenantIsolation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`, map[string]any{
|
`, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"organizationId": org1Owner.GetOrganizationID().String(),
|
"organizationId": org1Owner.GetOrganizationID().String(),
|
||||||
"name": factory.SafeName("ThirdParty"),
|
"name": factory.SafeName("ThirdParty"),
|
||||||
"businessOwnerId": org2ProfileID,
|
"administratorIds": []string{org2ProfileID},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.Error(t, err, "must not accept a businessOwnerId belonging to another organization")
|
require.Error(t, err, "must not accept an administratorId belonging to another organization")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("cannot update thirdParty to reference a security owner from another organization", func(t *testing.T) {
|
t.Run("cannot update thirdParty to reference an administrator from another organization", func(t *testing.T) {
|
||||||
org2ProfileID := factory.CreateUser(org2Owner)
|
org2ProfileID := factory.CreateUser(org2Owner)
|
||||||
otherThirdPartyID := factory.NewThirdParty(org1Owner).WithName("Org1 ThirdParty for SecurityOwner").Create()
|
otherThirdPartyID := factory.NewThirdParty(org1Owner).WithName("Org1 ThirdParty for Administrators").Create()
|
||||||
|
|
||||||
_, err := org1Owner.Do(`
|
_, err := org1Owner.Do(`
|
||||||
mutation($input: UpdateThirdPartyInput!) {
|
mutation($input: UpdateThirdPartyInput!) {
|
||||||
@@ -1209,11 +1098,11 @@ func TestThirdParty_TenantIsolation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`, map[string]any{
|
`, map[string]any{
|
||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": otherThirdPartyID,
|
"id": otherThirdPartyID,
|
||||||
"securityOwnerId": org2ProfileID,
|
"administratorIds": []string{org2ProfileID},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.Error(t, err, "must not accept a securityOwnerId belonging to another organization")
|
require.Error(t, err, "must not accept an administratorId belonging to another organization")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("cannot create thirdParty referencing a parent thirdParty from another organization", func(t *testing.T) {
|
t.Run("cannot create thirdParty referencing a parent thirdParty from another organization", func(t *testing.T) {
|
||||||
|
|||||||
@@ -23,15 +23,19 @@ package factory
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/brianvoe/gofakeit/v7"
|
"github.com/brianvoe/gofakeit/v7"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
"go.probo.inc/probo/e2e/internal/testutil"
|
"go.probo.inc/probo/e2e/internal/testutil"
|
||||||
|
"go.probo.inc/probo/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SafeName(prefix string) string {
|
func SafeName(prefix string) string {
|
||||||
@@ -225,6 +229,10 @@ func CreateThirdParty(c *testutil.Client, attrs ...Attrs) string {
|
|||||||
input["category"] = *cat
|
input["category"] = *cat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, ok := a["administratorIds"]; ok {
|
||||||
|
input["administratorIds"] = v
|
||||||
|
}
|
||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
CreateThirdParty struct {
|
CreateThirdParty struct {
|
||||||
ThirdPartyEdge struct {
|
ThirdPartyEdge struct {
|
||||||
@@ -241,6 +249,41 @@ func CreateThirdParty(c *testutil.Client, attrs ...Attrs) string {
|
|||||||
return result.CreateThirdParty.ThirdPartyEdge.Node.ID
|
return result.CreateThirdParty.ThirdPartyEdge.Node.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InjectCrossTenantThirdPartyAdministrator bypasses the application and writes a
|
||||||
|
// third_party_administrators row for a foreign profile, for read-gap security tests.
|
||||||
|
func InjectCrossTenantThirdPartyAdministrator(t *testing.T, thirdPartyID, foreignProfileID string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
client := test.PGClient(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
_, err := conn.Exec(ctx, `
|
||||||
|
INSERT INTO third_party_administrators (
|
||||||
|
third_party_id,
|
||||||
|
administrator_profile_id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
$1,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
FROM third_parties
|
||||||
|
WHERE id = $2
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`, foreignProfileID, thirdPartyID)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "test setup: cannot inject cross-tenant third party administrator")
|
||||||
|
}
|
||||||
|
|
||||||
func CreateFramework(c *testutil.Client, attrs ...Attrs) string {
|
func CreateFramework(c *testutil.Client, attrs ...Attrs) string {
|
||||||
c.T.Helper()
|
c.T.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'The headquarter address of the thirdParty',
|
description: 'The headquarter address of the thirdParty',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Business Owner ID',
|
displayName: 'Administrator IDs',
|
||||||
name: 'businessOwnerId',
|
name: 'administratorIds',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
@@ -129,20 +129,7 @@ export const description: INodeProperties[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
description: 'The ID of the business owner (People ID)',
|
description: 'Comma-separated administrator profile IDs',
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Security Owner ID',
|
|
||||||
name: 'securityOwnerId',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['thirdParty'],
|
|
||||||
operation: ['create'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The ID of the security owner (People ID)',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Additional Fields',
|
displayName: 'Additional Fields',
|
||||||
@@ -248,8 +235,7 @@ export async function execute(
|
|||||||
const websiteUrl = this.getNodeParameter('websiteUrl', itemIndex, '') as string;
|
const websiteUrl = this.getNodeParameter('websiteUrl', itemIndex, '') as string;
|
||||||
const legalName = this.getNodeParameter('legalName', itemIndex, '') as string;
|
const legalName = this.getNodeParameter('legalName', itemIndex, '') as string;
|
||||||
const headquarterAddress = this.getNodeParameter('headquarterAddress', itemIndex, '') as string;
|
const headquarterAddress = this.getNodeParameter('headquarterAddress', itemIndex, '') as string;
|
||||||
const businessOwnerId = this.getNodeParameter('businessOwnerId', itemIndex, '') as string;
|
const administratorIds = this.getNodeParameter('administratorIds', itemIndex, '') as string;
|
||||||
const securityOwnerId = this.getNodeParameter('securityOwnerId', itemIndex, '') as string;
|
|
||||||
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as {
|
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as {
|
||||||
statusPageUrl?: string;
|
statusPageUrl?: string;
|
||||||
termsOfServiceUrl?: string;
|
termsOfServiceUrl?: string;
|
||||||
@@ -307,8 +293,7 @@ export async function execute(
|
|||||||
if (websiteUrl) input.websiteUrl = websiteUrl;
|
if (websiteUrl) input.websiteUrl = websiteUrl;
|
||||||
if (legalName) input.legalName = legalName;
|
if (legalName) input.legalName = legalName;
|
||||||
if (headquarterAddress) input.headquarterAddress = headquarterAddress;
|
if (headquarterAddress) input.headquarterAddress = headquarterAddress;
|
||||||
if (businessOwnerId) input.businessOwnerId = businessOwnerId;
|
if (administratorIds) input.administratorIds = administratorIds.split(',').map(id => id.trim()).filter(Boolean);
|
||||||
if (securityOwnerId) input.securityOwnerId = securityOwnerId;
|
|
||||||
if (additionalFields.statusPageUrl) input.statusPageUrl = additionalFields.statusPageUrl;
|
if (additionalFields.statusPageUrl) input.statusPageUrl = additionalFields.statusPageUrl;
|
||||||
if (additionalFields.termsOfServiceUrl) input.termsOfServiceUrl = additionalFields.termsOfServiceUrl;
|
if (additionalFields.termsOfServiceUrl) input.termsOfServiceUrl = additionalFields.termsOfServiceUrl;
|
||||||
if (additionalFields.privacyPolicyUrl) input.privacyPolicyUrl = additionalFields.privacyPolicyUrl;
|
if (additionalFields.privacyPolicyUrl) input.privacyPolicyUrl = additionalFields.privacyPolicyUrl;
|
||||||
|
|||||||
@@ -57,18 +57,11 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'Whether to include organization in the response',
|
description: 'Whether to include organization in the response',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Include Business Owner',
|
displayName: 'Include Administrators',
|
||||||
name: 'includeBusinessOwner',
|
name: 'includeAdministrators',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
description: 'Whether to include business owner in the response',
|
description: 'Whether to include administrators details',
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Include Security Owner',
|
|
||||||
name: 'includeSecurityOwner',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
description: 'Whether to include security owner in the response',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -81,8 +74,7 @@ export async function execute(
|
|||||||
const thirdPartyId = this.getNodeParameter('thirdPartyId', itemIndex) as string;
|
const thirdPartyId = this.getNodeParameter('thirdPartyId', itemIndex) as string;
|
||||||
const options = this.getNodeParameter('options', itemIndex, {}) as {
|
const options = this.getNodeParameter('options', itemIndex, {}) as {
|
||||||
includeOrganization?: boolean;
|
includeOrganization?: boolean;
|
||||||
includeBusinessOwner?: boolean;
|
includeAdministrators?: boolean;
|
||||||
includeSecurityOwner?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const organizationFragment = options.includeOrganization
|
const organizationFragment = options.includeOrganization
|
||||||
@@ -92,16 +84,8 @@ export async function execute(
|
|||||||
}`
|
}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const businessOwnerFragment = options.includeBusinessOwner
|
const administratorsFragment = options.includeAdministrators
|
||||||
? `businessOwner {
|
? `administrators {
|
||||||
id
|
|
||||||
fullName
|
|
||||||
emailAddress
|
|
||||||
}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const securityOwnerFragment = options.includeSecurityOwner
|
|
||||||
? `securityOwner {
|
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
emailAddress
|
emailAddress
|
||||||
@@ -132,8 +116,7 @@ export async function execute(
|
|||||||
countries
|
countries
|
||||||
showOnCompliancePortal
|
showOnCompliancePortal
|
||||||
${organizationFragment}
|
${organizationFragment}
|
||||||
${businessOwnerFragment}
|
${administratorsFragment}
|
||||||
${securityOwnerFragment}
|
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,18 +94,11 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'Whether to include organization in the response',
|
description: 'Whether to include organization in the response',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Include Business Owner',
|
displayName: 'Include Administrators',
|
||||||
name: 'includeBusinessOwner',
|
name: 'includeAdministrators',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
description: 'Whether to include business owner in the response',
|
description: 'Whether to include administrators details',
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Include Security Owner',
|
|
||||||
name: 'includeSecurityOwner',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
description: 'Whether to include security owner in the response',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -121,8 +114,7 @@ export async function execute(
|
|||||||
const options = this.getNodeParameter('options', itemIndex, {}) as {
|
const options = this.getNodeParameter('options', itemIndex, {}) as {
|
||||||
filterLevel?: number;
|
filterLevel?: number;
|
||||||
includeOrganization?: boolean;
|
includeOrganization?: boolean;
|
||||||
includeBusinessOwner?: boolean;
|
includeAdministrators?: boolean;
|
||||||
includeSecurityOwner?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const organizationFragment = options.includeOrganization
|
const organizationFragment = options.includeOrganization
|
||||||
@@ -132,16 +124,8 @@ export async function execute(
|
|||||||
}`
|
}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const businessOwnerFragment = options.includeBusinessOwner
|
const administratorsFragment = options.includeAdministrators
|
||||||
? `businessOwner {
|
? `administrators {
|
||||||
id
|
|
||||||
fullName
|
|
||||||
emailAddress
|
|
||||||
}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const securityOwnerFragment = options.includeSecurityOwner
|
|
||||||
? `securityOwner {
|
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
emailAddress
|
emailAddress
|
||||||
@@ -183,8 +167,7 @@ export async function execute(
|
|||||||
name
|
name
|
||||||
}
|
}
|
||||||
${organizationFragment}
|
${organizationFragment}
|
||||||
${businessOwnerFragment}
|
${administratorsFragment}
|
||||||
${securityOwnerFragment}
|
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'The headquarter address of the thirdParty',
|
description: 'The headquarter address of the thirdParty',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Business Owner ID',
|
displayName: 'Administrator IDs',
|
||||||
name: 'businessOwnerId',
|
name: 'administratorIds',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
@@ -128,20 +128,7 @@ export const description: INodeProperties[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
description: 'The ID of the business owner (People ID)',
|
description: 'Comma-separated administrator profile IDs (omit to leave unchanged)',
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Security Owner ID',
|
|
||||||
name: 'securityOwnerId',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['thirdParty'],
|
|
||||||
operation: ['update'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The ID of the security owner (People ID)',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Show on Compliance Portal',
|
displayName: 'Show on Compliance Portal',
|
||||||
@@ -253,8 +240,7 @@ export async function execute(
|
|||||||
const websiteUrl = this.getNodeParameter('websiteUrl', itemIndex, '') as string;
|
const websiteUrl = this.getNodeParameter('websiteUrl', itemIndex, '') as string;
|
||||||
const legalName = this.getNodeParameter('legalName', itemIndex, '') as string;
|
const legalName = this.getNodeParameter('legalName', itemIndex, '') as string;
|
||||||
const headquarterAddress = this.getNodeParameter('headquarterAddress', itemIndex, '') as string;
|
const headquarterAddress = this.getNodeParameter('headquarterAddress', itemIndex, '') as string;
|
||||||
const businessOwnerId = this.getNodeParameter('businessOwnerId', itemIndex, '') as string;
|
const administratorIds = this.getNodeParameter('administratorIds', itemIndex, '') as string;
|
||||||
const securityOwnerId = this.getNodeParameter('securityOwnerId', itemIndex, '') as string;
|
|
||||||
const showOnCompliancePortal = this.getNodeParameter('showOnCompliancePortal', itemIndex) as boolean | undefined;
|
const showOnCompliancePortal = this.getNodeParameter('showOnCompliancePortal', itemIndex) as boolean | undefined;
|
||||||
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as {
|
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as {
|
||||||
statusPageUrl?: string;
|
statusPageUrl?: string;
|
||||||
@@ -307,8 +293,9 @@ export async function execute(
|
|||||||
if (websiteUrl !== undefined) input.websiteUrl = websiteUrl === '' ? null : websiteUrl;
|
if (websiteUrl !== undefined) input.websiteUrl = websiteUrl === '' ? null : websiteUrl;
|
||||||
if (legalName !== undefined) input.legalName = legalName === '' ? null : legalName;
|
if (legalName !== undefined) input.legalName = legalName === '' ? null : legalName;
|
||||||
if (headquarterAddress !== undefined) input.headquarterAddress = headquarterAddress === '' ? null : headquarterAddress;
|
if (headquarterAddress !== undefined) input.headquarterAddress = headquarterAddress === '' ? null : headquarterAddress;
|
||||||
if (businessOwnerId !== undefined) input.businessOwnerId = businessOwnerId === '' ? null : businessOwnerId;
|
if (administratorIds) {
|
||||||
if (securityOwnerId !== undefined) input.securityOwnerId = securityOwnerId === '' ? null : securityOwnerId;
|
input.administratorIds = administratorIds.split(',').map(id => id.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
if (showOnCompliancePortal !== undefined) input.showOnCompliancePortal = showOnCompliancePortal;
|
if (showOnCompliancePortal !== undefined) input.showOnCompliancePortal = showOnCompliancePortal;
|
||||||
if (additionalFields.statusPageUrl !== undefined) input.statusPageUrl = additionalFields.statusPageUrl === '' ? null : additionalFields.statusPageUrl;
|
if (additionalFields.statusPageUrl !== undefined) input.statusPageUrl = additionalFields.statusPageUrl === '' ? null : additionalFields.statusPageUrl;
|
||||||
if (additionalFields.termsOfServiceUrl !== undefined) input.termsOfServiceUrl = additionalFields.termsOfServiceUrl === '' ? null : additionalFields.termsOfServiceUrl;
|
if (additionalFields.termsOfServiceUrl !== undefined) input.termsOfServiceUrl = additionalFields.termsOfServiceUrl === '' ? null : additionalFields.termsOfServiceUrl;
|
||||||
|
|||||||
@@ -58,13 +58,14 @@ type createResponse struct {
|
|||||||
|
|
||||||
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
||||||
var (
|
var (
|
||||||
flagOrg string
|
flagOrg string
|
||||||
flagName string
|
flagName string
|
||||||
flagCategory string
|
flagCategory string
|
||||||
flagDescription string
|
flagDescription string
|
||||||
flagLegalName string
|
flagLegalName string
|
||||||
flagAddress string
|
flagAddress string
|
||||||
flagWebsite string
|
flagWebsite string
|
||||||
|
flagAdministratorID []string
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -178,6 +179,10 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
input["websiteUrl"] = flagWebsite
|
input["websiteUrl"] = flagWebsite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(flagAdministratorID) > 0 {
|
||||||
|
input["administratorIds"] = flagAdministratorID
|
||||||
|
}
|
||||||
|
|
||||||
data, err := client.Do(
|
data, err := client.Do(
|
||||||
createMutation,
|
createMutation,
|
||||||
map[string]any{"input": input},
|
map[string]any{"input": input},
|
||||||
@@ -210,6 +215,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
cmd.Flags().StringVar(&flagLegalName, "legal-name", "", "Legal name")
|
cmd.Flags().StringVar(&flagLegalName, "legal-name", "", "Legal name")
|
||||||
cmd.Flags().StringVar(&flagAddress, "address", "", "Headquarter address")
|
cmd.Flags().StringVar(&flagAddress, "address", "", "Headquarter address")
|
||||||
cmd.Flags().StringVar(&flagWebsite, "website", "", "Website URL")
|
cmd.Flags().StringVar(&flagWebsite, "website", "", "Website URL")
|
||||||
|
cmd.Flags().StringArrayVar(&flagAdministratorID, "administrator-id", nil, "Administrator profile ID (can be repeated)")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import (
|
|||||||
const updateMutation = `
|
const updateMutation = `
|
||||||
mutation($input: UpdateThirdPartyInput!) {
|
mutation($input: UpdateThirdPartyInput!) {
|
||||||
updateThirdParty(input: $input) {
|
updateThirdParty(input: $input) {
|
||||||
third_party {
|
thirdParty {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
category
|
category
|
||||||
@@ -47,18 +47,19 @@ type updateResponse struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
} `json:"third_party"`
|
} `json:"thirdParty"`
|
||||||
} `json:"updateThirdParty"`
|
} `json:"updateThirdParty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||||
var (
|
var (
|
||||||
flagName string
|
flagName string
|
||||||
flagDescription string
|
flagDescription string
|
||||||
flagCategory string
|
flagCategory string
|
||||||
flagLegalName string
|
flagLegalName string
|
||||||
flagAddress string
|
flagAddress string
|
||||||
flagWebsite string
|
flagWebsite string
|
||||||
|
flagAdministratorID []string
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -112,6 +113,17 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
input["websiteUrl"] = flagWebsite
|
input["websiteUrl"] = flagWebsite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("administrator-id") {
|
||||||
|
administratorIDs := make([]string, 0, len(flagAdministratorID))
|
||||||
|
for _, id := range flagAdministratorID {
|
||||||
|
if id != "" {
|
||||||
|
administratorIDs = append(administratorIDs, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input["administratorIds"] = administratorIDs
|
||||||
|
}
|
||||||
|
|
||||||
if len(input) == 1 {
|
if len(input) == 1 {
|
||||||
return fmt.Errorf("at least one field must be specified for update")
|
return fmt.Errorf("at least one field must be specified for update")
|
||||||
}
|
}
|
||||||
@@ -147,6 +159,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
cmd.Flags().StringVar(&flagLegalName, "legal-name", "", "Legal name")
|
cmd.Flags().StringVar(&flagLegalName, "legal-name", "", "Legal name")
|
||||||
cmd.Flags().StringVar(&flagAddress, "address", "", "Headquarter address")
|
cmd.Flags().StringVar(&flagAddress, "address", "", "Headquarter address")
|
||||||
cmd.Flags().StringVar(&flagWebsite, "website", "", "Website URL")
|
cmd.Flags().StringVar(&flagWebsite, "website", "", "Website URL")
|
||||||
|
cmd.Flags().StringArrayVar(&flagAdministratorID, "administrator-id", nil, "Administrator profile ID (can be repeated; empty clears)")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ query($id: ID!) {
|
|||||||
legalName
|
legalName
|
||||||
headquarterAddress
|
headquarterAddress
|
||||||
websiteUrl
|
websiteUrl
|
||||||
|
administrators {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
}
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
}
|
}
|
||||||
@@ -59,8 +63,12 @@ type viewResponse struct {
|
|||||||
LegalName *string `json:"legalName"`
|
LegalName *string `json:"legalName"`
|
||||||
HeadquarterAddress *string `json:"headquarterAddress"`
|
HeadquarterAddress *string `json:"headquarterAddress"`
|
||||||
WebsiteUrl *string `json:"websiteUrl"`
|
WebsiteUrl *string `json:"websiteUrl"`
|
||||||
CreatedAt string `json:"createdAt"`
|
Administrators []struct {
|
||||||
UpdatedAt string `json:"updatedAt"`
|
ID string `json:"id"`
|
||||||
|
FullName string `json:"fullName"`
|
||||||
|
} `json:"administrators"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +154,24 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command {
|
|||||||
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Website:"), *v.WebsiteUrl)
|
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Website:"), *v.WebsiteUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(v.Administrators) > 0 {
|
||||||
|
_, _ = fmt.Fprintf(out, "%s", label.Render("Administrators:"))
|
||||||
|
|
||||||
|
for i, a := range v.Administrators {
|
||||||
|
if i > 0 {
|
||||||
|
_, _ = fmt.Fprint(out, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.FullName != "" {
|
||||||
|
_, _ = fmt.Fprintf(out, "%s (%s)", a.FullName, a.ID)
|
||||||
|
} else {
|
||||||
|
_, _ = fmt.Fprint(out, a.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(out)
|
||||||
|
}
|
||||||
|
|
||||||
_, _ = fmt.Fprintln(out)
|
_, _ = fmt.Fprintln(out)
|
||||||
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt))
|
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt))
|
||||||
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt))
|
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt))
|
||||||
|
|||||||
70
pkg/coredata/migrations/20260727T100635Z.sql
Normal file
70
pkg/coredata/migrations/20260727T100635Z.sql
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
-- Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||||
|
--
|
||||||
|
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
-- of this software and associated documentation files (the "Software"), to deal
|
||||||
|
-- in the Software without restriction, including without limitation the rights
|
||||||
|
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
-- copies of the Software, and to permit persons to whom the Software is
|
||||||
|
-- furnished to do so, subject to the following conditions:
|
||||||
|
--
|
||||||
|
-- The above copyright notice and this permission notice shall be included in
|
||||||
|
-- all copies or substantial portions of the Software.
|
||||||
|
--
|
||||||
|
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
-- SOFTWARE.
|
||||||
|
|
||||||
|
CREATE TABLE third_party_administrators (
|
||||||
|
third_party_id text NOT NULL,
|
||||||
|
administrator_profile_id text NOT NULL,
|
||||||
|
tenant_id text NOT NULL,
|
||||||
|
organization_id text NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
updated_at timestamp with time zone NOT NULL,
|
||||||
|
PRIMARY KEY (third_party_id, administrator_profile_id),
|
||||||
|
FOREIGN KEY (third_party_id) REFERENCES third_parties(id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (administrator_profile_id) REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Migrate existing business and security owners into administrators (dedupe).
|
||||||
|
INSERT INTO third_party_administrators (
|
||||||
|
third_party_id,
|
||||||
|
administrator_profile_id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
business_owner_profile_id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM third_parties
|
||||||
|
WHERE business_owner_profile_id IS NOT NULL
|
||||||
|
ON CONFLICT (third_party_id, administrator_profile_id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO third_party_administrators (
|
||||||
|
third_party_id,
|
||||||
|
administrator_profile_id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
security_owner_profile_id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM third_parties
|
||||||
|
WHERE security_owner_profile_id IS NOT NULL
|
||||||
|
ON CONFLICT (third_party_id, administrator_profile_id) DO NOTHING;
|
||||||
@@ -166,8 +166,6 @@ type (
|
|||||||
SubprocessorsListURL *string `db:"subprocessors_list_url"`
|
SubprocessorsListURL *string `db:"subprocessors_list_url"`
|
||||||
Certifications []string `db:"certifications"`
|
Certifications []string `db:"certifications"`
|
||||||
Countries CountryCodes `db:"countries"`
|
Countries CountryCodes `db:"countries"`
|
||||||
BusinessOwnerID *gid.GID `db:"business_owner_profile_id"`
|
|
||||||
SecurityOwnerID *gid.GID `db:"security_owner_profile_id"`
|
|
||||||
StatusPageURL *string `db:"status_page_url"`
|
StatusPageURL *string `db:"status_page_url"`
|
||||||
TermsOfServiceURL *string `db:"terms_of_service_url"`
|
TermsOfServiceURL *string `db:"terms_of_service_url"`
|
||||||
SecurityPageURL *string `db:"security_page_url"`
|
SecurityPageURL *string `db:"security_page_url"`
|
||||||
@@ -263,8 +261,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -336,8 +332,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -411,8 +405,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -489,8 +481,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -566,8 +556,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -638,8 +626,6 @@ INSERT INTO
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -673,8 +659,6 @@ VALUES (
|
|||||||
@subprocessors_list_url,
|
@subprocessors_list_url,
|
||||||
@certifications,
|
@certifications,
|
||||||
@countries,
|
@countries,
|
||||||
@business_owner_profile_id,
|
|
||||||
@security_owner_profile_id,
|
|
||||||
@status_page_url,
|
@status_page_url,
|
||||||
@terms_of_service_url,
|
@terms_of_service_url,
|
||||||
@security_page_url,
|
@security_page_url,
|
||||||
@@ -710,8 +694,6 @@ VALUES (
|
|||||||
"subprocessors_list_url": v.SubprocessorsListURL,
|
"subprocessors_list_url": v.SubprocessorsListURL,
|
||||||
"certifications": v.Certifications,
|
"certifications": v.Certifications,
|
||||||
"countries": v.Countries,
|
"countries": v.Countries,
|
||||||
"business_owner_profile_id": v.BusinessOwnerID,
|
|
||||||
"security_owner_profile_id": v.SecurityOwnerID,
|
|
||||||
"status_page_url": v.StatusPageURL,
|
"status_page_url": v.StatusPageURL,
|
||||||
"terms_of_service_url": v.TermsOfServiceURL,
|
"terms_of_service_url": v.TermsOfServiceURL,
|
||||||
"security_page_url": v.SecurityPageURL,
|
"security_page_url": v.SecurityPageURL,
|
||||||
@@ -885,8 +867,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -957,8 +937,6 @@ SET
|
|||||||
terms_of_service_url = @terms_of_service_url,
|
terms_of_service_url = @terms_of_service_url,
|
||||||
security_page_url = @security_page_url,
|
security_page_url = @security_page_url,
|
||||||
trust_page_url = @trust_page_url,
|
trust_page_url = @trust_page_url,
|
||||||
business_owner_profile_id = @business_owner_profile_id,
|
|
||||||
security_owner_profile_id = @security_owner_profile_id,
|
|
||||||
show_on_trust_center = @show_on_trust_center,
|
show_on_trust_center = @show_on_trust_center,
|
||||||
level = @level,
|
level = @level,
|
||||||
vetting_status = @vetting_status,
|
vetting_status = @vetting_status,
|
||||||
@@ -994,8 +972,6 @@ WHERE %s
|
|||||||
"terms_of_service_url": v.TermsOfServiceURL,
|
"terms_of_service_url": v.TermsOfServiceURL,
|
||||||
"security_page_url": v.SecurityPageURL,
|
"security_page_url": v.SecurityPageURL,
|
||||||
"trust_page_url": v.TrustPageURL,
|
"trust_page_url": v.TrustPageURL,
|
||||||
"business_owner_profile_id": v.BusinessOwnerID,
|
|
||||||
"security_owner_profile_id": v.SecurityOwnerID,
|
|
||||||
"show_on_trust_center": v.ShowOnCompliancePortal,
|
"show_on_trust_center": v.ShowOnCompliancePortal,
|
||||||
"level": v.Level,
|
"level": v.Level,
|
||||||
"vetting_status": v.VettingStatus,
|
"vetting_status": v.VettingStatus,
|
||||||
@@ -1121,8 +1097,6 @@ WITH vend AS (
|
|||||||
v.subprocessors_list_url,
|
v.subprocessors_list_url,
|
||||||
v.certifications,
|
v.certifications,
|
||||||
v.countries,
|
v.countries,
|
||||||
v.business_owner_profile_id,
|
|
||||||
v.security_owner_profile_id,
|
|
||||||
v.status_page_url,
|
v.status_page_url,
|
||||||
v.terms_of_service_url,
|
v.terms_of_service_url,
|
||||||
v.security_page_url,
|
v.security_page_url,
|
||||||
@@ -1161,8 +1135,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -1270,8 +1242,6 @@ WITH vend AS (
|
|||||||
v.subprocessors_list_url,
|
v.subprocessors_list_url,
|
||||||
v.certifications,
|
v.certifications,
|
||||||
v.countries,
|
v.countries,
|
||||||
v.business_owner_profile_id,
|
|
||||||
v.security_owner_profile_id,
|
|
||||||
v.status_page_url,
|
v.status_page_url,
|
||||||
v.terms_of_service_url,
|
v.terms_of_service_url,
|
||||||
v.security_page_url,
|
v.security_page_url,
|
||||||
@@ -1310,8 +1280,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -1379,8 +1347,6 @@ WITH vend AS (
|
|||||||
v.subprocessors_list_url,
|
v.subprocessors_list_url,
|
||||||
v.certifications,
|
v.certifications,
|
||||||
v.countries,
|
v.countries,
|
||||||
v.business_owner_profile_id,
|
|
||||||
v.security_owner_profile_id,
|
|
||||||
v.status_page_url,
|
v.status_page_url,
|
||||||
v.terms_of_service_url,
|
v.terms_of_service_url,
|
||||||
v.security_page_url,
|
v.security_page_url,
|
||||||
@@ -1419,8 +1385,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -1555,8 +1519,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -1676,8 +1638,6 @@ WITH tps AS (
|
|||||||
v.subprocessors_list_url,
|
v.subprocessors_list_url,
|
||||||
v.certifications,
|
v.certifications,
|
||||||
v.countries,
|
v.countries,
|
||||||
v.business_owner_profile_id,
|
|
||||||
v.security_owner_profile_id,
|
|
||||||
v.status_page_url,
|
v.status_page_url,
|
||||||
v.terms_of_service_url,
|
v.terms_of_service_url,
|
||||||
v.security_page_url,
|
v.security_page_url,
|
||||||
@@ -1716,8 +1676,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -1814,8 +1772,6 @@ WITH RECURSIVE ancestor_chain AS (
|
|||||||
tp.subprocessors_list_url,
|
tp.subprocessors_list_url,
|
||||||
tp.certifications,
|
tp.certifications,
|
||||||
tp.countries,
|
tp.countries,
|
||||||
tp.business_owner_profile_id,
|
|
||||||
tp.security_owner_profile_id,
|
|
||||||
tp.status_page_url,
|
tp.status_page_url,
|
||||||
tp.terms_of_service_url,
|
tp.terms_of_service_url,
|
||||||
tp.security_page_url,
|
tp.security_page_url,
|
||||||
@@ -1859,8 +1815,6 @@ WITH RECURSIVE ancestor_chain AS (
|
|||||||
tp.subprocessors_list_url,
|
tp.subprocessors_list_url,
|
||||||
tp.certifications,
|
tp.certifications,
|
||||||
tp.countries,
|
tp.countries,
|
||||||
tp.business_owner_profile_id,
|
|
||||||
tp.security_owner_profile_id,
|
|
||||||
tp.status_page_url,
|
tp.status_page_url,
|
||||||
tp.terms_of_service_url,
|
tp.terms_of_service_url,
|
||||||
tp.security_page_url,
|
tp.security_page_url,
|
||||||
@@ -1898,8 +1852,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
@@ -1965,8 +1917,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
|
|||||||
200
pkg/coredata/third_party_administrator.go
Normal file
200
pkg/coredata/third_party_administrator.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
package coredata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ThirdPartyAdministrator struct {
|
||||||
|
ThirdPartyID gid.GID `db:"third_party_id"`
|
||||||
|
AdministratorProfileID gid.GID `db:"administrator_profile_id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThirdPartyAdministrators []*ThirdPartyAdministrator
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadByThirdPartyID loads all administrators for a third party.
|
||||||
|
func (as *ThirdPartyAdministrators) LoadByThirdPartyID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
thirdPartyID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
third_party_id,
|
||||||
|
administrator_profile_id,
|
||||||
|
organization_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM third_party_administrators
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND third_party_id = @third_party_id
|
||||||
|
ORDER BY created_at ASC, administrator_profile_id ASC;
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"third_party_id": thirdPartyID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdPartyAdministrator])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*as = result
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadByThirdPartyIDs loads all administrators for the given third parties.
|
||||||
|
func (as *ThirdPartyAdministrators) LoadByThirdPartyIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
thirdPartyIDs []gid.GID,
|
||||||
|
) error {
|
||||||
|
if len(thirdPartyIDs) == 0 {
|
||||||
|
*as = ThirdPartyAdministrators{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
third_party_id,
|
||||||
|
administrator_profile_id,
|
||||||
|
organization_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM third_party_administrators
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND third_party_id = ANY(@third_party_ids)
|
||||||
|
ORDER BY created_at ASC, administrator_profile_id ASC;
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"third_party_ids": thirdPartyIDs}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdPartyAdministrator])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*as = result
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeByThirdPartyID merges the given administrator profile IDs for a third party,
|
||||||
|
// inserting new ones, keeping existing ones, and deleting removed ones.
|
||||||
|
func (as *ThirdPartyAdministrators) MergeByThirdPartyID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Tx,
|
||||||
|
scope Scoper,
|
||||||
|
thirdPartyID gid.GID,
|
||||||
|
organizationID gid.GID,
|
||||||
|
administratorProfileIDs []gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
MERGE INTO third_party_administrators AS target
|
||||||
|
USING (
|
||||||
|
SELECT unnest(@administrator_profile_ids::text[]) AS administrator_profile_id
|
||||||
|
) AS source
|
||||||
|
ON
|
||||||
|
%[1]s
|
||||||
|
AND target.third_party_id = @third_party_id
|
||||||
|
AND target.administrator_profile_id = source.administrator_profile_id
|
||||||
|
WHEN NOT MATCHED THEN
|
||||||
|
INSERT (third_party_id, administrator_profile_id, tenant_id, organization_id, created_at, updated_at)
|
||||||
|
VALUES (@third_party_id, source.administrator_profile_id, @tenant_id, @organization_id, @now, @now)
|
||||||
|
WHEN NOT MATCHED BY SOURCE
|
||||||
|
AND %[1]s
|
||||||
|
AND target.third_party_id = @third_party_id THEN
|
||||||
|
DELETE;
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
ids := make([]string, len(administratorProfileIDs))
|
||||||
|
for i, id := range administratorProfileIDs {
|
||||||
|
ids[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"third_party_id": thirdPartyID,
|
||||||
|
"administrator_profile_ids": ids,
|
||||||
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": organizationID,
|
||||||
|
"now": now,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
if _, err := conn.Exec(ctx, q, args); err != nil {
|
||||||
|
return fmt.Errorf("cannot merge third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(ThirdPartyAdministrators, 0, len(administratorProfileIDs))
|
||||||
|
for _, profileID := range administratorProfileIDs {
|
||||||
|
result = append(
|
||||||
|
result,
|
||||||
|
&ThirdPartyAdministrator{
|
||||||
|
ThirdPartyID: thirdPartyID,
|
||||||
|
AdministratorProfileID: profileID,
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
*as = result
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -53,8 +53,6 @@ SELECT
|
|||||||
subprocessors_list_url,
|
subprocessors_list_url,
|
||||||
certifications,
|
certifications,
|
||||||
countries,
|
countries,
|
||||||
business_owner_profile_id,
|
|
||||||
security_owner_profile_id,
|
|
||||||
status_page_url,
|
status_page_url,
|
||||||
terms_of_service_url,
|
terms_of_service_url,
|
||||||
security_page_url,
|
security_page_url,
|
||||||
|
|||||||
@@ -435,8 +435,7 @@ type (
|
|||||||
TrustPageURL string
|
TrustPageURL string
|
||||||
Certifications string
|
Certifications string
|
||||||
Countries string
|
Countries string
|
||||||
BusinessOwner string
|
Administrators string
|
||||||
SecurityOwner string
|
|
||||||
Services []ThirdPartyListService
|
Services []ThirdPartyListService
|
||||||
Contacts []ThirdPartyListContact
|
Contacts []ThirdPartyListContact
|
||||||
RiskAssessments []ThirdPartyListRiskAssessment
|
RiskAssessments []ThirdPartyListRiskAssessment
|
||||||
|
|||||||
@@ -2534,31 +2534,34 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ownerIDSet := make(map[gid.GID]struct{})
|
thirdPartyIDs := make([]gid.GID, len(thirdParties))
|
||||||
ownerIDs := make([]gid.GID, 0)
|
for i, v := range thirdParties {
|
||||||
|
thirdPartyIDs[i] = v.ID
|
||||||
|
}
|
||||||
|
|
||||||
for _, v := range thirdParties {
|
var allAdministrators coredata.ThirdPartyAdministrators
|
||||||
if v.BusinessOwnerID != nil {
|
if err := allAdministrators.LoadByThirdPartyIDs(ctx, conn, scope, thirdPartyIDs); err != nil {
|
||||||
if _, ok := ownerIDSet[*v.BusinessOwnerID]; !ok {
|
return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load third party administrators: %w", err)
|
||||||
ownerIDs = append(ownerIDs, *v.BusinessOwnerID)
|
}
|
||||||
ownerIDSet[*v.BusinessOwnerID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.SecurityOwnerID != nil {
|
administratorsByThirdParty := make(map[gid.GID][]gid.GID, len(thirdParties))
|
||||||
if _, ok := ownerIDSet[*v.SecurityOwnerID]; !ok {
|
adminIDSet := make(map[gid.GID]struct{})
|
||||||
ownerIDs = append(ownerIDs, *v.SecurityOwnerID)
|
adminIDs := make([]gid.GID, 0)
|
||||||
ownerIDSet[*v.SecurityOwnerID] = struct{}{}
|
|
||||||
}
|
for _, a := range allAdministrators {
|
||||||
|
administratorsByThirdParty[a.ThirdPartyID] = append(administratorsByThirdParty[a.ThirdPartyID], a.AdministratorProfileID)
|
||||||
|
if _, ok := adminIDSet[a.AdministratorProfileID]; !ok {
|
||||||
|
adminIDs = append(adminIDs, a.AdministratorProfileID)
|
||||||
|
adminIDSet[a.AdministratorProfileID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
profileMap := make(map[gid.GID]*coredata.MembershipProfile)
|
profileMap := make(map[gid.GID]*coredata.MembershipProfile)
|
||||||
|
|
||||||
if len(ownerIDs) > 0 {
|
if len(adminIDs) > 0 {
|
||||||
var profiles coredata.MembershipProfiles
|
var profiles coredata.MembershipProfiles
|
||||||
if err := profiles.LoadByIDs(ctx, conn, scope, ownerIDs); err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
if err := profiles.LoadByIDs(ctx, conn, scope, adminIDs); err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load owner profiles: %w", err)
|
return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load administrator profiles: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range profiles {
|
for _, p := range profiles {
|
||||||
@@ -2566,11 +2569,6 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thirdPartyIDs := make([]gid.GID, len(thirdParties))
|
|
||||||
for i, v := range thirdParties {
|
|
||||||
thirdPartyIDs[i] = v.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
var allServices coredata.ThirdPartyServices
|
var allServices coredata.ThirdPartyServices
|
||||||
if err := allServices.LoadByThirdPartyIDs(ctx, conn, scope, thirdPartyIDs); err != nil {
|
if err := allServices.LoadByThirdPartyIDs(ctx, conn, scope, thirdPartyIDs); err != nil {
|
||||||
return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load thirdParty services: %w", err)
|
return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load thirdParty services: %w", err)
|
||||||
@@ -2651,8 +2649,7 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData(
|
|||||||
TrustPageURL: derefStringOrNotSpecified(v.TrustPageURL),
|
TrustPageURL: derefStringOrNotSpecified(v.TrustPageURL),
|
||||||
Certifications: joinOrNotSpecified(v.Certifications),
|
Certifications: joinOrNotSpecified(v.Certifications),
|
||||||
Countries: formatCountries(v.Countries),
|
Countries: formatCountries(v.Countries),
|
||||||
BusinessOwner: lookupProfileName(profileMap, v.BusinessOwnerID),
|
Administrators: lookupProfileNames(profileMap, administratorsByThirdParty[v.ID]),
|
||||||
SecurityOwner: lookupProfileName(profileMap, v.SecurityOwnerID),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, vs := range servicesByThirdParty[v.ID] {
|
for _, vs := range servicesByThirdParty[v.ID] {
|
||||||
@@ -2769,6 +2766,25 @@ func lookupProfileName(profiles map[gid.GID]*coredata.MembershipProfile, id *gid
|
|||||||
return "Not assigned"
|
return "Not assigned"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lookupProfileNames(profiles map[gid.GID]*coredata.MembershipProfile, ids []gid.GID) string {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return "Not assigned"
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
if p, ok := profiles[id]; ok && p.FullName != "" {
|
||||||
|
names = append(names, p.FullName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(names) == 0 {
|
||||||
|
return "Not assigned"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(names, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
func formatDataSensitivity(s coredata.DataSensitivity) string {
|
func formatDataSensitivity(s coredata.DataSensitivity) string {
|
||||||
switch s {
|
switch s {
|
||||||
case coredata.DataSensitivityNone:
|
case coredata.DataSensitivityNone:
|
||||||
|
|||||||
@@ -146,20 +146,13 @@
|
|||||||
{
|
{
|
||||||
"type": "heading",
|
"type": "heading",
|
||||||
"attrs": { "level": 3 },
|
"attrs": { "level": 3 },
|
||||||
"content": [{ "type": "text", "text": {{json (printf "2.%d.3 Owners" (add $i 1))}} }]
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.3 Administrators" (add $i 1))}} }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "paragraph",
|
"type": "paragraph",
|
||||||
"content": [
|
"content": [
|
||||||
{ "type": "text", "text": "Business Owner: ", "marks": [{ "type": "bold" }] },
|
{ "type": "text", "text": "Administrators: ", "marks": [{ "type": "bold" }] },
|
||||||
{ "type": "text", "text": {{json (default "—" $r.BusinessOwner)}} }
|
{ "type": "text", "text": {{json (default "—" $r.Administrators)}} }
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "paragraph",
|
|
||||||
"content": [
|
|
||||||
{ "type": "text", "text": "Security Owner: ", "marks": [{ "type": "bold" }] },
|
|
||||||
{ "type": "text", "text": {{json (default "—" $r.SecurityOwner)}} }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ type (
|
|||||||
TrustPageURL *string
|
TrustPageURL *string
|
||||||
TermsOfServiceURL *string
|
TermsOfServiceURL *string
|
||||||
StatusPageURL *string
|
StatusPageURL *string
|
||||||
BusinessOwnerID *gid.GID
|
AdministratorIDs []gid.GID
|
||||||
SecurityOwnerID *gid.GID
|
|
||||||
ParentThirdPartyID *gid.GID
|
ParentThirdPartyID *gid.GID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +82,7 @@ type (
|
|||||||
SecurityPageURL **string
|
SecurityPageURL **string
|
||||||
TrustPageURL **string
|
TrustPageURL **string
|
||||||
StatusPageURL **string
|
StatusPageURL **string
|
||||||
BusinessOwnerID **gid.GID
|
AdministratorIDs *[]gid.GID
|
||||||
SecurityOwnerID **gid.GID
|
|
||||||
ShowOnCompliancePortal *bool
|
ShowOnCompliancePortal *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,8 +128,11 @@ func (cvr *CreateThirdPartyRequest) Validate() error {
|
|||||||
v.Check(cvr.TrustPageURL, "trust_page_url", validator.SafeText(2048))
|
v.Check(cvr.TrustPageURL, "trust_page_url", validator.SafeText(2048))
|
||||||
v.Check(cvr.TermsOfServiceURL, "terms_of_service_url", validator.SafeText(2048))
|
v.Check(cvr.TermsOfServiceURL, "terms_of_service_url", validator.SafeText(2048))
|
||||||
v.Check(cvr.StatusPageURL, "status_page_url", validator.SafeText(2048))
|
v.Check(cvr.StatusPageURL, "status_page_url", validator.SafeText(2048))
|
||||||
v.Check(cvr.BusinessOwnerID, "business_owner_id", validator.GID(coredata.MembershipProfileEntityType))
|
v.Check(len(cvr.AdministratorIDs), "administrator_ids", validator.Max(100))
|
||||||
v.Check(cvr.SecurityOwnerID, "security_owner_id", validator.GID(coredata.MembershipProfileEntityType))
|
v.Check(cvr.AdministratorIDs, "administrator_ids", validator.NoDuplicates())
|
||||||
|
v.CheckEach(cvr.AdministratorIDs, "administrator_ids", func(_ int, item any) {
|
||||||
|
v.Check(item, "administrator_ids", validator.GID(coredata.MembershipProfileEntityType))
|
||||||
|
})
|
||||||
|
|
||||||
return v.Error()
|
return v.Error()
|
||||||
}
|
}
|
||||||
@@ -155,8 +156,14 @@ func (uvr *UpdateThirdPartyRequest) Validate() error {
|
|||||||
v.Check(uvr.TrustPageURL, "trust_page_url", validator.SafeText(2048))
|
v.Check(uvr.TrustPageURL, "trust_page_url", validator.SafeText(2048))
|
||||||
v.Check(uvr.TermsOfServiceURL, "terms_of_service_url", validator.SafeText(2048))
|
v.Check(uvr.TermsOfServiceURL, "terms_of_service_url", validator.SafeText(2048))
|
||||||
v.Check(uvr.StatusPageURL, "status_page_url", validator.SafeText(2048))
|
v.Check(uvr.StatusPageURL, "status_page_url", validator.SafeText(2048))
|
||||||
v.Check(uvr.BusinessOwnerID, "business_owner_id", validator.GID(coredata.MembershipProfileEntityType))
|
|
||||||
v.Check(uvr.SecurityOwnerID, "security_owner_id", validator.GID(coredata.MembershipProfileEntityType))
|
if uvr.AdministratorIDs != nil {
|
||||||
|
v.Check(len(*uvr.AdministratorIDs), "administrator_ids", validator.Max(100))
|
||||||
|
v.Check(*uvr.AdministratorIDs, "administrator_ids", validator.NoDuplicates())
|
||||||
|
v.CheckEach(*uvr.AdministratorIDs, "administrator_ids", func(_ int, item any) {
|
||||||
|
v.Check(item, "administrator_ids", validator.GID(coredata.MembershipProfileEntityType))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return v.Error()
|
return v.Error()
|
||||||
}
|
}
|
||||||
@@ -368,7 +375,12 @@ func (s ThirdPartyService) Update(
|
|||||||
return fmt.Errorf("cannot load thirdParty %q: %w", req.ID, err)
|
return fmt.Errorf("cannot load thirdParty %q: %w", req.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
previousThirdParty := webhooktypes.NewThirdParty(thirdParty)
|
previousAdministratorIDs, err := loadThirdPartyAdministratorIDs(ctx, conn, scope, thirdParty.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
previousThirdParty := webhooktypes.NewThirdParty(thirdParty, previousAdministratorIDs)
|
||||||
|
|
||||||
if req.Name != nil {
|
if req.Name != nil {
|
||||||
thirdParty.Name = *req.Name
|
thirdParty.Name = *req.Name
|
||||||
@@ -446,29 +458,24 @@ func (s ThirdPartyService) Update(
|
|||||||
thirdParty.Countries = req.Countries
|
thirdParty.Countries = req.Countries
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.BusinessOwnerID != nil {
|
if req.AdministratorIDs != nil {
|
||||||
if *req.BusinessOwnerID != nil {
|
if len(*req.AdministratorIDs) > 0 {
|
||||||
businessOwner := &coredata.MembershipProfile{}
|
profiles := coredata.MembershipProfiles{}
|
||||||
if err := businessOwner.LoadByID(ctx, conn, scope, **req.BusinessOwnerID); err != nil {
|
if err := profiles.LoadByIDs(ctx, conn, scope, *req.AdministratorIDs); err != nil {
|
||||||
return fmt.Errorf("cannot load business owner profile: %w", err)
|
return fmt.Errorf("cannot load administrator profiles: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
thirdParty.BusinessOwnerID = &businessOwner.ID
|
|
||||||
} else {
|
|
||||||
thirdParty.BusinessOwnerID = nil
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if req.SecurityOwnerID != nil {
|
administrators := &coredata.ThirdPartyAdministrators{}
|
||||||
if *req.SecurityOwnerID != nil {
|
if err := administrators.MergeByThirdPartyID(
|
||||||
securityOwner := &coredata.MembershipProfile{}
|
ctx,
|
||||||
if err := securityOwner.LoadByID(ctx, conn, scope, **req.SecurityOwnerID); err != nil {
|
conn,
|
||||||
return fmt.Errorf("cannot load security owner profile: %w", err)
|
scope,
|
||||||
}
|
thirdParty.ID,
|
||||||
|
thirdParty.OrganizationID,
|
||||||
thirdParty.SecurityOwnerID = &securityOwner.ID
|
*req.AdministratorIDs,
|
||||||
} else {
|
); err != nil {
|
||||||
thirdParty.SecurityOwnerID = nil
|
return fmt.Errorf("cannot merge third party administrators: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,13 +485,18 @@ func (s ThirdPartyService) Update(
|
|||||||
return fmt.Errorf("cannot update thirdParty: %w", err)
|
return fmt.Errorf("cannot update thirdParty: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
administratorIDs, err := loadThirdPartyAdministratorIDs(ctx, conn, scope, thirdParty.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := webhook.InsertUpdateData(
|
if err := webhook.InsertUpdateData(
|
||||||
ctx,
|
ctx,
|
||||||
conn,
|
conn,
|
||||||
scope,
|
scope,
|
||||||
thirdParty.OrganizationID,
|
thirdParty.OrganizationID,
|
||||||
coredata.WebhookEventTypeThirdPartyUpdated,
|
coredata.WebhookEventTypeThirdPartyUpdated,
|
||||||
webhooktypes.NewThirdParty(thirdParty),
|
webhooktypes.NewThirdParty(thirdParty, administratorIDs),
|
||||||
previousThirdParty,
|
previousThirdParty,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("cannot insert webhook event: %w", err)
|
return fmt.Errorf("cannot insert webhook event: %w", err)
|
||||||
@@ -560,13 +572,18 @@ func (s ThirdPartyService) Delete(
|
|||||||
return fmt.Errorf("cannot load thirdParty: %w", err)
|
return fmt.Errorf("cannot load thirdParty: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
administratorIDs, err := loadThirdPartyAdministratorIDs(ctx, conn, scope, thirdParty.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := webhook.InsertData(
|
if err := webhook.InsertData(
|
||||||
ctx,
|
ctx,
|
||||||
conn,
|
conn,
|
||||||
scope,
|
scope,
|
||||||
thirdParty.OrganizationID,
|
thirdParty.OrganizationID,
|
||||||
coredata.WebhookEventTypeThirdPartyDeleted,
|
coredata.WebhookEventTypeThirdPartyDeleted,
|
||||||
webhooktypes.NewThirdParty(thirdParty),
|
webhooktypes.NewThirdParty(thirdParty, administratorIDs),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("cannot insert webhook event: %w", err)
|
return fmt.Errorf("cannot insert webhook event: %w", err)
|
||||||
}
|
}
|
||||||
@@ -642,24 +659,6 @@ func (s ThirdPartyService) Create(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.BusinessOwnerID != nil {
|
|
||||||
businessOwner := &coredata.MembershipProfile{}
|
|
||||||
if err := businessOwner.LoadByID(ctx, conn, scope, *req.BusinessOwnerID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load business owner profile: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
thirdParty.BusinessOwnerID = &businessOwner.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.SecurityOwnerID != nil {
|
|
||||||
securityOwner := &coredata.MembershipProfile{}
|
|
||||||
if err := securityOwner.LoadByID(ctx, conn, scope, *req.SecurityOwnerID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load security owner profile: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
thirdParty.SecurityOwnerID = &securityOwner.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Category != nil {
|
if req.Category != nil {
|
||||||
thirdParty.Category = *req.Category
|
thirdParty.Category = *req.Category
|
||||||
} else {
|
} else {
|
||||||
@@ -670,13 +669,32 @@ func (s ThirdPartyService) Create(
|
|||||||
return fmt.Errorf("cannot insert thirdParty: %w", err)
|
return fmt.Errorf("cannot insert thirdParty: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(req.AdministratorIDs) > 0 {
|
||||||
|
profiles := coredata.MembershipProfiles{}
|
||||||
|
if err := profiles.LoadByIDs(ctx, conn, scope, req.AdministratorIDs); err != nil {
|
||||||
|
return fmt.Errorf("cannot load administrator profiles: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
administrators := &coredata.ThirdPartyAdministrators{}
|
||||||
|
if err := administrators.MergeByThirdPartyID(
|
||||||
|
ctx,
|
||||||
|
conn,
|
||||||
|
scope,
|
||||||
|
thirdParty.ID,
|
||||||
|
organization.ID,
|
||||||
|
req.AdministratorIDs,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("cannot merge third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := webhook.InsertData(
|
if err := webhook.InsertData(
|
||||||
ctx,
|
ctx,
|
||||||
conn,
|
conn,
|
||||||
scope,
|
scope,
|
||||||
organization.ID,
|
organization.ID,
|
||||||
coredata.WebhookEventTypeThirdPartyCreated,
|
coredata.WebhookEventTypeThirdPartyCreated,
|
||||||
webhooktypes.NewThirdParty(thirdParty),
|
webhooktypes.NewThirdParty(thirdParty, req.AdministratorIDs),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("cannot insert webhook event: %w", err)
|
return fmt.Errorf("cannot insert webhook event: %w", err)
|
||||||
}
|
}
|
||||||
@@ -786,7 +804,7 @@ func (s ThirdPartyService) ImportFromCommon(
|
|||||||
scope,
|
scope,
|
||||||
organization.ID,
|
organization.ID,
|
||||||
coredata.WebhookEventTypeThirdPartyCreated,
|
coredata.WebhookEventTypeThirdPartyCreated,
|
||||||
webhooktypes.NewThirdParty(thirdParty),
|
webhooktypes.NewThirdParty(thirdParty, nil),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("cannot insert webhook event: %w", err)
|
return fmt.Errorf("cannot insert webhook event: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1076,3 +1094,50 @@ func (s ThirdPartyService) ListForParentThirdPartyID(
|
|||||||
|
|
||||||
return page.NewPage(thirdParties, cursor), nil
|
return page.NewPage(thirdParties, cursor), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s ThirdPartyService) MapAdministratorIDsForThirdPartyIDs(
|
||||||
|
ctx context.Context, scope coredata.Scoper,
|
||||||
|
thirdPartyIDs []gid.GID,
|
||||||
|
) (map[gid.GID][]gid.GID, error) {
|
||||||
|
result := make(map[gid.GID][]gid.GID, len(thirdPartyIDs))
|
||||||
|
if len(thirdPartyIDs) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var administrators coredata.ThirdPartyAdministrators
|
||||||
|
|
||||||
|
err := s.svc.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
return administrators.LoadByThirdPartyIDs(ctx, conn, scope, thirdPartyIDs)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range administrators {
|
||||||
|
result[a.ThirdPartyID] = append(result[a.ThirdPartyID], a.AdministratorProfileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadThirdPartyAdministratorIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope coredata.Scoper,
|
||||||
|
thirdPartyID gid.GID,
|
||||||
|
) ([]gid.GID, error) {
|
||||||
|
administrators := &coredata.ThirdPartyAdministrators{}
|
||||||
|
if err := administrators.LoadByThirdPartyID(ctx, conn, scope, thirdPartyID); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]gid.GID, len(*administrators))
|
||||||
|
for i, a := range *administrators {
|
||||||
|
ids[i] = a.AdministratorProfileID
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,21 +58,22 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
Loaders struct {
|
Loaders struct {
|
||||||
Organization *dataloadgen.Loader[gid.GID, *coredata.Organization]
|
Organization *dataloadgen.Loader[gid.GID, *coredata.Organization]
|
||||||
Framework *dataloadgen.Loader[gid.GID, *coredata.Framework]
|
Framework *dataloadgen.Loader[gid.GID, *coredata.Framework]
|
||||||
Control *dataloadgen.Loader[gid.GID, *coredata.Control]
|
Control *dataloadgen.Loader[gid.GID, *coredata.Control]
|
||||||
ThirdParty *dataloadgen.Loader[gid.GID, *coredata.ThirdParty]
|
ThirdParty *dataloadgen.Loader[gid.GID, *coredata.ThirdParty]
|
||||||
Document *dataloadgen.Loader[gid.GID, *coredata.Document]
|
Document *dataloadgen.Loader[gid.GID, *coredata.Document]
|
||||||
Profile *dataloadgen.Loader[gid.GID, *coredata.MembershipProfile]
|
Profile *dataloadgen.Loader[gid.GID, *coredata.MembershipProfile]
|
||||||
Risk *dataloadgen.Loader[gid.GID, *coredata.Risk]
|
Risk *dataloadgen.Loader[gid.GID, *coredata.Risk]
|
||||||
Measure *dataloadgen.Loader[gid.GID, *coredata.Measure]
|
Measure *dataloadgen.Loader[gid.GID, *coredata.Measure]
|
||||||
Task *dataloadgen.Loader[gid.GID, *coredata.Task]
|
Task *dataloadgen.Loader[gid.GID, *coredata.Task]
|
||||||
File *dataloadgen.Loader[gid.GID, *coredata.File]
|
File *dataloadgen.Loader[gid.GID, *coredata.File]
|
||||||
CookieBanner *dataloadgen.Loader[gid.GID, *coredata.CookieBanner]
|
CookieBanner *dataloadgen.Loader[gid.GID, *coredata.CookieBanner]
|
||||||
CookieCategory *dataloadgen.Loader[gid.GID, *coredata.CookieCategory]
|
CookieCategory *dataloadgen.Loader[gid.GID, *coredata.CookieCategory]
|
||||||
CommonTrackerPattern *dataloadgen.Loader[gid.GID, *coredata.CommonTrackerPattern]
|
CommonTrackerPattern *dataloadgen.Loader[gid.GID, *coredata.CommonTrackerPattern]
|
||||||
CommonThirdParty *dataloadgen.Loader[gid.GID, *coredata.CommonThirdParty]
|
CommonThirdParty *dataloadgen.Loader[gid.GID, *coredata.CommonThirdParty]
|
||||||
Authorize *dataloadgen.Loader[AuthorizeKey, AuthorizeResult]
|
ThirdPartyAdministratorIDs *dataloadgen.Loader[gid.GID, []gid.GID]
|
||||||
|
Authorize *dataloadgen.Loader[AuthorizeKey, AuthorizeResult]
|
||||||
}
|
}
|
||||||
|
|
||||||
batchFetcher struct {
|
batchFetcher struct {
|
||||||
@@ -109,20 +110,21 @@ func NewMiddleware(proboSvc *probo.Service, iamSvc *iam.Service, cookieBannerSvc
|
|||||||
|
|
||||||
func (f *batchFetcher) newLoaders() *Loaders {
|
func (f *batchFetcher) newLoaders() *Loaders {
|
||||||
return &Loaders{
|
return &Loaders{
|
||||||
Organization: dataloadgen.NewMappedLoader(f.fetchOrganizations),
|
Organization: dataloadgen.NewMappedLoader(f.fetchOrganizations),
|
||||||
Framework: dataloadgen.NewMappedLoader(f.fetchFrameworks),
|
Framework: dataloadgen.NewMappedLoader(f.fetchFrameworks),
|
||||||
Control: dataloadgen.NewMappedLoader(f.fetchControls),
|
Control: dataloadgen.NewMappedLoader(f.fetchControls),
|
||||||
ThirdParty: dataloadgen.NewMappedLoader(f.fetchThirdParties),
|
ThirdParty: dataloadgen.NewMappedLoader(f.fetchThirdParties),
|
||||||
Document: dataloadgen.NewMappedLoader(f.fetchDocuments),
|
Document: dataloadgen.NewMappedLoader(f.fetchDocuments),
|
||||||
Profile: dataloadgen.NewMappedLoader(f.fetchProfiles),
|
Profile: dataloadgen.NewMappedLoader(f.fetchProfiles),
|
||||||
Risk: dataloadgen.NewMappedLoader(f.fetchRisks),
|
Risk: dataloadgen.NewMappedLoader(f.fetchRisks),
|
||||||
Measure: dataloadgen.NewMappedLoader(f.fetchMeasures),
|
Measure: dataloadgen.NewMappedLoader(f.fetchMeasures),
|
||||||
Task: dataloadgen.NewMappedLoader(f.fetchTasks),
|
Task: dataloadgen.NewMappedLoader(f.fetchTasks),
|
||||||
File: dataloadgen.NewMappedLoader(f.fetchFiles),
|
File: dataloadgen.NewMappedLoader(f.fetchFiles),
|
||||||
CookieBanner: dataloadgen.NewMappedLoader(f.fetchCookieBanners),
|
CookieBanner: dataloadgen.NewMappedLoader(f.fetchCookieBanners),
|
||||||
CookieCategory: dataloadgen.NewMappedLoader(f.fetchCookieCategories),
|
CookieCategory: dataloadgen.NewMappedLoader(f.fetchCookieCategories),
|
||||||
CommonTrackerPattern: dataloadgen.NewMappedLoader(f.fetchCommonTrackerPatterns),
|
CommonTrackerPattern: dataloadgen.NewMappedLoader(f.fetchCommonTrackerPatterns),
|
||||||
CommonThirdParty: dataloadgen.NewMappedLoader(f.fetchCommonThirdParties),
|
CommonThirdParty: dataloadgen.NewMappedLoader(f.fetchCommonThirdParties),
|
||||||
|
ThirdPartyAdministratorIDs: dataloadgen.NewMappedLoader(f.fetchThirdPartyAdministratorIDs),
|
||||||
Authorize: dataloadgen.NewMappedLoader(
|
Authorize: dataloadgen.NewMappedLoader(
|
||||||
f.fetchAuthorizes,
|
f.fetchAuthorizes,
|
||||||
dataloadgen.WithoutCache(),
|
dataloadgen.WithoutCache(),
|
||||||
@@ -350,6 +352,26 @@ func (f *batchFetcher) fetchCommonThirdParties(ctx context.Context, keys []gid.G
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *batchFetcher) fetchThirdPartyAdministratorIDs(ctx context.Context, keys []gid.GID) (map[gid.GID][]gid.GID, error) {
|
||||||
|
scope := coredata.NewScopeFromObjectID(keys[0])
|
||||||
|
|
||||||
|
administratorIDsByThirdPartyID, err := f.probo.ThirdParties.MapAdministratorIDsForThirdPartyIDs(ctx, scope, keys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot batch load third party administrator ids: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[gid.GID][]gid.GID, len(keys))
|
||||||
|
for _, id := range keys {
|
||||||
|
if ids, ok := administratorIDsByThirdPartyID[id]; ok {
|
||||||
|
result[id] = ids
|
||||||
|
} else {
|
||||||
|
result[id] = []gid.GID{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// fetchAuthorizes evaluates the batch with a single AuthorizeMulti call and
|
// fetchAuthorizes evaluates the batch with a single AuthorizeMulti call and
|
||||||
// surfaces per-key denials via dataloadgen.MappedFetchError. When
|
// surfaces per-key denials via dataloadgen.MappedFetchError. When
|
||||||
// AuthorizeMulti cannot evaluate the batch as a whole (e.g. mixed
|
// AuthorizeMulti cannot evaluate the batch as a whole (e.g. mixed
|
||||||
|
|||||||
@@ -285,8 +285,7 @@ type ThirdParty implements Node {
|
|||||||
filter: MeasureFilter
|
filter: MeasureFilter
|
||||||
): MeasureConnection! @goField(forceResolver: true)
|
): MeasureConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
businessOwner: Profile @goField(forceResolver: true)
|
administrators: [Profile!]! @goField(forceResolver: true)
|
||||||
securityOwner: Profile @goField(forceResolver: true)
|
|
||||||
|
|
||||||
statusPageUrl: String
|
statusPageUrl: String
|
||||||
termsOfServiceUrl: String
|
termsOfServiceUrl: String
|
||||||
@@ -540,8 +539,7 @@ input CreateThirdPartyInput {
|
|||||||
trustPageUrl: String
|
trustPageUrl: String
|
||||||
statusPageUrl: String
|
statusPageUrl: String
|
||||||
termsOfServiceUrl: String
|
termsOfServiceUrl: String
|
||||||
businessOwnerId: ID
|
administratorIds: [ID!]
|
||||||
securityOwnerId: ID
|
|
||||||
parentThirdPartyId: ID
|
parentThirdPartyId: ID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,8 +562,7 @@ input UpdateThirdPartyInput {
|
|||||||
countries: [CountryCode!]
|
countries: [CountryCode!]
|
||||||
securityPageUrl: String @goField(omittable: true)
|
securityPageUrl: String @goField(omittable: true)
|
||||||
trustPageUrl: String @goField(omittable: true)
|
trustPageUrl: String @goField(omittable: true)
|
||||||
businessOwnerId: ID @goField(omittable: true)
|
administratorIds: [ID!]
|
||||||
securityOwnerId: ID @goField(omittable: true)
|
|
||||||
showOnCompliancePortal: Boolean
|
showOnCompliancePortal: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/vikstrous/dataloadgen"
|
"github.com/vikstrous/dataloadgen"
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/iam"
|
"go.probo.inc/probo/pkg/iam"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
"go.probo.inc/probo/pkg/probo"
|
"go.probo.inc/probo/pkg/probo"
|
||||||
@@ -58,8 +59,7 @@ func (r *mutationResolver) CreateThirdParty(ctx context.Context, input types.Cre
|
|||||||
Certifications: input.Certifications,
|
Certifications: input.Certifications,
|
||||||
SecurityPageURL: input.SecurityPageURL,
|
SecurityPageURL: input.SecurityPageURL,
|
||||||
TrustPageURL: input.TrustPageURL,
|
TrustPageURL: input.TrustPageURL,
|
||||||
BusinessOwnerID: input.BusinessOwnerID,
|
AdministratorIDs: input.AdministratorIds,
|
||||||
SecurityOwnerID: input.SecurityOwnerID,
|
|
||||||
Countries: input.Countries,
|
Countries: input.Countries,
|
||||||
ParentThirdPartyID: input.ParentThirdPartyID,
|
ParentThirdPartyID: input.ParentThirdPartyID,
|
||||||
},
|
},
|
||||||
@@ -120,6 +120,11 @@ func (r *mutationResolver) UpdateThirdParty(ctx context.Context, input types.Upd
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var administratorIDs *[]gid.GID
|
||||||
|
if input.AdministratorIds != nil {
|
||||||
|
administratorIDs = &input.AdministratorIds
|
||||||
|
}
|
||||||
|
|
||||||
thirdParty, err := r.probo.ThirdParties.Update(
|
thirdParty, err := r.probo.ThirdParties.Update(
|
||||||
ctx, scope,
|
ctx, scope,
|
||||||
probo.UpdateThirdPartyRequest{
|
probo.UpdateThirdPartyRequest{
|
||||||
@@ -140,8 +145,7 @@ func (r *mutationResolver) UpdateThirdParty(ctx context.Context, input types.Upd
|
|||||||
WebsiteURL: gqlutils.UnwrapOmittable(input.WebsiteURL),
|
WebsiteURL: gqlutils.UnwrapOmittable(input.WebsiteURL),
|
||||||
Category: input.Category,
|
Category: input.Category,
|
||||||
Certifications: input.Certifications,
|
Certifications: input.Certifications,
|
||||||
BusinessOwnerID: gqlutils.UnwrapOmittable(input.BusinessOwnerID),
|
AdministratorIDs: administratorIDs,
|
||||||
SecurityOwnerID: gqlutils.UnwrapOmittable(input.SecurityOwnerID),
|
|
||||||
ShowOnCompliancePortal: input.ShowOnCompliancePortal,
|
ShowOnCompliancePortal: input.ShowOnCompliancePortal,
|
||||||
Countries: input.Countries,
|
Countries: input.Countries,
|
||||||
},
|
},
|
||||||
@@ -855,56 +859,45 @@ func (r *thirdPartyResolver) Measures(ctx context.Context, obj *types.ThirdParty
|
|||||||
return types.NewMeasureConnection(page, r, obj.ID, measureFilter), nil
|
return types.NewMeasureConnection(page, r, obj.ID, measureFilter), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BusinessOwner is the resolver for the businessOwner field.
|
// Administrators is the resolver for the administrators field.
|
||||||
func (r *thirdPartyResolver) BusinessOwner(ctx context.Context, obj *types.ThirdParty) (*types.Profile, error) {
|
func (r *thirdPartyResolver) Administrators(ctx context.Context, obj *types.ThirdParty) ([]*types.Profile, error) {
|
||||||
if obj.BusinessOwner == nil {
|
if _, err := r.authorize(ctx, obj.ID, probo.ActionThirdPartyGet); err != nil {
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := r.authorize(ctx, obj.BusinessOwner.ID, iam.ActionMembershipProfileGet); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
loaders := dataloader.FromContext(ctx)
|
loaders := dataloader.FromContext(ctx)
|
||||||
|
|
||||||
businessOwner, err := loaders.Profile.Load(ctx, obj.BusinessOwner.ID)
|
administratorIDs, err := loaders.ThirdPartyAdministratorIDs.Load(ctx, obj.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
|
r.logger.ErrorCtx(ctx, "cannot get third party administrator ids", log.Error(err))
|
||||||
return nil, gqlutils.NotFound(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot get business owner", log.Error(err))
|
|
||||||
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewProfile(businessOwner), nil
|
if len(administratorIDs) == 0 {
|
||||||
}
|
return []*types.Profile{}, nil
|
||||||
|
|
||||||
// SecurityOwner is the resolver for the securityOwner field.
|
|
||||||
func (r *thirdPartyResolver) SecurityOwner(ctx context.Context, obj *types.ThirdParty) (*types.Profile, error) {
|
|
||||||
if obj.SecurityOwner == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := r.authorize(ctx, obj.SecurityOwner.ID, iam.ActionMembershipProfileGet); err != nil {
|
if _, err := r.batchAuthorize(ctx, iam.ActionMembershipProfileGet, administratorIDs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
loaders := dataloader.FromContext(ctx)
|
profiles, err := loaders.Profile.LoadAll(ctx, administratorIDs)
|
||||||
|
|
||||||
securityOwner, err := loaders.Profile.Load(ctx, obj.SecurityOwner.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
|
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
|
||||||
return nil, gqlutils.NotFound(ctx, err)
|
return nil, gqlutils.NotFound(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot get security owner", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot get third party administrators", log.Error(err))
|
||||||
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewProfile(securityOwner), nil
|
result := make([]*types.Profile, len(profiles))
|
||||||
|
for i, p := range profiles {
|
||||||
|
result[i] = types.NewProfile(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParentThirdParty is the resolver for the parentThirdParty field.
|
// ParentThirdParty is the resolver for the parentThirdParty field.
|
||||||
|
|||||||
@@ -98,18 +98,6 @@ func NewThirdParty(v *coredata.ThirdParty) *ThirdParty {
|
|||||||
CreatedAt: v.CreatedAt,
|
CreatedAt: v.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.BusinessOwnerID != nil {
|
|
||||||
object.BusinessOwner = &Profile{
|
|
||||||
ID: *v.BusinessOwnerID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.SecurityOwnerID != nil {
|
|
||||||
object.SecurityOwner = &Profile{
|
|
||||||
ID: *v.SecurityOwnerID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.ParentThirdPartyID != nil {
|
if v.ParentThirdPartyID != nil {
|
||||||
object.ParentThirdParty = &ThirdParty{
|
object.ParentThirdParty = &ThirdParty{
|
||||||
ID: *v.ParentThirdPartyID,
|
ID: *v.ParentThirdPartyID,
|
||||||
|
|||||||
@@ -90,7 +90,17 @@ func (r *Resolver) ListThirdPartiesTool(ctx context.Context, req *mcp.CallToolRe
|
|||||||
panic(fmt.Errorf("cannot list organization thirdParties: %w", err))
|
panic(fmt.Errorf("cannot list organization thirdParties: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, types.NewListThirdPartiesOutput(page), nil
|
thirdPartyIDs := make([]gid.GID, len(page.Data))
|
||||||
|
for i, tp := range page.Data {
|
||||||
|
thirdPartyIDs[i] = tp.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
administratorIDsByThirdPartyID, err := prb.ThirdParties.MapAdministratorIDsForThirdPartyIDs(ctx, scope, thirdPartyIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.ListThirdPartiesOutput{}, fmt.Errorf("cannot load third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.NewListThirdPartiesOutput(page, administratorIDsByThirdPartyID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddThirdPartyTool handles the addThirdParty tool
|
// AddThirdPartyTool handles the addThirdParty tool
|
||||||
@@ -135,8 +145,7 @@ func (r *Resolver) AddThirdPartyTool(ctx context.Context, req *mcp.CallToolReque
|
|||||||
SubprocessorsListURL: input.SubprocessorsListURL,
|
SubprocessorsListURL: input.SubprocessorsListURL,
|
||||||
Certifications: input.Certifications,
|
Certifications: input.Certifications,
|
||||||
Countries: countries,
|
Countries: countries,
|
||||||
BusinessOwnerID: input.BusinessOwnerID,
|
AdministratorIDs: input.AdministratorIds,
|
||||||
SecurityOwnerID: input.SecurityOwnerID,
|
|
||||||
StatusPageURL: input.StatusPageURL,
|
StatusPageURL: input.StatusPageURL,
|
||||||
TermsOfServiceURL: input.TermsOfServiceURL,
|
TermsOfServiceURL: input.TermsOfServiceURL,
|
||||||
SecurityPageURL: input.SecurityPageURL,
|
SecurityPageURL: input.SecurityPageURL,
|
||||||
@@ -147,7 +156,7 @@ func (r *Resolver) AddThirdPartyTool(ctx context.Context, req *mcp.CallToolReque
|
|||||||
return nil, types.AddThirdPartyOutput{}, fmt.Errorf("failed to create thirdParty: %w", err)
|
return nil, types.AddThirdPartyOutput{}, fmt.Errorf("failed to create thirdParty: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, types.NewAddThirdPartyOutput(thirdParty), nil
|
return nil, types.NewAddThirdPartyOutput(thirdParty, input.AdministratorIds), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateThirdPartyTool handles the updateThirdParty tool
|
// UpdateThirdPartyTool handles the updateThirdParty tool
|
||||||
@@ -225,14 +234,9 @@ func (r *Resolver) UpdateThirdPartyTool(ctx context.Context, req *mcp.CallToolRe
|
|||||||
trustPageURL = &input.TrustPageURL
|
trustPageURL = &input.TrustPageURL
|
||||||
}
|
}
|
||||||
|
|
||||||
var businessOwnerID **gid.GID
|
var administratorIDs *[]gid.GID
|
||||||
if input.BusinessOwnerID != nil {
|
if input.AdministratorIds != nil {
|
||||||
businessOwnerID = &input.BusinessOwnerID
|
administratorIDs = &input.AdministratorIds
|
||||||
}
|
|
||||||
|
|
||||||
var securityOwnerID **gid.GID
|
|
||||||
if input.SecurityOwnerID != nil {
|
|
||||||
securityOwnerID = &input.SecurityOwnerID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var category *coredata.ThirdPartyCategory
|
var category *coredata.ThirdPartyCategory
|
||||||
@@ -267,8 +271,7 @@ func (r *Resolver) UpdateThirdPartyTool(ctx context.Context, req *mcp.CallToolRe
|
|||||||
SubprocessorsListURL: subprocessorsListURL,
|
SubprocessorsListURL: subprocessorsListURL,
|
||||||
Certifications: input.Certifications,
|
Certifications: input.Certifications,
|
||||||
Countries: countries,
|
Countries: countries,
|
||||||
BusinessOwnerID: businessOwnerID,
|
AdministratorIDs: administratorIDs,
|
||||||
SecurityOwnerID: securityOwnerID,
|
|
||||||
StatusPageURL: statusPageURL,
|
StatusPageURL: statusPageURL,
|
||||||
TermsOfServiceURL: termsOfServiceURL,
|
TermsOfServiceURL: termsOfServiceURL,
|
||||||
SecurityPageURL: securityPageURL,
|
SecurityPageURL: securityPageURL,
|
||||||
@@ -279,7 +282,12 @@ func (r *Resolver) UpdateThirdPartyTool(ctx context.Context, req *mcp.CallToolRe
|
|||||||
return nil, types.UpdateThirdPartyOutput{}, fmt.Errorf("failed to update thirdParty: %w", err)
|
return nil, types.UpdateThirdPartyOutput{}, fmt.Errorf("failed to update thirdParty: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, types.NewUpdateThirdPartyOutput(thirdParty), nil
|
administratorIDsByThirdPartyID, err := svc.ThirdParties.MapAdministratorIDsForThirdPartyIDs(ctx, scope, []gid.GID{thirdParty.ID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.UpdateThirdPartyOutput{}, fmt.Errorf("cannot load third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.NewUpdateThirdPartyOutput(thirdParty, administratorIDsByThirdPartyID[thirdParty.ID]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) ListRisksTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRisksInput) (*mcp.CallToolResult, types.ListRisksOutput, error) {
|
func (r *Resolver) ListRisksTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRisksInput) (*mcp.CallToolResult, types.ListRisksOutput, error) {
|
||||||
@@ -5414,8 +5422,13 @@ func (r *Resolver) VetThirdPartyTool(ctx context.Context, req *mcp.CallToolReque
|
|||||||
return nil, types.VetThirdPartyOutput{}, fmt.Errorf("internal server error")
|
return nil, types.VetThirdPartyOutput{}, fmt.Errorf("internal server error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
administratorIDsByThirdPartyID, err := r.proboSvc.ThirdParties.MapAdministratorIDsForThirdPartyIDs(ctx, scope, []gid.GID{thirdParty.ID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.VetThirdPartyOutput{}, fmt.Errorf("cannot load third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil, types.VetThirdPartyOutput{
|
return nil, types.VetThirdPartyOutput{
|
||||||
ThirdParty: types.NewThirdParty(thirdParty),
|
ThirdParty: types.NewThirdParty(thirdParty, administratorIDsByThirdPartyID[thirdParty.ID]),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6318,7 +6331,17 @@ func (r *Resolver) ListChildThirdPartiesTool(ctx context.Context, req *mcp.CallT
|
|||||||
panic(fmt.Errorf("cannot list child third parties: %w", err))
|
panic(fmt.Errorf("cannot list child third parties: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, types.NewListChildThirdPartiesOutput(page), nil
|
thirdPartyIDs := make([]gid.GID, len(page.Data))
|
||||||
|
for i, tp := range page.Data {
|
||||||
|
thirdPartyIDs[i] = tp.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
administratorIDsByThirdPartyID, err := r.proboSvc.ThirdParties.MapAdministratorIDsForThirdPartyIDs(ctx, scope, thirdPartyIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.ListChildThirdPartiesOutput{}, fmt.Errorf("cannot load third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.NewListChildThirdPartiesOutput(page, administratorIDsByThirdPartyID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) ListRiskAssessmentsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskAssessmentsInput) (*mcp.CallToolResult, types.ListRiskAssessmentsOutput, error) {
|
func (r *Resolver) ListRiskAssessmentsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskAssessmentsInput) (*mcp.CallToolResult, types.ListRiskAssessmentsOutput, error) {
|
||||||
@@ -7658,8 +7681,13 @@ func (r *Resolver) GetThirdPartyTool(ctx context.Context, req *mcp.CallToolReque
|
|||||||
return nil, types.GetThirdPartyOutput{}, fmt.Errorf("cannot get third party: %w", err)
|
return nil, types.GetThirdPartyOutput{}, fmt.Errorf("cannot get third party: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
administratorIDsByThirdPartyID, err := r.proboSvc.ThirdParties.MapAdministratorIDsForThirdPartyIDs(ctx, scope, []gid.GID{thirdParty.ID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.GetThirdPartyOutput{}, fmt.Errorf("cannot load third party administrators: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil, types.GetThirdPartyOutput{
|
return nil, types.GetThirdPartyOutput{
|
||||||
ThirdParty: types.NewThirdParty(thirdParty),
|
ThirdParty: types.NewThirdParty(thirdParty, administratorIDsByThirdPartyID[thirdParty.ID]),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -825,16 +825,11 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
description: Countries or regions (ISO 3166-1 alpha-2 country codes, EU, or GLOBAL)
|
description: Countries or regions (ISO 3166-1 alpha-2 country codes, EU, or GLOBAL)
|
||||||
business_owner_id:
|
administrator_ids:
|
||||||
anyOf:
|
type: array
|
||||||
- $ref: "#/components/schemas/GID"
|
items:
|
||||||
- type: "null"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Business owner ID
|
description: Administrator profile IDs
|
||||||
security_owner_id:
|
|
||||||
anyOf:
|
|
||||||
- $ref: "#/components/schemas/GID"
|
|
||||||
- type: "null"
|
|
||||||
description: Security owner ID
|
|
||||||
status_page_url:
|
status_page_url:
|
||||||
type:
|
type:
|
||||||
- string
|
- string
|
||||||
@@ -1051,12 +1046,11 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
description: Countries or regions (ISO 3166-1 alpha-2 country codes, EU, or GLOBAL)
|
description: Countries or regions (ISO 3166-1 alpha-2 country codes, EU, or GLOBAL)
|
||||||
business_owner_id:
|
administrator_ids:
|
||||||
$ref: "#/components/schemas/GID"
|
type: array
|
||||||
description: Business owner ID
|
items:
|
||||||
security_owner_id:
|
$ref: "#/components/schemas/GID"
|
||||||
$ref: "#/components/schemas/GID"
|
description: Administrator profile IDs
|
||||||
description: Security owner ID
|
|
||||||
status_page_url:
|
status_page_url:
|
||||||
type: string
|
type: string
|
||||||
description: Status page URL
|
description: Status page URL
|
||||||
@@ -1152,12 +1146,11 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
description: Countries or regions (ISO 3166-1 alpha-2 country codes, EU, or GLOBAL)
|
description: Countries or regions (ISO 3166-1 alpha-2 country codes, EU, or GLOBAL)
|
||||||
business_owner_id:
|
administrator_ids:
|
||||||
$ref: "#/components/schemas/GID"
|
type: array
|
||||||
description: Business owner ID
|
items:
|
||||||
security_owner_id:
|
$ref: "#/components/schemas/GID"
|
||||||
$ref: "#/components/schemas/GID"
|
description: Administrator profile IDs
|
||||||
description: Security owner ID
|
|
||||||
status_page_url:
|
status_page_url:
|
||||||
type: string
|
type: string
|
||||||
description: Status page URL
|
description: Status page URL
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,12 +65,16 @@ func NewAddThirdPartyRiskAssessmentOutput(v *coredata.ThirdPartyRiskAssessment)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewThirdParty(v *coredata.ThirdParty) *ThirdParty {
|
func NewThirdParty(v *coredata.ThirdParty, administratorIDs []gid.GID) *ThirdParty {
|
||||||
countries := make([]string, len(v.Countries))
|
countries := make([]string, len(v.Countries))
|
||||||
for i, c := range v.Countries {
|
for i, c := range v.Countries {
|
||||||
countries[i] = string(c)
|
countries[i] = string(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if administratorIDs == nil {
|
||||||
|
administratorIDs = []gid.GID{}
|
||||||
|
}
|
||||||
|
|
||||||
return &ThirdParty{
|
return &ThirdParty{
|
||||||
ID: v.ID,
|
ID: v.ID,
|
||||||
OrganizationID: v.OrganizationID,
|
OrganizationID: v.OrganizationID,
|
||||||
@@ -86,8 +91,7 @@ func NewThirdParty(v *coredata.ThirdParty) *ThirdParty {
|
|||||||
SubprocessorsListURL: v.SubprocessorsListURL,
|
SubprocessorsListURL: v.SubprocessorsListURL,
|
||||||
Certifications: v.Certifications,
|
Certifications: v.Certifications,
|
||||||
Countries: countries,
|
Countries: countries,
|
||||||
BusinessOwnerID: v.BusinessOwnerID,
|
AdministratorIds: administratorIDs,
|
||||||
SecurityOwnerID: v.SecurityOwnerID,
|
|
||||||
StatusPageURL: v.StatusPageURL,
|
StatusPageURL: v.StatusPageURL,
|
||||||
TermsOfServiceURL: v.TermsOfServiceURL,
|
TermsOfServiceURL: v.TermsOfServiceURL,
|
||||||
SecurityPageURL: v.SecurityPageURL,
|
SecurityPageURL: v.SecurityPageURL,
|
||||||
@@ -98,10 +102,13 @@ func NewThirdParty(v *coredata.ThirdParty) *ThirdParty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListThirdPartiesOutput(thirdPartyPage *page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField]) ListThirdPartiesOutput {
|
func NewListThirdPartiesOutput(
|
||||||
|
thirdPartyPage *page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField],
|
||||||
|
administratorIDsByThirdPartyID map[gid.GID][]gid.GID,
|
||||||
|
) ListThirdPartiesOutput {
|
||||||
thirdParties := make([]*ThirdParty, 0, len(thirdPartyPage.Data))
|
thirdParties := make([]*ThirdParty, 0, len(thirdPartyPage.Data))
|
||||||
for _, v := range thirdPartyPage.Data {
|
for _, v := range thirdPartyPage.Data {
|
||||||
thirdParties = append(thirdParties, NewThirdParty(v))
|
thirdParties = append(thirdParties, NewThirdParty(v, administratorIDsByThirdPartyID[v.ID]))
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextCursor *page.CursorKey
|
var nextCursor *page.CursorKey
|
||||||
@@ -117,10 +124,13 @@ func NewListThirdPartiesOutput(thirdPartyPage *page.Page[*coredata.ThirdParty, c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListChildThirdPartiesOutput(thirdPartyPage *page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField]) ListChildThirdPartiesOutput {
|
func NewListChildThirdPartiesOutput(
|
||||||
|
thirdPartyPage *page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField],
|
||||||
|
administratorIDsByThirdPartyID map[gid.GID][]gid.GID,
|
||||||
|
) ListChildThirdPartiesOutput {
|
||||||
thirdParties := make([]*ThirdParty, 0, len(thirdPartyPage.Data))
|
thirdParties := make([]*ThirdParty, 0, len(thirdPartyPage.Data))
|
||||||
for _, v := range thirdPartyPage.Data {
|
for _, v := range thirdPartyPage.Data {
|
||||||
thirdParties = append(thirdParties, NewThirdParty(v))
|
thirdParties = append(thirdParties, NewThirdParty(v, administratorIDsByThirdPartyID[v.ID]))
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextCursor *page.CursorKey
|
var nextCursor *page.CursorKey
|
||||||
@@ -136,15 +146,15 @@ func NewListChildThirdPartiesOutput(thirdPartyPage *page.Page[*coredata.ThirdPar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddThirdPartyOutput(v *coredata.ThirdParty) AddThirdPartyOutput {
|
func NewAddThirdPartyOutput(v *coredata.ThirdParty, administratorIDs []gid.GID) AddThirdPartyOutput {
|
||||||
return AddThirdPartyOutput{
|
return AddThirdPartyOutput{
|
||||||
ThirdParty: NewThirdParty(v),
|
ThirdParty: NewThirdParty(v, administratorIDs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUpdateThirdPartyOutput(v *coredata.ThirdParty) UpdateThirdPartyOutput {
|
func NewUpdateThirdPartyOutput(v *coredata.ThirdParty, administratorIDs []gid.GID) UpdateThirdPartyOutput {
|
||||||
return UpdateThirdPartyOutput{
|
return UpdateThirdPartyOutput{
|
||||||
ThirdParty: NewThirdParty(v),
|
ThirdParty: NewThirdParty(v, administratorIDs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,13 +46,16 @@ type ThirdParty struct {
|
|||||||
HeadquarterAddress *string `json:"headquarterAddress"`
|
HeadquarterAddress *string `json:"headquarterAddress"`
|
||||||
LegalName *string `json:"legalName"`
|
LegalName *string `json:"legalName"`
|
||||||
WebsiteURL *string `json:"websiteUrl"`
|
WebsiteURL *string `json:"websiteUrl"`
|
||||||
BusinessOwnerID *gid.GID `json:"businessOwnerId"`
|
AdministratorIDs []gid.GID `json:"administratorIds"`
|
||||||
SecurityOwnerID *gid.GID `json:"securityOwnerId"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewThirdParty(v *coredata.ThirdParty) *ThirdParty {
|
func NewThirdParty(v *coredata.ThirdParty, administratorIDs []gid.GID) *ThirdParty {
|
||||||
|
if administratorIDs == nil {
|
||||||
|
administratorIDs = []gid.GID{}
|
||||||
|
}
|
||||||
|
|
||||||
return &ThirdParty{
|
return &ThirdParty{
|
||||||
ID: v.ID,
|
ID: v.ID,
|
||||||
Name: v.Name,
|
Name: v.Name,
|
||||||
@@ -72,8 +75,7 @@ func NewThirdParty(v *coredata.ThirdParty) *ThirdParty {
|
|||||||
HeadquarterAddress: v.HeadquarterAddress,
|
HeadquarterAddress: v.HeadquarterAddress,
|
||||||
LegalName: v.LegalName,
|
LegalName: v.LegalName,
|
||||||
WebsiteURL: v.WebsiteURL,
|
WebsiteURL: v.WebsiteURL,
|
||||||
BusinessOwnerID: v.BusinessOwnerID,
|
AdministratorIDs: administratorIDs,
|
||||||
SecurityOwnerID: v.SecurityOwnerID,
|
|
||||||
CreatedAt: v.CreatedAt,
|
CreatedAt: v.CreatedAt,
|
||||||
UpdatedAt: v.UpdatedAt,
|
UpdatedAt: v.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user