Migrate people to identities / memberships / profiles

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-03 17:44:04 +04:00
parent 89e35af703
commit 70dec6cc7d
4 changed files with 557 additions and 18 deletions

View File

@@ -24,15 +24,21 @@ import (
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
)
type (
MembershipProfile struct {
ID gid.GID `db:"id"`
MembershipID gid.GID `db:"membership_id"`
FullName string `db:"full_name"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
MembershipID gid.GID `db:"membership_id"`
FullName string `db:"full_name"`
Kind PeopleKind `db:"kind"`
AdditionalEmailAddresses mail.Addrs `db:"additional_email_addresses"`
Position *string `db:"position"`
ContractStartDate *time.Time `db:"contract_start_date"`
ContractEndDate *time.Time `db:"contract_end_date"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
)
@@ -61,12 +67,17 @@ SELECT
id,
membership_id,
full_name,
kind,
additionalEmailAddresses,
position,
contract_start_date,
contract_end_date,
created_at,
updated_at
FROM
iam_membership_profiles
WHERE
%s
%s
AND membership_id = @membership_id
LIMIT 1;
`
@@ -106,12 +117,17 @@ SELECT
id,
membership_id,
full_name,
kind,
additionalEmailAddresses,
position,
contract_start_date,
contract_end_date,
created_at,
updated_at
FROM
iam_membership_profiles
WHERE
%s
%s
AND id = @profile_id
LIMIT 1;
`
@@ -151,6 +167,11 @@ INSERT INTO
id,
membership_id,
full_name,
kind,
additionalEmailAddresses,
position,
contract_start_date,
contract_end_date,
created_at,
updated_at
)
@@ -159,18 +180,28 @@ VALUES (
@id,
@membership_id,
@full_name,
@kind,
@additionalEmailAddresses,
@position,
@contract_start_date,
@contract_end_date,
@created_at,
@updated_at
)
`
args := pgx.StrictNamedArgs{
"tenant_id": p.ID.TenantID().String(),
"id": p.ID,
"membership_id": p.MembershipID,
"full_name": p.FullName,
"created_at": p.CreatedAt,
"updated_at": p.UpdatedAt,
"tenant_id": p.ID.TenantID().String(),
"id": p.ID,
"membership_id": p.MembershipID,
"full_name": p.FullName,
"kind": p.Kind,
"additionalEmailAddresses": p.AdditionalEmailAddresses,
"position": p.Position,
"contract_start_date": p.ContractStartDate,
"contract_end_date": p.ContractEndDate,
"created_at": p.CreatedAt,
"updated_at": p.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
@@ -191,6 +222,11 @@ UPDATE
iam_membership_profiles
SET
full_name = @full_name,
kind = @kind,
additionalEmailAddresses = @additionalEmailAddresses,
position = @position,
contract_start_date = @contract_start_date,
contract_end_date = @contract_end_date,
updated_at = @updated_at
WHERE
id = @id
@@ -200,9 +236,14 @@ WHERE
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"id": p.ID,
"full_name": p.FullName,
"updated_at": p.UpdatedAt,
"id": p.ID,
"full_name": p.FullName,
"kind": p.Kind,
"additionalEmailAddresses": p.AdditionalEmailAddresses,
"position": p.Position,
"contract_start_date": p.ContractStartDate,
"contract_end_date": p.ContractEndDate,
"updated_at": p.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())

View File

@@ -0,0 +1,498 @@
-- 1. Add peoples column onto profiles
ALTER TABLE
iam_membership_profiles
ADD
COLUMN additional_email_addresses CITEXT [] NOT NULL,
ADD
COLUMN kind PEOPLE_KIND NOT NULL,
ADD
COLUMN contract_start_date DATE,
ADD
COLUMN contract_end_date DATE,
ADD
COLUMN position TEXT;
-- 2. Add profile references to all table referencing peoples
-- was owner_id, NOT NULL
ALTER TABLE
assets
ADD
COLUMN owner_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE RESTRICT;
-- was owner_id, NOT NULL
ALTER TABLE
continual_improvements
ADD
COLUMN owner_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE RESTRICT;
-- was owner_id, NOT NULL
ALTER TABLE
data
ADD
COLUMN owner_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE RESTRICT;
-- was owner_id, NOT NULL
ALTER TABLE
document_versions
ADD
COLUMN owner_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE RESTRICT;
-- was attendee_id, NOT NULL
ALTER TABLE
meeting_attendees
ADD
COLUMN attendee_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE CASCADE;
-- was owner_id, NOT NULL
ALTER TABLE
nonconformities
ADD
COLUMN owner_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE RESTRICT;
-- was owner_id, NOT NULL
ALTER TABLE
obligations
ADD
COLUMN owner_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE RESTRICT;
-- was owner_id, can be null
ALTER TABLE
documents
ADD
COLUMN owner_profile_id REFERENCES iam_membership_profiles(id);
-- was signed_by, NOT NULL
ALTER TABLE
document_version_signatures
ADD
COLUMN signed_by_profile_id REFERENCES iam_membership_profiles(id);
-- was data_protection_officer_id, can be null
ALTER TABLE
processing_activities
ADD
COLUMN dpo_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE RESTRICT;
-- was owner_id, can be null
ALTER TABLE
risks
ADD
COLUMN owner_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE
SET
NULL;
-- was owner_id, NOT NULL
ALTER TABLE
states_of_applicability
ADD
COLUMN owner_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE RESTRICT;
-- was assigned_to, can be null
ALTER TABLE
tasks
ADD
COLUMN assigned_to_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE
SET
NULL;
-- was business_owner_id, can be null
ALTER TABLE
vendors
ADD
COLUMN business_owner_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE
SET
NULL;
-- was security_owner_id, can be null
ALTER TABLE
vendors
ADD
COLUMN security_owner_profile_id REFERENCES iam_membership_profiles(id) ON UPDATE CASCADE ON DELETE
SET
NULL;
-- 3. Create missing identities
INSERT INTO
identities (
id,
created_at,
updated_at,
email_address,
email_address_verified,
full_name
)
SELECT
generate_gid('\x0000000000000000' :: bytea, 11),
NOW(),
NOW(),
p.primary_email_address,
FALSE,
p.full_name
FROM
peoples p ON CONFLICT DO NOTHING;
-- 4. Create missing memberships & profiles
WITH people_identities AS (
SELECT
i.id AS identity_id,
p.tenant_id AS tenant_id,
p.organization_id AS organization_id,
p.kind AS kind,
p.full_name AS full_name,
p.additional_email_addresses AS additional_email_addresses,
p.position AS position,
p.contract_start_date AS contract_start_date,
p.contract_end_date AS contract_end_date,
sc.id IS NOT NULL AS scim
FROM
peoples
JOIN identities i ON i.primary_email_address = p.primary_email_address
JOIN organizations o ON o.id = p.organization_id
LEFT JOIN iam_scim_configurations sc ON sc.organization_id = o.id
),
people_memberships AS (
INSERT INTO
iam_memberships (
id,
tenant_id,
identity_id,
organization_id,
role,
source,
state,
created_at,
updated_at
)
SELECT
generate_gid(decode_base64_unpadded(pi.tenant_id), 39),
pi.tenant_id,
pi.identity_id,
pi.organization_id,
'EMPLOYEE' :: authz_role,
'MANUAL',
CASE
WHEN pi.scim THEN 'INACTIVE' :: membership_state
ELSE 'ACTIVE' :: membership_state
END,
NOW(),
NOW()
FROM
people_identities pi ON CONFLICT DO NOTHING RETURNING id AS membership_id,
pi.identity_id AS identity_id,
pi.tenant_id AS tenant_id,
pi.organization_id AS organization_id,
pi.kind AS kind,
pi.full_name AS full_name,
pi.additional_email_addresses AS additional_email_addresses,
pi.position AS position,
pi.contract_start_date AS contract_start_date,
pi.contract_end_date AS contract_start_date,
) -- Create missing profiles
-- Note: we always have a profile for an existing membership (they are created together in the same TX),
-- So it is safe to create the missing profiles from the newly created memberships
INSERT INTO
iam_membership_profiles (
id,
tenant_id,
membership_id,
full_name,
kind,
additional_email_addresses,
position,
contract_start_date,
contract_end_date,
created_at,
updated_at
)
SELECT
generate_gid(decode_base64_unpadded(pm.tenant_id), 51),
pm.tenant_id,
pm.membership_id,
pm.full_name,
pm.kind,
pm.additional_email_addresses,
pm.position,
pm.contract_start_date,
pm.contract_end_date,
NOW(),
NOW()
FROM
people_memberships pm ON CONFLICT DO NOTHING;
-- 5. Update all profiles from peoples
WITH people_memberships AS (
SELECT
m.id AS membership_id,
COALESCE(p.additional_email_addresses, [] :: CITEXT),
COALESCE(p.kind, 'EMPLOYEE' :: PEOPLE_KIND),
p.contract_start_date,
p.contract_end_date,
p.position,
FROM
iam_memberships m
JOIN identities i ON i.id = m.identity_id
LEFT JOIN peoples p ON p.primary_email_address = i.email_address
AND p.organization_id = m.organization_id
)
UPDATE
iam_membership_profiles
SET
additional_email_addresses = pm.additional_email_addresses,
kind = pm.kind,
contract_start_date = pm.contract_start_date,
contract_end_date = pm.contract_end_date,
position = pm.position
FROM
people_memberships pm
WHERE
membership_id = pm.membership_id;
-- 6. Fill new references to profiles (e.g. previous references to peoples)
WITH people_profiles AS (
SELECT
mp.id AS membership_profile_id,
p.id AS people_id
FROM
iam_membership_profiles mp
JOIN iam_memberships m ON m.id = mp.membership_id
JOIN identities i ON i.id = m.identity_id
LEFT JOIN peoples p ON p.primary_email_address = i.email_address
),
updated_assets AS (
UPDATE
assets a
SET
owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
a.owner_id = pp.people_id
),
updated_ci AS (
UPDATE
continual_improvements ci
SET
owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
ci.owner_id = pp.people_id
),
updated_data AS (
UPDATE
data d
SET
owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
d.owner_id = pp.people_id
),
updated_dv AS (
UPDATE
document_versions dv
SET
owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
dv.owner_id = pp.people_id
),
updated_ma AS (
UPDATE
meeting_attendees ma
SET
attendee_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
ma.attendee_id = pp.people_id
),
updated_nc AS (
UPDATE
nonconformities nc
SET
owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
nc.owner_id = pp.people_id
),
updated_obligations AS (
UPDATE
obligations o
SET
owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
o.owner_id = pp.people_id
),
updated_documents AS (
UPDATE
documents d
SET
owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
d.owner_id = pp.people_id
),
updated_dvs AS (
UPDATE
document_version_signatures dvs
SET
signed_by_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
dvs.signed_by = pp.people_id
),
updated_pa AS (
UPDATE
processing_activities pa
SET
dpo_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
pa.data_protection_officer_id = pp.people_id
),
updated_risks AS (
UPDATE
risks r
SET
owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
r.owner_id = pp.people_id
),
updated_soa AS (
UPDATE
states_of_applicability soa
SET
owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
soa.owner_id = pp.people_id
),
updated_tasks AS (
UPDATE
tasks t
SET
assigned_to_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
t.assigned_to = pp.people_id
),
updated_vendors AS (
UPDATE
vendors v
SET
business_owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
v.business_owner_id = pp.people_id
)
UPDATE
vendors v
SET
security_owner_profile_id = pp.membership_profile_id
FROM
people_profiles pp
WHERE
v.security_owner_id = pp.people_id;
-- 7. Now that references are filled, add the NOT NULL constraints to those who need it
ALTER TABLE
assets
ALTER COLUMN
owner_profile_id
SET
NOT NULL;
ALTER TABLE
continual_improvements
ALTER COLUMN
owner_profile_id
SET
NOT NULL;
ALTER TABLE
data
ALTER COLUMN
owner_profile_id
SET
NOT NULL;
ALTER TABLE
document_versions
ALTER COLUMN
owner_profile_id
SET
NOT NULL;
ALTER TABLE
meeting_attendees
ALTER COLUMN
attendee_profile_id
SET
NOT NULL;
ALTER TABLE
nonconformities
ALTER COLUMN
owner_profile_id
SET
NOT NULL;
ALTER TABLE
obligations
ALTER COLUMN
owner_profile_id
SET
NOT NULL;
ALTER TABLE
document_version_signatures
ALTER COLUMN
signed_by_profile_id
SET
NOT NULL;
ALTER TABLE
states_of_applicability
ALTER COLUMN
owner_profile_id
SET
NOT NULL;
-- 8. Expire any potential invitation that is now obsolete since we created memberships
ALTER TABLE
iam_invitations
ALTER COLUMN
email TYPE CITEXT [];
WITH people_identities AS (
SELECT
m.organization_id AS organization_id,
i.email_address AS email_address
FROM
iam_memberships m
JOIN identities i ON i.id = m.identity_id
JOIN peoples p ON p.primary_email_address = i.email_address
AND p.organization_id = m.organization_id
)
UPDATE
iam_invitations i
SET
expires_at = NOW()
FROM
people_identities pi
WHERE
pi.organization_id = i.organization_id
AND i.email = pi.email_address;

View File

@@ -287,7 +287,7 @@ func (s *AccountService) AcceptInvitation(
return fmt.Errorf("cannot update invitation: %w", err)
}
// Accept other pending invitations for email in organization
// Expire other pending invitations for email in organization
invitations := &coredata.Invitations{}
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
if err := invitations.ExpireByEmailAndOrganization(

View File

@@ -1852,7 +1852,7 @@ func (r *mutationResolver) CreateTrustCenterAccess(ctx context.Context, input ty
prb := r.ProboService(ctx, input.TrustCenterID.TenantID())
// TODO: should not create an access nor identity, but send an invitation
// TODO: should not create an access nor identity, but send a compliance page invitation
identity, err := r.iam.AuthService.LoadOrCreateIdentity(
ctx,
&iam.LoadOrCreateIdentityRequest{