Move source and state from membership to profile

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-13 21:21:11 +04:00
parent ec3d8fad40
commit 56ec0ab3c3
33 changed files with 1066 additions and 1220 deletions

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<b6eef6dfea77fdf5e4e151eef11f4484>>
* @generated SignedSource<<d8cb4fb18045bbd1eb91c3dba7c99a79>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -222,6 +222,20 @@ return {
"plural": false,
"selections": [
(v12/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -259,20 +273,6 @@ return {
"name": "role",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": "canUpdate",
"args": [
@@ -430,12 +430,12 @@ return {
]
},
"params": {
"cacheID": "25aa17e466fdec627ba3d24d5230301b",
"cacheID": "cc77d44877ae736c858e7458b525d75a",
"id": null,
"metadata": {},
"name": "PeopleListFragment_RefetchQuery",
"operationKind": "query",
"text": "query PeopleListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: ProfileOrder = {direction: ASC, field: FULL_NAME}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleListFragment_16fISc\n id\n }\n}\n\nfragment PeopleListFragment_16fISc on Organization {\n profiles(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n fullName\n kind\n position\n membership {\n id\n role\n source\n state\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n"
"text": "query PeopleListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: ProfileOrder = {direction: ASC, field: FULL_NAME}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleListFragment_16fISc\n id\n }\n}\n\nfragment PeopleListFragment_16fISc on Organization {\n profiles(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n kind\n position\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<6e9b0a4fbf6ce65f11f2367f2ec5b6d4>>
* @generated SignedSource<<67410bc0aa6e9426f4534a0938429b6e>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,9 +10,8 @@
import { ReaderFragment } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
export type MembershipSource = "MANUAL" | "SAML" | "SCIM";
export type MembershipState = "ACTIVE" | "INACTIVE";
export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT";
export type ProfileState = "ACTIVE" | "INACTIVE";
import { FragmentRefs } from "relay-runtime";
export type PeopleListItemFragment$data = {
readonly canUpdate: boolean;
@@ -28,10 +27,10 @@ export type PeopleListItemFragment$data = {
readonly canUpdate: boolean;
readonly id: string;
readonly role: MembershipRole;
readonly source: MembershipSource;
readonly state: MembershipState;
};
readonly position: string | null | undefined;
readonly source: string;
readonly state: ProfileState;
readonly " $fragmentType": "PeopleListItemFragment";
};
export type PeopleListItemFragment$key = {
@@ -54,6 +53,20 @@ return {
"name": "PeopleListItemFragment",
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -93,20 +106,6 @@ return {
"name": "role",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": "canUpdate",
"args": [
@@ -186,6 +185,6 @@ return {
};
})();
(node as any).hash = "89702fbdf02294212269adb22efb65fe";
(node as any).hash = "ad9ed05edf606ec084c6f53462530b8b";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<e628337d33e88ca5cfe6e45a55eebe32>>
* @generated SignedSource<<7f025767ad277b8b8de6cbe9b1490312>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -345,6 +345,20 @@ return {
"plural": false,
"selections": [
(v10/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
(v11/*: any*/),
{
"alias": null,
@@ -370,20 +384,6 @@ return {
"selections": [
(v10/*: any*/),
(v12/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": "canUpdate",
"args": [
@@ -552,12 +552,12 @@ return {
]
},
"params": {
"cacheID": "4303ec6c2fa72181d747bb9053446aaa",
"cacheID": "b67de1f40d556a5e4a6d78104e2eb224",
"id": null,
"metadata": {},
"name": "PeoplePageQuery",
"operationKind": "query",
"text": "query PeoplePageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canInviteUser: permission(action: \"iam:invitation:create\")\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n }\n ...PeopleListFragment_8lnpd\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n totalCount\n __typename\n }\n ...InvitationListFragment_1PypFi\n }\n id\n }\n}\n\nfragment InvitationListFragment_1PypFi on Organization {\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment InvitationListItemFragment on Invitation {\n id\n fullName\n email\n role\n status\n createdAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment PeopleListFragment_8lnpd on Organization {\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n fullName\n kind\n position\n membership {\n id\n role\n source\n state\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n"
"text": "query PeoplePageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canInviteUser: permission(action: \"iam:invitation:create\")\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n }\n ...PeopleListFragment_8lnpd\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n totalCount\n __typename\n }\n ...InvitationListFragment_1PypFi\n }\n id\n }\n}\n\nfragment InvitationListFragment_1PypFi on Organization {\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment InvitationListItemFragment on Invitation {\n id\n fullName\n email\n role\n status\n createdAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment PeopleListFragment_8lnpd on Organization {\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n kind\n position\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<6bc4cdb83f5825b5d606fe579fa8f7a8>>
* @generated SignedSource<<f347bcc313be984e835dd6518091a71c>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -16,10 +16,8 @@ export type SCIMEventListItemFragment$data = {
readonly ipAddress: string;
readonly method: string;
readonly path: string;
readonly profile: {
readonly fullName: string;
} | null | undefined;
readonly statusCode: number;
readonly userName: string;
readonly " $fragmentType": "SCIMEventListItemFragment";
};
export type SCIMEventListItemFragment$key = {
@@ -78,19 +76,8 @@ const node: ReaderFragment = {
{
"alias": null,
"args": null,
"concreteType": "Profile",
"kind": "LinkedField",
"name": "profile",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"kind": "ScalarField",
"name": "userName",
"storageKey": null
}
],
@@ -98,6 +85,6 @@ const node: ReaderFragment = {
"abstractKey": null
};
(node as any).hash = "b38af9ac9b660d3aab75e00e2c55dfb6";
(node as any).hash = "9df2e986643ab90589ebc6e4631cde24";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<22d63f81020d1e99f7cdb84fc23913a4>>
* @generated SignedSource<<08f3ffe97dd35388d5ce01977170773b>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -224,20 +224,8 @@ return {
{
"alias": null,
"args": null,
"concreteType": "Profile",
"kind": "LinkedField",
"name": "profile",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
(v8/*: any*/)
],
"kind": "ScalarField",
"name": "userName",
"storageKey": null
},
(v7/*: any*/)
@@ -315,12 +303,12 @@ return {
]
},
"params": {
"cacheID": "58b36de1b4c1f54de6433a95b297b570",
"cacheID": "5e80fcd5f3479d5b6bce65b59364d3f7",
"id": null,
"metadata": {},
"name": "SCIMEventListPaginationQuery",
"operationKind": "query",
"text": "query SCIMEventListPaginationQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...SCIMEventListFragment_pbnwq\n id\n }\n}\n\nfragment SCIMEventListFragment_pbnwq on SCIMConfiguration {\n events(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n ...SCIMEventListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SCIMEventListItemFragment on SCIMEvent {\n method\n path\n statusCode\n errorMessage\n ipAddress\n createdAt\n profile {\n fullName\n id\n }\n}\n"
"text": "query SCIMEventListPaginationQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...SCIMEventListFragment_pbnwq\n id\n }\n}\n\nfragment SCIMEventListFragment_pbnwq on SCIMConfiguration {\n events(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n ...SCIMEventListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SCIMEventListItemFragment on SCIMEvent {\n method\n path\n statusCode\n errorMessage\n ipAddress\n createdAt\n userName\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<339e086642b09eb0bacbc51601014fe7>>
* @generated SignedSource<<164d2e51d3c593d5effe16958d2d8f0e>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -277,20 +277,8 @@ return {
{
"alias": null,
"args": null,
"concreteType": "Profile",
"kind": "LinkedField",
"name": "profile",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
(v3/*: any*/)
],
"kind": "ScalarField",
"name": "userName",
"storageKey": null
},
(v2/*: any*/)
@@ -404,12 +392,12 @@ return {
]
},
"params": {
"cacheID": "3d49152bd3fd384a0b1de3d012268ebe",
"cacheID": "b98c546a2c8fc038c121c21f3b495a7d",
"id": null,
"metadata": {},
"name": "SCIMSettingsPageQuery",
"operationKind": "query",
"text": "query SCIMSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n scimConfiguration {\n id\n bridge {\n id\n }\n ...SCIMEventListFragment\n }\n ...SCIMConfigurationFragment\n ...ConnectorListFragment\n }\n id\n }\n}\n\nfragment ConnectorListFragment on Organization {\n scimConfiguration {\n ...GoogleWorkspaceConnectorFragment\n id\n }\n}\n\nfragment GoogleWorkspaceConnectorFragment on SCIMConfiguration {\n id\n bridge {\n id\n excludedUserNames\n connector {\n id\n createdAt\n }\n }\n}\n\nfragment SCIMConfigurationFragment on Organization {\n canCreateSCIMConfiguration: permission(action: \"iam:scim-configuration:create\")\n canDeleteSCIMConfiguration: permission(action: \"iam:scim-configuration:delete\")\n scimConfiguration {\n id\n endpointUrl\n bridge {\n id\n }\n }\n}\n\nfragment SCIMEventListFragment on SCIMConfiguration {\n events(first: 20) {\n edges {\n node {\n id\n ...SCIMEventListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SCIMEventListItemFragment on SCIMEvent {\n method\n path\n statusCode\n errorMessage\n ipAddress\n createdAt\n profile {\n fullName\n id\n }\n}\n"
"text": "query SCIMSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n scimConfiguration {\n id\n bridge {\n id\n }\n ...SCIMEventListFragment\n }\n ...SCIMConfigurationFragment\n ...ConnectorListFragment\n }\n id\n }\n}\n\nfragment ConnectorListFragment on Organization {\n scimConfiguration {\n ...GoogleWorkspaceConnectorFragment\n id\n }\n}\n\nfragment GoogleWorkspaceConnectorFragment on SCIMConfiguration {\n id\n bridge {\n id\n excludedUserNames\n connector {\n id\n createdAt\n }\n }\n}\n\nfragment SCIMConfigurationFragment on Organization {\n canCreateSCIMConfiguration: permission(action: \"iam:scim-configuration:create\")\n canDeleteSCIMConfiguration: permission(action: \"iam:scim-configuration:delete\")\n scimConfiguration {\n id\n endpointUrl\n bridge {\n id\n }\n }\n}\n\nfragment SCIMEventListFragment on SCIMConfiguration {\n events(first: 20) {\n edges {\n node {\n id\n ...SCIMEventListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SCIMEventListItemFragment on SCIMEvent {\n method\n path\n statusCode\n errorMessage\n ipAddress\n createdAt\n userName\n}\n"
}
};
})();

View File

@@ -24,14 +24,14 @@ import { EditMemberDialog } from "../../settings/_components/EditMemberDialog";
const fragment = graphql`
fragment PeopleListItemFragment on Profile {
id
source
state
fullName
kind
position
membership @required(action: THROW) {
id
role
source
state
canUpdate: permission(action: "iam:membership:update")
canDelete: permission(action: "iam:membership-profile:delete")
}
@@ -68,7 +68,7 @@ export function MemberListItem(props: {
const profile = useFragment<PeopleListItemFragment$key>(fragment, fKey);
const isInactive = profile.membership.state === "INACTIVE";
const isInactive = profile.state === "INACTIVE";
const [removeMembership, isRemoving] = useMutationWithToasts(
removeMemberMutation,
@@ -118,7 +118,7 @@ export function MemberListItem(props: {
<Td>
<div className="flex items-center gap-2">
{profile.identity.email}
<Badge variant="info">{profile.membership.source}</Badge>
<Badge variant="info">{profile.source}</Badge>
</div>
</Td>
<Td>{profile.kind}</Td>

View File

@@ -14,9 +14,7 @@ const SCIMEventListItemFragment = graphql`
errorMessage
ipAddress
createdAt
profile {
fullName
}
userName
}
`;
@@ -89,8 +87,7 @@ export function SCIMEventListItem(props: {
<div className="flex gap-8 text-sm">
<div>
<span className="text-txt-secondary">User: </span>
{/* FIXME */}
{/* <span>{event.membership?.profile?.fullName || "-"}</span> */}
<span>{event.userName || "-"}</span>
</div>
<div>
<span className="text-txt-secondary">IP Address: </span>

View File

@@ -25,24 +25,17 @@ import (
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/page"
)
type (
Membership struct {
ID gid.GID `db:"id"`
IdentityID gid.GID `db:"identity_id"`
OrganizationID gid.GID `db:"organization_id"`
Role MembershipRole `db:"role"`
Source MembershipSource `db:"source"`
State MembershipState `db:"state"`
// FIXME: remove after scim is based on profile
EmailAddress mail.Addr `db:"-"`
FullName string `db:"-"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
IdentityID gid.GID `db:"identity_id"`
OrganizationID gid.GID `db:"organization_id"`
Role MembershipRole `db:"role"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Memberships []*Membership
@@ -66,8 +59,6 @@ SELECT
identity_id,
organization_id,
role,
source,
state,
created_at,
updated_at
FROM
@@ -109,8 +100,6 @@ INSERT INTO
identity_id,
organization_id,
role,
source,
state,
created_at,
updated_at
)
@@ -120,8 +109,6 @@ VALUES (
@identity_id,
@organization_id,
@role,
@source,
@state,
@created_at,
@updated_at
);
@@ -133,8 +120,6 @@ VALUES (
"identity_id": m.IdentityID,
"organization_id": m.OrganizationID,
"role": m.Role,
"source": m.Source,
"state": m.State,
"created_at": m.CreatedAt,
"updated_at": m.UpdatedAt,
}
@@ -168,8 +153,6 @@ SELECT
identity_id,
organization_id,
role,
source,
state,
created_at,
updated_at
FROM
@@ -209,8 +192,7 @@ func (m *Membership) AuthorizationAttributes(ctx context.Context, conn pg.Conn)
SELECT
identity_id,
organization_id,
role,
source
role
FROM
iam_memberships
WHERE
@@ -221,12 +203,10 @@ LIMIT 1;
var identityID gid.GID
var organizationID gid.GID
var role MembershipRole
var source MembershipSource
if err := conn.QueryRow(ctx, q, m.ID).Scan(
&identityID,
&organizationID,
&role,
&source,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrResourceNotFound
@@ -238,7 +218,6 @@ LIMIT 1;
"identity_id": identityID.String(),
"organization_id": organizationID.String(),
"role": role.String(),
"source": source.String(),
}, nil
}
@@ -255,8 +234,6 @@ SELECT
identity_id,
organization_id,
role,
source,
state,
created_at,
updated_at
FROM
@@ -299,8 +276,6 @@ UPDATE
iam_memberships
SET
role = @role,
source = @source,
state = @state,
updated_at = @updated_at
WHERE
id = @id
@@ -312,8 +287,6 @@ WHERE
args := pgx.StrictNamedArgs{
"id": m.ID,
"role": m.Role,
"source": m.Source,
"state": m.State,
"updated_at": m.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())
@@ -358,180 +331,51 @@ WHERE
return nil
}
func (m *Memberships) LoadByOrganizationID(
func (m *Membership) LoadActiveByIdentityIDAndOrganizationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
identityID gid.GID,
organizationID gid.GID,
cursor *page.Cursor[MembershipOrderField],
filter *MembershipFilter,
) error {
query := `
q := `
SELECT
id,
identity_id,
organization_id,
role,
source,
state,
created_at,
updated_at
FROM
iam_memberships
WHERE
%s
AND %s
organization_id = @organization_id
AND %s
`
query = fmt.Sprintf(query, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{
"organization_id": organizationID,
}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, query, args)
if err != nil {
return fmt.Errorf("cannot query memberships: %w", err)
}
memberships, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Membership])
if err != nil {
return fmt.Errorf("cannot collect memberships: %w", err)
}
*m = memberships
return nil
}
func (m *Memberships) CountByOrganizationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
filter *MembershipFilter,
) (int, error) {
query := `
SELECT
COUNT(*)
m.id,
m.identity_id,
m.organization_id,
m.role,
m.created_at,
m.updated_at
FROM
iam_memberships m
JOIN
identities i ON m.identity_id = i.id
INNER JOIN iam_membership_profiles p
ON p.identity_id = m.identity_id AND p.organization_id = m.organization_id
WHERE
m.organization_id = @organization_id
AND m.%s
AND %s
p.state = @state
AND m.identity_id = @identity_id
AND m.organization_id = @organization_id
LIMIT 1
`
query = fmt.Sprintf(query, scope.SQLFragment(), filter.SQLFragment())
args := pgx.StrictNamedArgs{
"state": ProfileStateActive,
"identity_id": identityID,
"organization_id": organizationID,
}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
row := conn.QueryRow(ctx, query, args)
var count int
if err := row.Scan(&count); err != nil {
return 0, fmt.Errorf("cannot count memberships: %w", err)
}
return count, nil
}
func (m *Memberships) CountByIdentityID(
ctx context.Context,
conn pg.Conn,
identityID gid.GID,
) (int, error) {
query := `
SELECT
COUNT(*)
FROM
iam_memberships
WHERE
identity_id = @identity_id
AND state = 'ACTIVE'
`
args := pgx.StrictNamedArgs{
"identity_id": identityID,
}
row := conn.QueryRow(ctx, query, args)
var count int
if err := row.Scan(&count); err != nil {
return 0, fmt.Errorf("cannot count memberships: %w", err)
}
return count, nil
}
func (m *Memberships) LoadAllByIdentityID(
ctx context.Context,
conn pg.Conn,
identityID gid.GID,
) error {
q := `
SELECT
id,
identity_id,
organization_id,
role,
source,
state,
created_at,
updated_at
FROM
iam_memberships
WHERE
identity_id = $1
;
`
rows, err := conn.Query(ctx, q, identityID)
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query memberships: %w", err)
}
memberships, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Membership])
membership, err := pgx.CollectExactlyOneRow(rows, pgx.RowToAddrOfStructByName[Membership])
if err != nil {
return fmt.Errorf("cannot collect memberships: %w", err)
}
*m = memberships
return nil
}
func (m *Memberships) ResetSCIMSources(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
) error {
q := `
UPDATE iam_memberships
SET
source = 'MANUAL',
updated_at = @updated_at
WHERE
%s
AND organization_id = @organization_id
AND source = 'SCIM'
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.NamedArgs{
"organization_id": organizationID,
"updated_at": time.Now(),
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot reset SCIM membership sources: %w", err)
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}
return fmt.Errorf("cannot collect membership: %w", err)
}
*m = *membership
return nil
}

View File

@@ -1,108 +0,0 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"github.com/jackc/pgx/v5"
"go.probo.inc/probo/pkg/mail"
)
type MembershipFilter struct {
email *mail.Addr
role *MembershipRole
state *MembershipState
source *MembershipSource
}
func NewMembershipFilter() *MembershipFilter {
return &MembershipFilter{}
}
func (f *MembershipFilter) WithEmail(email *mail.Addr) *MembershipFilter {
f.email = email
return f
}
func (f *MembershipFilter) Email() *mail.Addr {
return f.email
}
func (f *MembershipFilter) WithRole(role MembershipRole) *MembershipFilter {
f.role = &role
return f
}
func (f *MembershipFilter) Role() *MembershipRole {
return f.role
}
func (f *MembershipFilter) WithState(state MembershipState) *MembershipFilter {
f.state = &state
return f
}
func (f *MembershipFilter) State() *MembershipState {
return f.state
}
func (f *MembershipFilter) WithSource(source MembershipSource) *MembershipFilter {
f.source = &source
return f
}
func (f *MembershipFilter) Source() *MembershipSource {
return f.source
}
func (f *MembershipFilter) SQLArguments() pgx.StrictNamedArgs {
return pgx.StrictNamedArgs{
"filter_email": f.email,
"filter_role": f.role,
"filter_state": f.state,
"filter_source": f.source,
}
}
func (f *MembershipFilter) SQLFragment() string {
return `
(
CASE
WHEN @filter_email::text IS NOT NULL THEN
i.email_address = @filter_email::text
ELSE TRUE
END
)
AND (
CASE
WHEN @filter_role::text IS NOT NULL THEN
m.role = @filter_role::authz_role
ELSE TRUE
END
)
AND (
CASE
WHEN @filter_state::text IS NOT NULL THEN
m.state = @filter_state::membership_state
ELSE TRUE
END
)
AND (
CASE
WHEN @filter_source::text IS NOT NULL THEN
m.source = @filter_source::text
ELSE TRUE
END
)`
}

View File

@@ -34,6 +34,8 @@ type (
IdentityID gid.GID `db:"identity_id"`
OrganizationID gid.GID `db:"organization_id"`
EmailAddress mail.Addr `db:"email_address"`
Source ProfileSource `db:"source"`
State ProfileState `db:"state"`
FullName string `db:"full_name"`
Kind MembershipProfileKind `db:"kind"`
AdditionalEmailAddresses mail.Addrs `db:"additional_email_addresses"`
@@ -93,6 +95,8 @@ SELECT
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
@@ -149,6 +153,8 @@ SELECT
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
@@ -208,6 +214,8 @@ SELECT
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
@@ -257,30 +265,35 @@ func (p *MembershipProfiles) LoadByOrganizationID(
q := `
WITH profiles AS (
SELECT
id,
identity_id,
organization_id,
full_name,
kind,
additional_email_addresses,
position,
contract_start_date,
contract_end_date,
created_at,
updated_at
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
p.created_at,
p.updated_at
FROM
iam_membership_profiles
iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE
%s
AND organization_id = @organization_id
AND %s
p.%s
AND p.organization_id = @organization_id
AND %s
)
SELECT
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
@@ -291,7 +304,8 @@ SELECT
p.created_at,
p.updated_at
FROM profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE
%s
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
@@ -317,7 +331,6 @@ INNER JOIN identities i ON i.id = p.identity_id
func (p *MembershipProfiles) LoadByIdentityID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
identityID gid.GID,
cursor *page.Cursor[MembershipProfileOrderField],
filter *MembershipProfileFilter,
@@ -325,29 +338,34 @@ func (p *MembershipProfiles) LoadByIdentityID(
q := `
WITH profiles AS (
SELECT
id,
identity_id,
organization_id,
full_name,
kind,
additional_email_addresses,
position,
contract_start_date,
contract_end_date,
created_at,
updated_at
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
p.position,
p.contract_start_date,
p.contract_end_date,
p.created_at,
p.updated_at
FROM
iam_membership_profiles
iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE
%s
AND identity_id = @identity_id
p.identity_id = @identity_id
AND %s
)
SELECT
p.id,
p.identity_id,
p.organization_id,
i.email_address,
p.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
@@ -358,16 +376,14 @@ SELECT
p.created_at,
p.updated_at
FROM profiles p
INNER JOIN identities i ON i.id = p.identity_id
INNER JOIN organizations o ON o.id = p.organization_id
WHERE
%s
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
q = fmt.Sprintf(q, filter.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"identity_id": identityID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
@@ -600,6 +616,8 @@ WITH attendees AS (
p.identity_id,
p.organization_id,
i.email_address,
p.source,
p.state,
p.full_name,
p.kind,
p.additional_email_addresses,
@@ -624,6 +642,8 @@ SELECT
organization_id,
kind,
email_address,
source,
state,
full_name,
additional_email_addresses,
position,
@@ -684,6 +704,8 @@ SELECT
p.kind,
p.full_name,
i.email_address,
p.source,
p.state,
p.additional_email_addresses,
p.position,
p.contract_start_date,
@@ -725,10 +747,11 @@ func (p *MembershipProfiles) CountByIdentityID(
SELECT
COUNT(*)
FROM
iam_membership_profiles
iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE
%s
AND identity_id = @identity_id
AND p.identity_id = @identity_id
`
q = fmt.Sprintf(q, filter.SQLFragment())
@@ -758,11 +781,12 @@ func (p *MembershipProfiles) CountByOrganizationID(
SELECT
COUNT(*)
FROM
iam_membership_profiles
iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE
%s
p.%s
AND %s
AND organization_id = @organization_id
AND p.organization_id = @organization_id
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
@@ -782,6 +806,45 @@ WHERE
return count, nil
}
func (p *MembershipProfiles) CountActiveOwnerByOrganizationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
) (int, error) {
q := `
SELECT
COUNT(*)
FROM
iam_membership_profiles p
INNER JOIN iam_memberships m ON m.identity_id = p.identity_id AND m.organization_id = p.organization_id
WHERE
%s
AND p.organization_id = @organization_id
AND p.state = @state
AND m.role = @role
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"state": ProfileStateActive,
"role": MembershipRoleOwner,
"organization_id": organizationID,
}
maps.Copy(args, scope.SQLArguments())
row := conn.QueryRow(ctx, q, args)
var count int
err := row.Scan(&count)
if err != nil {
return 0, fmt.Errorf("cannot collect count: %w", err)
}
return count, nil
}
func (p *MembershipProfile) Insert(
ctx context.Context,
conn pg.Conn,
@@ -793,6 +856,8 @@ INSERT INTO
id,
identity_id,
organization_id,
source,
state,
full_name,
kind,
additional_email_addresses,
@@ -807,6 +872,8 @@ VALUES (
@id,
@identity_id,
@organization_id,
@source,
@state,
@full_name,
@kind,
COALESCE(@additional_email_addresses, '{}'::CITEXT[]),
@@ -823,6 +890,8 @@ VALUES (
"id": p.ID,
"identity_id": p.IdentityID,
"organization_id": p.OrganizationID,
"source": p.Source,
"state": p.State,
"full_name": p.FullName,
"kind": p.Kind,
"additional_email_addresses": p.AdditionalEmailAddresses,
@@ -850,6 +919,8 @@ func (p *MembershipProfile) Update(
UPDATE
iam_membership_profiles
SET
source = @source,
state = @state,
full_name = @full_name,
kind = @kind,
additional_email_addresses = @additional_email_addresses,
@@ -866,6 +937,8 @@ WHERE
args := pgx.StrictNamedArgs{
"id": p.ID,
"source": p.Source,
"state": p.State,
"full_name": p.FullName,
"kind": p.Kind,
"additional_email_addresses": p.AdditionalEmailAddresses,
@@ -888,6 +961,38 @@ WHERE
return nil
}
func (p *MembershipProfiles) ResetSCIMSources(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
) error {
q := `
UPDATE iam_membership_profiles
SET
source = 'MANUAL',
updated_at = @updated_at
WHERE
%s
AND organization_id = @organization_id
AND source = 'SCIM'
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.NamedArgs{
"organization_id": organizationID,
"updated_at": time.Now(),
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot reset SCIM user sources: %w", err)
}
return nil
}
func (p *MembershipProfile) Delete(
ctx context.Context,
conn pg.Conn,

View File

@@ -18,12 +18,16 @@ import (
"time"
"github.com/jackc/pgx/v5"
"go.probo.inc/probo/pkg/mail"
)
type (
MembershipProfileFilter struct {
excludeContractEnded *bool
currentDate time.Time
email *mail.Addr
state *ProfileState
source *ProfileSource
}
)
@@ -34,20 +38,72 @@ func NewMembershipProfileFilter(excludeContractEnded *bool) *MembershipProfileFi
}
}
func (f *MembershipProfileFilter) WithEmail(email *mail.Addr) *MembershipProfileFilter {
f.email = email
return f
}
func (f *MembershipProfileFilter) Email() *mail.Addr {
return f.email
}
func (f *MembershipProfileFilter) WithState(state ProfileState) *MembershipProfileFilter {
f.state = &state
return f
}
func (f *MembershipProfileFilter) State() *ProfileState {
return f.state
}
func (f *MembershipProfileFilter) WithSource(source ProfileSource) *MembershipProfileFilter {
f.source = &source
return f
}
func (f *MembershipProfileFilter) Source() *ProfileSource {
return f.source
}
func (f *MembershipProfileFilter) SQLArguments() pgx.StrictNamedArgs {
return pgx.StrictNamedArgs{
"filter_email": f.email,
"exclude_contract_ended": f.excludeContractEnded,
"current_date": f.currentDate,
"filter_state": f.state,
"filter_source": f.source,
}
}
func (f *MembershipProfileFilter) SQLFragment() string {
return `
(
CASE
WHEN @filter_email::text IS NOT NULL THEN
i.email_address = @filter_email::text
ELSE TRUE
END
)
AND (
CASE
WHEN @exclude_contract_ended::boolean IS NOT NULL AND @exclude_contract_ended::boolean = true THEN
(contract_end_date IS NULL OR contract_end_date >= @current_date::date)
(p.contract_end_date IS NULL OR p.contract_end_date >= @current_date::date)
ELSE TRUE
END
)`
)
AND (
CASE
WHEN @filter_state::text IS NOT NULL THEN
p.state = @filter_state::membership_state
ELSE TRUE
END
)
AND (
CASE
WHEN @filter_source::text IS NOT NULL THEN
p.source = @filter_source::text
ELSE TRUE
END
)
`
}

View File

@@ -1,2 +1,46 @@
ALTER TABLE
iam_membership_profiles
ADD
COLUMN state membership_state NOT NULL DEFAULT 'ACTIVE',
ADD
COLUMN source TEXT NOT NULL DEFAULT 'MANUAL';
UPDATE
iam_membership_profiles p
SET
state = m.state,
source = m.source
FROM
iam_memberships m
WHERE
m.id = p.membership_id;
ALTER TABLE
iam_membership_profiles DROP COLUMN membership_id;
ALTER TABLE
iam_scim_events
ADD
COLUMN user_name CITEXT NOT NULL DEFAULT '';
WITH emails AS (
SELECT
i.email_address,
m.id
FROM
iam_memberships m
INNER JOIN identities i ON i.id = m.identity_id
)
UPDATE
iam_scim_events se
SET
user_name = e.email_address
FROM
emails e
WHERE
e.id = se.membership_id;
ALTER TABLE
iam_scim_events
ALTER COLUMN
user_name DROP DEFAULT;

View File

@@ -19,19 +19,19 @@ import (
"fmt"
)
type MembershipSource string
type ProfileSource string
const (
MembershipSourceManual MembershipSource = "MANUAL"
MembershipSourceSAML MembershipSource = "SAML"
MembershipSourceSCIM MembershipSource = "SCIM"
ProfileSourceManual ProfileSource = "MANUAL"
ProfileSourceSAML ProfileSource = "SAML"
ProfileSourceSCIM ProfileSource = "SCIM"
)
func (s MembershipSource) String() string {
func (s ProfileSource) String() string {
return string(s)
}
func (s *MembershipSource) Scan(value any) error {
func (s *ProfileSource) Scan(value any) error {
var str string
switch v := value.(type) {
case string:
@@ -39,22 +39,22 @@ func (s *MembershipSource) Scan(value any) error {
case []byte:
str = string(v)
default:
return fmt.Errorf("unsupported type for MembershipSource: %T", value)
return fmt.Errorf("unsupported type for ProfileSource: %T", value)
}
switch str {
case "MANUAL":
*s = MembershipSourceManual
*s = ProfileSourceManual
case "SAML":
*s = MembershipSourceSAML
*s = ProfileSourceSAML
case "SCIM":
*s = MembershipSourceSCIM
*s = ProfileSourceSCIM
default:
return fmt.Errorf("invalid MembershipSource value: %q", str)
return fmt.Errorf("invalid ProfileSource value: %q", str)
}
return nil
}
func (s MembershipSource) Value() (driver.Value, error) {
func (s ProfileSource) Value() (driver.Value, error) {
return s.String(), nil
}

View File

@@ -19,18 +19,18 @@ import (
"fmt"
)
type MembershipState string
type ProfileState string
const (
MembershipStateActive MembershipState = "ACTIVE"
MembershipStateInactive MembershipState = "INACTIVE"
ProfileStateActive ProfileState = "ACTIVE"
ProfileStateInactive ProfileState = "INACTIVE"
)
func (s MembershipState) String() string {
func (s ProfileState) String() string {
return string(s)
}
func (s *MembershipState) Scan(value any) error {
func (s *ProfileState) Scan(value any) error {
var str string
switch v := value.(type) {
case string:
@@ -38,20 +38,20 @@ func (s *MembershipState) Scan(value any) error {
case []byte:
str = string(v)
default:
return fmt.Errorf("unsupported type for MembershipState: %T", value)
return fmt.Errorf("unsupported type for ProfileState: %T", value)
}
switch str {
case "ACTIVE":
*s = MembershipStateActive
*s = ProfileStateActive
case "INACTIVE":
*s = MembershipStateInactive
*s = ProfileStateInactive
default:
return fmt.Errorf("invalid MembershipState value: %q", str)
return fmt.Errorf("invalid ProfileState value: %q", str)
}
return nil
}
func (s MembershipState) Value() (driver.Value, error) {
func (s ProfileState) Value() (driver.Value, error) {
return s.String(), nil
}

View File

@@ -39,7 +39,7 @@ type (
ResponseBody *string `db:"response_body"`
StatusCode int `db:"status_code"`
ErrorMessage *string `db:"error_message"`
MembershipID *gid.GID `db:"membership_id"`
UserName string `db:"user_name"`
IPAddress net.IP `db:"ip_address"`
CreatedAt time.Time `db:"created_at"`
}
@@ -87,7 +87,7 @@ SELECT
response_body,
status_code,
error_message,
membership_id,
user_name,
ip_address,
created_at
FROM
@@ -139,7 +139,7 @@ INSERT INTO iam_scim_events (
response_body,
status_code,
error_message,
membership_id,
user_name,
ip_address,
created_at
) VALUES (
@@ -153,7 +153,7 @@ INSERT INTO iam_scim_events (
@response_body,
@status_code,
@error_message,
@membership_id,
@user_name,
@ip_address,
@created_at
)
@@ -170,7 +170,7 @@ INSERT INTO iam_scim_events (
"response_body": s.ResponseBody,
"status_code": s.StatusCode,
"error_message": s.ErrorMessage,
"membership_id": s.MembershipID,
"user_name": s.UserName,
"ip_address": s.IPAddress,
"created_at": s.CreatedAt,
}
@@ -201,7 +201,7 @@ SELECT
response_body,
status_code,
error_message,
membership_id,
user_name,
ip_address,
created_at
FROM
@@ -281,7 +281,7 @@ SELECT
response_body,
status_code,
error_message,
membership_id,
user_name,
ip_address,
created_at
FROM

View File

@@ -188,6 +188,7 @@ func (s *AccountService) AcceptInvitation(
) (*coredata.Invitation, *coredata.Membership, error) {
var (
now = time.Now()
profile = &coredata.MembershipProfile{}
membership = &coredata.Membership{}
invitation = &coredata.Invitation{}
)
@@ -228,47 +229,24 @@ func (s *AccountService) AcceptInvitation(
tenantID := invitation.OrganizationID.TenantID()
scope := coredata.NewScope(invitation.OrganizationID.TenantID())
existingMembership := &coredata.Membership{}
if err := existingMembership.LoadByIdentityAndOrg(
existingProfile := &coredata.MembershipProfile{}
if err := existingProfile.LoadByIdentityIDAndOrganizationID(
ctx,
tx,
scope,
identityID,
invitation.OrganizationID,
); err != nil && err != coredata.ErrResourceNotFound {
return fmt.Errorf("cannot load existing membership: %w", err)
}
if existingMembership.ID != gid.Nil && existingMembership.State == coredata.MembershipStateInactive {
existingMembership.State = coredata.MembershipStateActive
existingMembership.Role = invitation.Role
existingMembership.UpdatedAt = now
if err := existingMembership.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot reactivate membership: %w", err)
); err != nil {
if !errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("cannot load existing profile: %w", err)
}
membership = existingMembership
} else {
membership = &coredata.Membership{
ID: gid.New(tenantID, coredata.MembershipEntityType),
IdentityID: identityID,
OrganizationID: invitation.OrganizationID,
Role: invitation.Role,
Source: coredata.MembershipSourceManual,
State: coredata.MembershipStateActive,
CreatedAt: now,
UpdatedAt: now,
}
if err := membership.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot create membership: %w", err)
}
profile := &coredata.MembershipProfile{
profile = &coredata.MembershipProfile{
ID: gid.New(tenantID, coredata.MembershipProfileEntityType),
IdentityID: identity.ID,
OrganizationID: invitation.OrganizationID,
Source: coredata.ProfileSourceManual,
State: coredata.ProfileStateActive,
FullName: identity.FullName,
CreatedAt: now,
UpdatedAt: now,
@@ -277,6 +255,51 @@ func (s *AccountService) AcceptInvitation(
if err := profile.Insert(ctx, tx); err != nil {
return fmt.Errorf("cannot insert profile: %w", err)
}
} else {
if existingProfile.State == coredata.ProfileStateInactive {
existingProfile.State = coredata.ProfileStateActive
if err := existingProfile.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot reactivate profile: %w", err)
}
}
profile = existingProfile
}
existingMembership := &coredata.Membership{}
if err := existingMembership.LoadByIdentityAndOrg(
ctx,
tx,
scope,
identityID,
invitation.OrganizationID,
); err != nil {
if !errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("cannot load existing membership: %w", err)
}
membership = &coredata.Membership{
ID: gid.New(tenantID, coredata.MembershipEntityType),
IdentityID: identityID,
OrganizationID: invitation.OrganizationID,
Role: invitation.Role,
CreatedAt: now,
UpdatedAt: now,
}
if err := membership.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot create membership: %w", err)
}
} else {
existingMembership.Role = invitation.Role
existingMembership.UpdatedAt = now
if err := existingMembership.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot assign membership role: %w", err)
}
membership = existingMembership
}
invitation.AcceptedAt = &now
@@ -384,32 +407,6 @@ func (s *AccountService) CountPendingInvitations(
return count, nil
}
func (s *AccountService) CountMemberships(
ctx context.Context,
identityID gid.GID,
) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) (err error) {
memberships := coredata.Memberships{}
count, err = memberships.CountByIdentityID(ctx, conn, identityID)
if err != nil {
return fmt.Errorf("cannot count memberships: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s AccountService) ChangePassword(ctx context.Context, identityID gid.GID, req *ChangePasswordRequest) error {
if err := req.Validate(); err != nil {
return fmt.Errorf("invalid request: %w", err)
@@ -857,7 +854,7 @@ func (s *AccountService) ListProfilesForIdentity(
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := profiles.LoadByIdentityID(ctx, conn, coredata.NewNoScope(), identityID, cursor, filter); err != nil {
if err := profiles.LoadByIdentityID(ctx, conn, identityID, cursor, filter); err != nil {
return fmt.Errorf("cannot load profiles: %w", err)
}

View File

@@ -81,11 +81,10 @@ func (a *Authorizer) authorize(ctx context.Context, conn pg.Conn, params Authori
resourceOrgID := resourceAttrs["organization_id"]
// Find role for resource's organization
memberships, err := a.loadMemberships(ctx, conn, params.Principal)
membership, err := a.loadMembership(ctx, conn, params.Principal, resourceOrgID)
if err != nil {
return fmt.Errorf("cannot load memberships for principal: %w", err)
}
membership := findMembershipForOrg(memberships, resourceOrgID)
// Check whether the viewer is currently assuming the org of the accessed resource
if membership != nil && params.Session != nil {
@@ -144,12 +143,31 @@ func (a *Authorizer) authorize(ctx context.Context, conn pg.Conn, params Authori
return NewInsufficientPermissionsError(params.Principal, params.Resource, params.Action)
}
func (a *Authorizer) loadMemberships(ctx context.Context, conn pg.Conn, principalID gid.GID) (coredata.Memberships, error) {
var memberships coredata.Memberships
if err := memberships.LoadAllByIdentityID(ctx, conn, principalID); err != nil {
return nil, fmt.Errorf("cannot load memberships: %w", err)
func (a *Authorizer) loadMembership(
ctx context.Context,
conn pg.Conn,
principalID gid.GID,
resourceOrgID string,
) (*coredata.Membership, error) {
if resourceOrgID == "" {
return nil, nil
}
return memberships, nil
orgID, err := gid.ParseGID(resourceOrgID)
if err != nil {
return nil, fmt.Errorf("cannot parse gid: %w", err)
}
membership := &coredata.Membership{}
if err := membership.LoadActiveByIdentityIDAndOrganizationID(ctx, conn, principalID, orgID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, nil
}
return nil, fmt.Errorf("cannot load active membership: %w", err)
}
return membership, nil
}
func (a *Authorizer) getActiveChildSessionForMembership(
@@ -240,13 +258,3 @@ func (a *Authorizer) buildPoliciesForRole(role string) []*policy.Policy {
return policies
}
func findMembershipForOrg(memberships coredata.Memberships, orgID string) *coredata.Membership {
for _, m := range memberships {
if m.OrganizationID.String() == orgID && m.State == coredata.MembershipStateActive {
return m
}
}
return nil
}

View File

@@ -123,28 +123,28 @@ func (e ErrMembershipNotFound) Error() string {
return fmt.Sprintf("membership %q not found", e.MembershipID)
}
type ErrMembershipInactive struct {
MembershipID gid.GID
type ErrUserInactive struct {
ProfileID gid.GID
}
func NewMembershipInactiveError(membershipID gid.GID) error {
return &ErrMembershipInactive{MembershipID: membershipID}
func NewUserInactiveError(profileID gid.GID) error {
return &ErrUserInactive{ProfileID: profileID}
}
func (e ErrMembershipInactive) Error() string {
return fmt.Sprintf("membership %q is inactive", e.MembershipID)
func (e ErrUserInactive) Error() string {
return fmt.Sprintf("user %q is inactive", e.ProfileID)
}
type ErrMembershipManagedBySCIM struct {
MembershipID gid.GID
type ErrUserManagedBySCIM struct {
ProfileID gid.GID
}
func NewMembershipManagedBySCIMError(membershipID gid.GID) error {
return &ErrMembershipManagedBySCIM{MembershipID: membershipID}
func NewUserManagedBySCIMError(profileID gid.GID) error {
return &ErrUserManagedBySCIM{ProfileID: profileID}
}
func (e ErrMembershipManagedBySCIM) Error() string {
return fmt.Sprintf("membership %q is managed by SCIM and cannot be deleted manually", e.MembershipID)
func (e ErrUserManagedBySCIM) Error() string {
return fmt.Sprintf("user %q is managed by SCIM and cannot be deleted manually", e.ProfileID)
}
type ErrLastActiveOwner struct {
@@ -156,7 +156,7 @@ func NewLastActiveOwnerError(membershipID gid.GID) error {
}
func (e ErrLastActiveOwner) Error() string {
return fmt.Sprintf("cannot remove membership %q: last active owner of the organization", e.MembershipID)
return fmt.Sprintf("cannot remove profile %q: last active owner of the organization", e.MembershipID)
}
type ErrOrganizationNotFound struct{ OrganizationID gid.GID }
@@ -220,17 +220,17 @@ func (e ErrSessionExpired) Error() string {
return fmt.Sprintf("session %q expired", e.SessionID)
}
type ErrMembershipAlreadyExists struct {
type ErrUserAlreadyExists struct {
IdentityID gid.GID
OrganizationID gid.GID
}
func NewMembershipAlreadyExistsError(identityID gid.GID, organizationID gid.GID) error {
return &ErrMembershipAlreadyExists{IdentityID: identityID, OrganizationID: organizationID}
func NewUserAlreadyExistsError(identityID gid.GID, organizationID gid.GID) error {
return &ErrUserAlreadyExists{IdentityID: identityID, OrganizationID: organizationID}
}
func (e ErrMembershipAlreadyExists) Error() string {
return fmt.Sprintf("membership already exists for identity %q in organization %q", e.IdentityID, e.OrganizationID)
func (e ErrUserAlreadyExists) Error() string {
return fmt.Sprintf("user already exists for identity %q in organization %q", e.IdentityID, e.OrganizationID)
}
type ErrSAMLConfigurationNotFound struct{ ConfigID gid.GID }

View File

@@ -212,33 +212,6 @@ func NewOrganizationService(svc *Service) *OrganizationService {
return &OrganizationService{Service: svc}
}
func (s *OrganizationService) CountMemberships(
ctx context.Context,
organizationID gid.GID,
) (int, error) {
var count int
scope := coredata.NewScopeFromObjectID(organizationID)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) (err error) {
memberships := coredata.Memberships{}
count, err = memberships.CountByOrganizationID(ctx, conn, scope, organizationID, coredata.NewMembershipFilter())
if err != nil {
return fmt.Errorf("cannot count memberships: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *OrganizationService) UpdateMempership(
ctx context.Context,
organizationID gid.GID,
@@ -282,48 +255,53 @@ func (s *OrganizationService) UpdateMempership(
func (s *OrganizationService) RemoveMember(
ctx context.Context,
organizationID gid.GID,
membershipID gid.GID,
profileID gid.GID,
) error {
scope := coredata.NewScopeFromObjectID(organizationID)
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
membership := coredata.Membership{}
profile := coredata.MembershipProfile{}
if err := membership.LoadByID(ctx, tx, scope, membershipID); err != nil {
if err := profile.LoadByID(ctx, tx, scope, profileID); err != nil {
if err == coredata.ErrResourceNotFound {
return NewMembershipNotFoundError(membershipID)
return NewProfileNotFoundError(profileID)
}
return fmt.Errorf("cannot load profile: %w", err)
}
if profile.OrganizationID != organizationID {
return NewMembershipNotFoundError(profile.ID)
}
if profile.Source == coredata.ProfileSourceSCIM {
return NewUserManagedBySCIMError(profileID)
}
membership := &coredata.Membership{}
if err := membership.LoadByIdentityAndOrg(ctx, tx, scope, profile.IdentityID, profile.OrganizationID); err != nil {
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.OrganizationID != organizationID {
return NewMembershipNotFoundError(membership.ID)
}
if membership.Source == coredata.MembershipSourceSCIM {
return NewMembershipManagedBySCIMError(membershipID)
}
if membership.Role == coredata.MembershipRoleOwner && membership.State == coredata.MembershipStateActive {
memberships := coredata.Memberships{}
filter := coredata.NewMembershipFilter().
WithRole(coredata.MembershipRoleOwner).
WithState(coredata.MembershipStateActive)
count, err := memberships.CountByOrganizationID(ctx, tx, scope, organizationID, filter)
if membership.Role == coredata.MembershipRoleOwner && profile.State == coredata.ProfileStateActive {
profiles := coredata.MembershipProfiles{}
count, err := profiles.CountActiveOwnerByOrganizationID(ctx, tx, scope, organizationID)
if err != nil {
return fmt.Errorf("cannot count active owners: %w", err)
}
if count <= 1 {
return NewLastActiveOwnerError(membershipID)
return NewLastActiveOwnerError(profileID)
}
}
err := membership.Delete(ctx, tx, scope, membershipID)
if err != nil {
if err := profile.Delete(ctx, tx, scope, profileID); err != nil {
return fmt.Errorf("cannot delete profile: %w", err)
}
if err := membership.Delete(ctx, tx, scope, membership.ID); err != nil {
return fmt.Errorf("cannot delete membership: %w", err)
}
@@ -466,14 +444,14 @@ func (s *OrganizationService) InviteMember(
identityExists := identity.ID != gid.Nil
if identityExists {
membership := &coredata.Membership{}
err = membership.LoadByIdentityAndOrg(ctx, tx, scope, identity.ID, organizationID)
profile := &coredata.MembershipProfile{}
err = profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, identity.ID, organizationID)
if err != nil && err != coredata.ErrResourceNotFound {
return fmt.Errorf("cannot load membership: %w", err)
return fmt.Errorf("cannot load profile: %w", err)
}
if membership.ID != gid.Nil && membership.State == coredata.MembershipStateActive {
return NewMembershipAlreadyExistsError(identity.ID, organizationID)
if profile.ID != gid.Nil && profile.State == coredata.ProfileStateActive {
return NewUserAlreadyExistsError(identity.ID, organizationID)
}
}
@@ -549,13 +527,22 @@ func (s *OrganizationService) CreateOrganization(
UpdatedAt: now,
}
profile = &coredata.MembershipProfile{
ID: gid.New(tenantID, coredata.MembershipProfileEntityType),
IdentityID: identityID,
OrganizationID: organization.ID,
Source: coredata.ProfileSourceManual,
State: coredata.ProfileStateActive,
FullName: req.Name,
CreatedAt: now,
UpdatedAt: now,
}
membership = &coredata.Membership{
ID: gid.New(tenantID, coredata.MembershipEntityType),
IdentityID: identityID,
OrganizationID: organizationID,
Role: coredata.MembershipRoleOwner,
Source: coredata.MembershipSourceManual,
State: coredata.MembershipStateActive,
CreatedAt: now,
UpdatedAt: now,
}
@@ -688,25 +675,16 @@ func (s *OrganizationService) CreateOrganization(
organization.HorizontalLogoFileID = &horizontalLogoFile.ID
}
err = membership.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot create membership: %w", err)
}
profile := &coredata.MembershipProfile{
ID: gid.New(tenantID, coredata.MembershipProfileEntityType),
IdentityID: identity.ID,
OrganizationID: organization.ID,
FullName: identity.FullName,
CreatedAt: now,
UpdatedAt: now,
}
err = profile.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert profile: %w", err)
}
err = membership.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert membership: %w", err)
}
if err := organizationContext.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert organization context: %w", err)
}
@@ -1508,10 +1486,10 @@ func (s OrganizationService) DeleteSCIMConfiguration(
return scim.NewSCIMConfigurationNotFoundError(configID)
}
memberships := &coredata.Memberships{}
err = memberships.ResetSCIMSources(ctx, tx, scope, config.OrganizationID)
profiles := &coredata.MembershipProfiles{}
err = profiles.ResetSCIMSources(ctx, tx, scope, config.OrganizationID)
if err != nil {
return fmt.Errorf("cannot reset membership sources: %w", err)
return fmt.Errorf("cannot reset user sources: %w", err)
}
// Delete SCIM bridge and its connector if they exist

View File

@@ -89,12 +89,12 @@ func (e ErrSAMLAutoSignupDisabled) Error() string {
return fmt.Sprintf("SAML auto-signup is disabled for configuration %q", e.ConfigID)
}
type ErrMembershipInactive struct{ MembershipID gid.GID }
type ErrUserInactive struct{ ProfileID gid.GID }
func NewMembershipInactiveError(membershipID gid.GID) error {
return &ErrMembershipInactive{MembershipID: membershipID}
func NewUserInactiveError(profileID gid.GID) error {
return &ErrUserInactive{ProfileID: profileID}
}
func (e ErrMembershipInactive) Error() string {
return fmt.Sprintf("membership %q is inactive", e.MembershipID)
func (e ErrUserInactive) Error() string {
return fmt.Sprintf("user %q is inactive", e.ProfileID)
}

View File

@@ -296,16 +296,23 @@ func (s *Service) HandleAssertion(
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
err = profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, identity.ID, config.OrganizationID)
if err != nil && err != coredata.ErrResourceNotFound {
return fmt.Errorf("cannot load profile: %w", err)
}
if err := profile.LoadByIdentityIDAndOrganizationID(
ctx,
tx,
scope,
identity.ID,
config.OrganizationID,
); err != nil {
if !errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("cannot load profile: %w", err)
}
if profile.ID == gid.Nil {
profile = &coredata.MembershipProfile{
ID: gid.New(membership.ID.TenantID(), coredata.MembershipProfileEntityType),
IdentityID: identity.ID,
OrganizationID: config.OrganizationID,
Source: coredata.ProfileSourceSAML,
State: coredata.ProfileStateActive,
FullName: fullname,
CreatedAt: now,
UpdatedAt: now,
@@ -315,25 +322,28 @@ func (s *Service) HandleAssertion(
if err != nil {
return fmt.Errorf("cannot insert membership profile: %w", err)
}
} else {
if profile.State == coredata.ProfileStateInactive {
return NewUserInactiveError(membership.ID)
}
}
err = membership.LoadByIdentityAndOrg(ctx, tx, scope, identity.ID, config.OrganizationID)
if err != nil && err != coredata.ErrResourceNotFound {
return fmt.Errorf("cannot load membership: %w", err)
}
if err := membership.LoadByIdentityAndOrg(
ctx,
tx,
scope,
identity.ID,
config.OrganizationID,
); err != nil {
if !errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.ID != gid.Nil && membership.State == coredata.MembershipStateInactive {
return NewMembershipInactiveError(membership.ID)
}
if membership.ID == gid.Nil {
membership = &coredata.Membership{
ID: gid.New(config.ID.TenantID(), coredata.MembershipEntityType),
IdentityID: identity.ID,
OrganizationID: config.OrganizationID,
Role: coredata.MembershipRoleEmployee,
Source: coredata.MembershipSourceSAML,
State: coredata.MembershipStateActive,
CreatedAt: now,
UpdatedAt: now,
}
@@ -358,34 +368,26 @@ func (s *Service) HandleAssertion(
}
}
if membership.Source != coredata.MembershipSourceSCIM {
needsUpdate := false
if profile.Source != coredata.ProfileSourceSCIM {
profile.FullName = fullname
profile.UpdatedAt = now
if profile.Source == coredata.ProfileSourceManual {
profile.Source = coredata.ProfileSourceSAML
}
err = profile.Update(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot update profile: %w", err)
}
if role != nil {
membership.Role = *role
membership.UpdatedAt = now
needsUpdate = true
}
if membership.Source == coredata.MembershipSourceManual {
membership.Source = coredata.MembershipSourceSAML
membership.UpdatedAt = now
needsUpdate = true
}
if needsUpdate {
err = membership.Update(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot update membership: %w", err)
}
}
profile.FullName = fullname
profile.UpdatedAt = now
err = profile.Update(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot update profile: %w", err)
}
}
return nil

View File

@@ -24,8 +24,8 @@ import (
"go.probo.inc/probo/pkg/mail"
)
func ParseUserFilter(expr scimfilter.Expression) (*coredata.MembershipFilter, error) {
filter := coredata.NewMembershipFilter()
func ParseUserFilter(expr scimfilter.Expression) (*coredata.MembershipProfileFilter, error) {
filter := coredata.NewMembershipProfileFilter(nil)
if expr == nil {
return filter, nil

View File

@@ -19,6 +19,7 @@ import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net"
"strings"
@@ -138,103 +139,120 @@ func (s *Service) CreateUser(
}
now := time.Now()
membershipState := coredata.MembershipStateActive
profileState := coredata.ProfileStateActive
if !active {
membershipState = coredata.MembershipStateInactive
profileState = coredata.ProfileStateInactive
}
var membership *coredata.Membership
var profile *coredata.MembershipProfile
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
err = s.pg.WithTx(ctx, func(tx pg.Conn) error {
// Check if identity exists
identity := &coredata.Identity{}
err := identity.LoadByEmail(ctx, tx, emailAddr)
if err := identity.LoadByEmail(ctx, tx, emailAddr); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
// Create new identity
identity = &coredata.Identity{
ID: gid.New(gid.NilTenant, coredata.IdentityEntityType),
EmailAddress: emailAddr,
FullName: fullName,
HashedPassword: nil,
EmailAddressVerified: false,
CreatedAt: now,
UpdatedAt: now,
}
if err == coredata.ErrResourceNotFound {
// Create new identity
identity = &coredata.Identity{
ID: gid.New(gid.NilTenant, coredata.IdentityEntityType),
EmailAddress: emailAddr,
FullName: fullName,
HashedPassword: nil,
EmailAddressVerified: false,
CreatedAt: now,
UpdatedAt: now,
err = identity.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert identity: %w", err)
}
} else {
return fmt.Errorf("cannot load identity: %w", err)
}
}
err = identity.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert identity: %w", err)
// Check if profile exists
profile = &coredata.MembershipProfile{}
if err := profile.LoadByIdentityIDAndOrganizationID(
ctx,
tx,
coredata.NewScopeFromObjectID(config.OrganizationID),
identity.ID,
config.OrganizationID,
); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
profile = &coredata.MembershipProfile{
ID: gid.New(config.OrganizationID.TenantID(), coredata.MembershipProfileEntityType),
IdentityID: identity.ID,
OrganizationID: config.OrganizationID,
Source: coredata.ProfileSourceSCIM,
State: profileState,
FullName: fullName,
CreatedAt: now,
UpdatedAt: now,
}
err = profile.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert profile: %w", err)
}
} else {
return fmt.Errorf("cannot load profile: %w", err)
}
} else {
profile.Source = coredata.ProfileSourceSCIM
profile.State = profileState
profile.UpdatedAt = now
if err := profile.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update profile: %w", err)
}
} else if err != nil {
return fmt.Errorf("cannot load identity: %w", err)
}
// Check if membership exists
membership = &coredata.Membership{}
err = membership.LoadByIdentityAndOrg(ctx, tx, scope, identity.ID, config.OrganizationID)
if err := membership.LoadByIdentityAndOrg(
ctx,
tx,
scope,
identity.ID,
config.OrganizationID,
); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
// Create new membership
membership = &coredata.Membership{
ID: gid.New(config.OrganizationID.TenantID(), coredata.MembershipEntityType),
IdentityID: identity.ID,
OrganizationID: config.OrganizationID,
Role: coredata.MembershipRoleEmployee,
CreatedAt: now,
UpdatedAt: now,
}
if err == coredata.ErrResourceNotFound {
// Create new membership
membership = &coredata.Membership{
ID: gid.New(config.OrganizationID.TenantID(), coredata.MembershipEntityType),
IdentityID: identity.ID,
OrganizationID: config.OrganizationID,
Role: coredata.MembershipRoleEmployee,
Source: coredata.MembershipSourceSCIM,
State: membershipState,
CreatedAt: now,
UpdatedAt: now,
}
err = membership.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert membership: %w", err)
}
err = membership.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert membership: %w", err)
}
// Expire all pending invitations for email in organization
invitations := &coredata.Invitations{}
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
err := invitations.ExpireByEmailAndOrganization(
ctx,
tx,
coredata.NewScopeFromObjectID(config.OrganizationID),
emailAddr,
config.OrganizationID,
onlyPending,
)
// Create membership profile
membershipProfile := &coredata.MembershipProfile{
ID: gid.New(membership.ID.TenantID(), coredata.MembershipProfileEntityType),
IdentityID: identity.ID,
OrganizationID: config.OrganizationID,
FullName: fullName,
CreatedAt: now,
UpdatedAt: now,
}
err = membershipProfile.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert membership profile: %w", err)
}
// Expire all pending invitations for email in organization
invitations := &coredata.Invitations{}
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
err := invitations.ExpireByEmailAndOrganization(
ctx,
tx,
coredata.NewScopeFromObjectID(config.OrganizationID),
emailAddr,
config.OrganizationID,
onlyPending,
)
if err != nil {
return fmt.Errorf("cannot expire pending invitations by email")
}
} else if err != nil {
return fmt.Errorf("cannot load membership: %w", err)
} else {
// Update existing membership - follow what SCIM tells us
membership.Source = coredata.MembershipSourceSCIM
membership.State = membershipState
membership.UpdatedAt = now
err = membership.Update(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot update membership: %w", err)
if err != nil {
return fmt.Errorf("cannot expire pending invitations by email")
}
} else {
return fmt.Errorf("cannot load membership: %w", err)
}
}
@@ -245,32 +263,45 @@ func (s *Service) CreateUser(
return scim.Resource{}, err
}
return membershipToResource(membership), nil
return userToResource(profile), nil
}
func (s *Service) GetUser(
ctx context.Context,
config *coredata.SCIMConfiguration,
membershipID gid.GID,
profileID gid.GID,
) (scim.Resource, error) {
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
var membership *coredata.Membership
var (
profile *coredata.MembershipProfile
membership *coredata.Membership
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
membership = &coredata.Membership{}
err := membership.LoadByID(ctx, conn, scope, membershipID)
if err != nil {
profile = &coredata.MembershipProfile{}
if err := profile.LoadByID(ctx, conn, scope, profileID); err != nil {
if err == coredata.ErrResourceNotFound {
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
return scimerrors.ScimErrorResourceNotFound(profileID.String())
}
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.OrganizationID != config.OrganizationID {
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
if profile.OrganizationID != config.OrganizationID {
return scimerrors.ScimErrorResourceNotFound(profileID.String())
}
membership = &coredata.Membership{}
if err := membership.LoadByIdentityAndOrg(
ctx,
conn,
scope,
profile.IdentityID,
profile.OrganizationID,
); err != nil {
return fmt.Errorf("cannot load membership: %w", err)
}
return nil
@@ -281,7 +312,7 @@ func (s *Service) GetUser(
return scim.Resource{}, err
}
return membershipToResource(membership), nil
return userToResource(profile), nil
}
func (s *Service) ListUsers(
@@ -301,31 +332,31 @@ func (s *Service) ListUsers(
// when they don't exist in the identity provider.
// 2. When a manual user exists in the identity provider but not in the
// SCIM list, CreateUser is called which enrolls them into SCIM management.
filter.WithSource(coredata.MembershipSourceSCIM)
filter.WithSource(coredata.ProfileSourceSCIM)
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
var memberships coredata.Memberships
var profiles coredata.MembershipProfiles
var totalCount int
err = s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
var err error
totalCount, err = memberships.CountByOrganizationID(ctx, conn, scope, config.OrganizationID, filter)
totalCount, err = profiles.CountByOrganizationID(ctx, conn, scope, config.OrganizationID, filter)
if err != nil {
return fmt.Errorf("cannot count memberships: %w", err)
return fmt.Errorf("cannot count profiles: %w", err)
}
orderBy := page.OrderBy[coredata.MembershipOrderField]{
Field: coredata.MembershipOrderFieldCreatedAt,
orderBy := page.OrderBy[coredata.MembershipProfileOrderField]{
Field: coredata.MembershipProfileOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
cursor := page.NewCursor(count, nil, page.Head, orderBy)
err = memberships.LoadByOrganizationID(ctx, conn, scope, config.OrganizationID, cursor, filter)
err = profiles.LoadByOrganizationID(ctx, conn, scope, config.OrganizationID, cursor, filter)
if err != nil {
return fmt.Errorf("cannot load memberships: %w", err)
return fmt.Errorf("cannot load profiles: %w", err)
}
return nil
@@ -336,9 +367,9 @@ func (s *Service) ListUsers(
return nil, 0, err
}
resources := make([]scim.Resource, 0, len(memberships))
for _, m := range memberships {
resources = append(resources, membershipToResource(m))
resources := make([]scim.Resource, 0, len(profiles))
for _, p := range profiles {
resources = append(resources, userToResource(p))
}
return resources, totalCount, nil
@@ -347,61 +378,95 @@ func (s *Service) ListUsers(
func (s *Service) ReplaceUser(
ctx context.Context,
config *coredata.SCIMConfiguration,
membershipID gid.GID,
profileID gid.GID,
attributes scim.ResourceAttributes,
) (scim.Resource, error) {
fullName, active := ParseUserFromReplaceAttributes(attributes)
membership, err := s.updateUser(ctx, config, membershipID, fullName, active)
profile, err := s.updateUser(ctx, config, profileID, fullName, active)
if err != nil {
return scim.Resource{}, err
}
return membershipToResource(membership), nil
return userToResource(profile), nil
}
func (s *Service) PatchUser(
ctx context.Context,
config *coredata.SCIMConfiguration,
membershipID gid.GID,
profileID gid.GID,
operations []scim.PatchOperation,
) (scim.Resource, error) {
fullName, active := ParseUserFromPatchOperations(operations)
membership, err := s.updateUser(ctx, config, membershipID, fullName, active)
profile, err := s.updateUser(ctx, config, profileID, fullName, active)
if err != nil {
return scim.Resource{}, err
}
return membershipToResource(membership), nil
return userToResource(profile), nil
}
func (s *Service) updateUser(
ctx context.Context,
config *coredata.SCIMConfiguration,
membershipID gid.GID,
profileID gid.GID,
fullName string,
active *bool,
) (*coredata.Membership, error) {
) (*coredata.MembershipProfile, error) {
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
now := time.Now()
var membership *coredata.Membership
var (
membership *coredata.Membership
profile *coredata.MembershipProfile
)
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
membership = &coredata.Membership{}
if err := membership.LoadByID(ctx, tx, scope, membershipID); err != nil {
if err == coredata.ErrResourceNotFound {
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
profile = &coredata.MembershipProfile{}
if err := profile.LoadByID(ctx, tx, scope, profileID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return scimerrors.ScimErrorResourceNotFound(profileID.String())
}
return fmt.Errorf("cannot load profile: %w", err)
}
if profile.OrganizationID != config.OrganizationID {
return scimerrors.ScimErrorResourceNotFound(profileID.String())
}
membership = &coredata.Membership{}
if err := membership.LoadByIdentityAndOrg(ctx, tx, scope, profile.IdentityID, profile.OrganizationID); err != nil {
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.OrganizationID != config.OrganizationID {
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
shouldReactivate := active != nil && *active && profile.State == coredata.ProfileStateInactive
shouldDeactivate := active != nil && !*active && profile.State == coredata.ProfileStateActive
if fullName != "" {
profile.FullName = fullName
profile.UpdatedAt = now
}
needsUpdate := false
if shouldReactivate {
profile.State = coredata.ProfileStateActive
profile.UpdatedAt = now
} else if shouldDeactivate {
profile.State = coredata.ProfileStateInactive
profile.UpdatedAt = now
}
if profile.Source != coredata.ProfileSourceSCIM {
profile.Source = coredata.ProfileSourceSCIM
profile.UpdatedAt = now
}
if err := profile.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update membership profile: %w", err)
}
needsUpdate := shouldReactivate || shouldDeactivate
if active != nil {
identity := &coredata.Identity{}
@@ -409,11 +474,8 @@ func (s *Service) updateUser(
return fmt.Errorf("cannot load identity: %w", err)
}
if *active && membership.State == coredata.MembershipStateInactive {
membership.State = coredata.MembershipStateActive
if shouldReactivate {
membership.Role = coredata.MembershipRoleEmployee
needsUpdate = true
// Expire all pending invitations for email in organization
invitations := &coredata.Invitations{}
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
@@ -427,10 +489,7 @@ func (s *Service) updateUser(
); err != nil {
return fmt.Errorf("cannot expire pending invitations by email: %w", err)
}
} else if !*active && membership.State == coredata.MembershipStateActive {
membership.State = coredata.MembershipStateInactive
needsUpdate = true
} else if shouldDeactivate {
// Expire all pending invitations for email in organization
invitations := &coredata.Invitations{}
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
@@ -447,11 +506,6 @@ func (s *Service) updateUser(
}
}
if membership.Source != coredata.MembershipSourceSCIM {
membership.Source = coredata.MembershipSourceSCIM
needsUpdate = true
}
if needsUpdate {
membership.UpdatedAt = now
if err := membership.Update(ctx, tx, scope); err != nil {
@@ -459,18 +513,6 @@ func (s *Service) updateUser(
}
}
profile := &coredata.MembershipProfile{}
if err := profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, membership.IdentityID, membership.OrganizationID); err == nil {
if fullName != "" {
profile.FullName = fullName
profile.UpdatedAt = now
if err := profile.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update membership profile: %w", err)
}
}
}
return nil
},
)
@@ -479,7 +521,7 @@ func (s *Service) updateUser(
return nil, err
}
return membership, nil
return profile, nil
}
func (s *Service) DeleteUser(
@@ -538,19 +580,14 @@ func (s *Service) LogEvent(
config *coredata.SCIMConfiguration,
method string,
path string,
membershipID *gid.GID,
userName string,
ipAddress net.IP,
statusCode int,
errorMessage *string,
) {
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
var mID gid.GID
if membershipID != nil {
mID = *membershipID
}
event := s.createEvent(config, method, path, mID, ipAddress, statusCode, errorMessage)
event := s.createEvent(config, method, path, userName, ipAddress, statusCode, errorMessage)
err := s.pg.WithConn(
ctx,
@@ -572,7 +609,7 @@ func (s *Service) createEvent(
config *coredata.SCIMConfiguration,
method string,
path string,
membershipID gid.GID,
userName string,
ipAddress net.IP,
statusCode int,
errorMessage *string,
@@ -586,13 +623,10 @@ func (s *Service) createEvent(
StatusCode: statusCode,
ErrorMessage: errorMessage,
IPAddress: ipAddress,
UserName: userName,
CreatedAt: time.Now(),
}
if membershipID != gid.Nil {
event.MembershipID = &membershipID
}
return event
}
@@ -730,28 +764,28 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) (fullName st
return fullName, active
}
func membershipToResource(m *coredata.Membership) scim.Resource {
func userToResource(p *coredata.MembershipProfile) scim.Resource {
return scim.Resource{
ID: m.ID.String(),
ExternalID: optional.NewString(m.ID.String()),
ID: p.ID.String(),
ExternalID: optional.NewString(p.ID.String()),
Attributes: scim.ResourceAttributes{
"userName": m.EmailAddress.String(),
"displayName": m.FullName,
"active": m.State == coredata.MembershipStateActive,
"userName": p.EmailAddress.String(),
"displayName": p.FullName,
"active": p.State == coredata.ProfileStateActive,
"name": map[string]any{
"formatted": m.FullName,
"formatted": p.FullName,
},
"emails": []map[string]any{
{
"value": m.EmailAddress.String(),
"value": p.EmailAddress.String(),
"type": "work",
"primary": true,
},
},
},
Meta: scim.Meta{
Created: &m.CreatedAt,
LastModified: &m.UpdatedAt,
Created: &p.CreatedAt,
LastModified: &p.UpdatedAt,
},
}
}

View File

@@ -324,6 +324,7 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
now = time.Now()
rootSession = &coredata.Session{}
identity = &coredata.Identity{}
profile = &coredata.MembershipProfile{}
membership = &coredata.Membership{}
childSession = &coredata.Session{}
scope = coredata.NewScopeFromObjectID(organizationID)
@@ -353,6 +354,18 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
return fmt.Errorf("cannot load identity: %w", err)
}
err = profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, rootSession.IdentityID, organizationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return NewProfileNotFoundError(organizationID)
}
return fmt.Errorf("cannot load profile: %w", err)
}
if profile.State == coredata.ProfileStateInactive {
return NewUserInactiveError(profile.ID)
}
err = membership.LoadByIdentityInOrganization(ctx, tx, rootSession.IdentityID, organizationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
@@ -361,10 +374,6 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.State == coredata.MembershipStateInactive {
return NewMembershipInactiveError(membership.ID)
}
tenantID := scope.GetTenantID()
childSession = &coredata.Session{
ID: gid.New(tenantID, coredata.SessionEntityType),
@@ -417,6 +426,7 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
now = time.Now()
rootSession = &coredata.Session{}
identity = &coredata.Identity{}
profile = &coredata.MembershipProfile{}
membership = &coredata.Membership{}
childSession = &coredata.Session{}
scope = coredata.NewScopeFromObjectID(organizationID)
@@ -446,6 +456,18 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
return fmt.Errorf("cannot load identity: %w", err)
}
err = profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, rootSession.IdentityID, organizationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return NewProfileNotFoundError(organizationID)
}
return fmt.Errorf("cannot load profile: %w", err)
}
if profile.State == coredata.ProfileStateInactive {
return NewUserInactiveError(profile.ID)
}
err = membership.LoadByIdentityInOrganization(ctx, tx, rootSession.IdentityID, organizationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
@@ -454,10 +476,6 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.State == coredata.MembershipStateInactive {
return NewMembershipInactiveError(membership.ID)
}
tenantID := scope.GetTenantID()
childSession = &coredata.Session{
ID: gid.New(tenantID, coredata.SessionEntityType),
@@ -497,6 +515,7 @@ func (s SessionService) AssumeOrganizationSession(
now = time.Now()
rootSession = &coredata.Session{}
identity = &coredata.Identity{}
profile = &coredata.MembershipProfile{}
membership = &coredata.Membership{}
childSession = &coredata.Session{}
scope = coredata.NewScopeFromObjectID(organizationID)
@@ -524,6 +543,17 @@ func (s SessionService) AssumeOrganizationSession(
return fmt.Errorf("cannot load identity: %w", err)
}
if err := profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, rootSession.IdentityID, organizationID); err != nil {
if err == coredata.ErrResourceNotFound {
return NewProfileNotFoundError(organizationID)
}
return fmt.Errorf("cannot load profile: %w", err)
}
if profile.State == coredata.ProfileStateInactive {
return NewUserInactiveError(profile.ID)
}
if err := membership.LoadByIdentityInOrganization(ctx, tx, rootSession.IdentityID, organizationID); err != nil {
if err == coredata.ErrResourceNotFound {
return NewMembershipNotFoundError(organizationID)
@@ -531,10 +561,6 @@ func (s SessionService) AssumeOrganizationSession(
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.State == coredata.MembershipStateInactive {
return NewMembershipInactiveError(membership.ID)
}
samlConfig := &coredata.SAMLConfiguration{}
err := samlConfig.LoadByOrganizationIDAndEmailDomain(
ctx,

View File

@@ -181,6 +181,8 @@ type Identity implements Node {
type Profile implements Node {
id: ID!
fullName: String!
source: String!
state: ProfileState!
additionalEmailAddresses: [EmailAddr!]!
kind: ProfileKind!
position: String
@@ -198,6 +200,32 @@ type Profile implements Node {
@session(required: PRESENT)
}
enum ProfileState
@goModel(model: "go.probo.inc/probo/pkg/coredata.ProfileState") {
ACTIVE @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateActive")
INACTIVE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateInactive")
}
enum ProfileSource
@goModel(model: "go.probo.inc/probo/pkg/coredata.ProfileSource") {
MANUAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileSourceManual")
SAML @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileSourceSAML")
SCIM @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileSourceSCIM")
}
enum ProfileKind @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileKind") {
EMPLOYEE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindEmployee")
CONTRACTOR
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindContractor")
SERVICE_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindServiceAccount"
)
}
type Organization implements Node {
id: ID!
name: String!
@@ -243,17 +271,6 @@ type Organization implements Node {
@session(required: PRESENT)
}
enum ProfileKind @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileKind") {
EMPLOYEE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindEmployee")
CONTRACTOR
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindContractor")
SERVICE_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindServiceAccount"
)
}
enum MembershipRole
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") {
OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner")
@@ -265,27 +282,10 @@ enum MembershipRole
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor")
}
enum MembershipSource
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipSource") {
MANUAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipSourceManual")
SAML @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipSourceSAML")
SCIM @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipSourceSCIM")
}
enum MembershipState
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipState") {
ACTIVE @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipStateActive")
INACTIVE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipStateInactive")
}
type Membership implements Node {
id: ID!
createdAt: Datetime!
role: MembershipRole!
source: MembershipSource!
state: MembershipState!
lastSession: Session @goField(forceResolver: true)
@@ -440,7 +440,7 @@ type SCIMEvent implements Node {
requestBody: String
responseBody: String
errorMessage: String
profile: Profile @goField(forceResolver: true)
userName: String!
ipAddress: String!
createdAt: Datetime!

View File

@@ -192,8 +192,6 @@ type ComplexityRoot struct {
LastSession func(childComplexity int) int
Permission func(childComplexity int, action string) int
Role func(childComplexity int) int
Source func(childComplexity int) int
State func(childComplexity int) int
}
Mutation struct {
@@ -299,6 +297,8 @@ type ComplexityRoot struct {
Organization func(childComplexity int) int
Permission func(childComplexity int, action string) int
Position func(childComplexity int) int
Source func(childComplexity int) int
State func(childComplexity int) int
UpdatedAt func(childComplexity int) int
}
@@ -414,10 +414,10 @@ type ComplexityRoot struct {
Method func(childComplexity int) int
Path func(childComplexity int) int
Permission func(childComplexity int, action string) int
Profile func(childComplexity int) int
RequestBody func(childComplexity int) int
ResponseBody func(childComplexity int) int
StatusCode func(childComplexity int) int
UserName func(childComplexity int) int
}
SCIMEventConnection struct {
@@ -605,8 +605,6 @@ type SCIMConfigurationResolver interface {
Permission(ctx context.Context, obj *types.SCIMConfiguration, action string) (bool, error)
}
type SCIMEventResolver interface {
Profile(ctx context.Context, obj *types.SCIMEvent) (*types.Profile, error)
Permission(ctx context.Context, obj *types.SCIMEvent, action string) (bool, error)
}
type SCIMEventConnectionResolver interface {
@@ -1042,18 +1040,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.Membership.Role(childComplexity), true
case "Membership.source":
if e.complexity.Membership.Source == nil {
break
}
return e.complexity.Membership.Source(childComplexity), true
case "Membership.state":
if e.complexity.Membership.State == nil {
break
}
return e.complexity.Membership.State(childComplexity), true
case "Mutation.acceptInvitation":
if e.complexity.Mutation.AcceptInvitation == nil {
@@ -1706,6 +1692,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.Profile.Position(childComplexity), true
case "Profile.source":
if e.complexity.Profile.Source == nil {
break
}
return e.complexity.Profile.Source(childComplexity), true
case "Profile.state":
if e.complexity.Profile.State == nil {
break
}
return e.complexity.Profile.State(childComplexity), true
case "Profile.updatedAt":
if e.complexity.Profile.UpdatedAt == nil {
break
@@ -2142,12 +2140,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.SCIMEvent.Permission(childComplexity, args["action"].(string)), true
case "SCIMEvent.profile":
if e.complexity.SCIMEvent.Profile == nil {
break
}
return e.complexity.SCIMEvent.Profile(childComplexity), true
case "SCIMEvent.requestBody":
if e.complexity.SCIMEvent.RequestBody == nil {
break
@@ -2166,6 +2158,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.SCIMEvent.StatusCode(childComplexity), true
case "SCIMEvent.userName":
if e.complexity.SCIMEvent.UserName == nil {
break
}
return e.complexity.SCIMEvent.UserName(childComplexity), true
case "SCIMEventConnection.edges":
if e.complexity.SCIMEventConnection.Edges == nil {
@@ -2683,6 +2681,8 @@ type Identity implements Node {
type Profile implements Node {
id: ID!
fullName: String!
source: String!
state: ProfileState!
additionalEmailAddresses: [EmailAddr!]!
kind: ProfileKind!
position: String
@@ -2700,6 +2700,32 @@ type Profile implements Node {
@session(required: PRESENT)
}
enum ProfileState
@goModel(model: "go.probo.inc/probo/pkg/coredata.ProfileState") {
ACTIVE @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateActive")
INACTIVE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateInactive")
}
enum ProfileSource
@goModel(model: "go.probo.inc/probo/pkg/coredata.ProfileSource") {
MANUAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileSourceManual")
SAML @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileSourceSAML")
SCIM @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileSourceSCIM")
}
enum ProfileKind @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileKind") {
EMPLOYEE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindEmployee")
CONTRACTOR
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindContractor")
SERVICE_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindServiceAccount"
)
}
type Organization implements Node {
id: ID!
name: String!
@@ -2745,17 +2771,6 @@ type Organization implements Node {
@session(required: PRESENT)
}
enum ProfileKind @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileKind") {
EMPLOYEE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindEmployee")
CONTRACTOR
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindContractor")
SERVICE_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindServiceAccount"
)
}
enum MembershipRole
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") {
OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner")
@@ -2767,27 +2782,10 @@ enum MembershipRole
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor")
}
enum MembershipSource
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipSource") {
MANUAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipSourceManual")
SAML @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipSourceSAML")
SCIM @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipSourceSCIM")
}
enum MembershipState
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipState") {
ACTIVE @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipStateActive")
INACTIVE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipStateInactive")
}
type Membership implements Node {
id: ID!
createdAt: Datetime!
role: MembershipRole!
source: MembershipSource!
state: MembershipState!
lastSession: Session @goField(forceResolver: true)
@@ -2942,7 +2940,7 @@ type SCIMEvent implements Node {
requestBody: String
responseBody: String
errorMessage: String
profile: Profile @goField(forceResolver: true)
userName: String!
ipAddress: String!
createdAt: Datetime!
@@ -4335,10 +4333,6 @@ func (ec *executionContext) fieldContext_AcceptInvitationPayload_membership(_ co
return ec.fieldContext_Membership_createdAt(ctx, field)
case "role":
return ec.fieldContext_Membership_role(ctx, field)
case "source":
return ec.fieldContext_Membership_source(ctx, field)
case "state":
return ec.fieldContext_Membership_state(ctx, field)
case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field)
case "permission":
@@ -4756,10 +4750,6 @@ func (ec *executionContext) fieldContext_CreateOrganizationPayload_membership(_
return ec.fieldContext_Membership_createdAt(ctx, field)
case "role":
return ec.fieldContext_Membership_role(ctx, field)
case "source":
return ec.fieldContext_Membership_source(ctx, field)
case "state":
return ec.fieldContext_Membership_state(ctx, field)
case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field)
case "permission":
@@ -6338,64 +6328,6 @@ func (ec *executionContext) fieldContext_Membership_role(_ context.Context, fiel
return fc, nil
}
func (ec *executionContext) _Membership_source(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Membership_source,
func(ctx context.Context) (any, error) {
return obj.Source, nil
},
nil,
ec.marshalNMembershipSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipSource,
true,
true,
)
}
func (ec *executionContext) fieldContext_Membership_source(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Membership",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type MembershipSource does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Membership_state(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Membership_state,
func(ctx context.Context) (any, error) {
return obj.State, nil
},
nil,
ec.marshalNMembershipState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipState,
true,
true,
)
}
func (ec *executionContext) fieldContext_Membership_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Membership",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type MembershipState does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Membership_lastSession(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -8921,6 +8853,10 @@ func (ec *executionContext) fieldContext_Organization_viewer(_ context.Context,
return ec.fieldContext_Profile_id(ctx, field)
case "fullName":
return ec.fieldContext_Profile_fullName(ctx, field)
case "source":
return ec.fieldContext_Profile_source(ctx, field)
case "state":
return ec.fieldContext_Profile_state(ctx, field)
case "additionalEmailAddresses":
return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field)
case "kind":
@@ -9086,10 +9022,6 @@ func (ec *executionContext) fieldContext_OrganizationSessionCreated_membership(_
return ec.fieldContext_Membership_createdAt(ctx, field)
case "role":
return ec.fieldContext_Membership_role(ctx, field)
case "source":
return ec.fieldContext_Membership_source(ctx, field)
case "state":
return ec.fieldContext_Membership_state(ctx, field)
case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field)
case "permission":
@@ -9714,6 +9646,64 @@ func (ec *executionContext) fieldContext_Profile_fullName(_ context.Context, fie
return fc, nil
}
func (ec *executionContext) _Profile_source(ctx context.Context, field graphql.CollectedField, obj *types.Profile) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Profile_source,
func(ctx context.Context) (any, error) {
return obj.Source, nil
},
nil,
ec.marshalNString2string,
true,
true,
)
}
func (ec *executionContext) fieldContext_Profile_source(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Profile",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Profile_state(ctx context.Context, field graphql.CollectedField, obj *types.Profile) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Profile_state,
func(ctx context.Context) (any, error) {
return obj.State, nil
},
nil,
ec.marshalNProfileState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState,
true,
true,
)
}
func (ec *executionContext) fieldContext_Profile_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Profile",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type ProfileState does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Profile_additionalEmailAddresses(ctx context.Context, field graphql.CollectedField, obj *types.Profile) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -10063,10 +10053,6 @@ func (ec *executionContext) fieldContext_Profile_membership(_ context.Context, f
return ec.fieldContext_Membership_createdAt(ctx, field)
case "role":
return ec.fieldContext_Membership_role(ctx, field)
case "source":
return ec.fieldContext_Membership_source(ctx, field)
case "state":
return ec.fieldContext_Membership_state(ctx, field)
case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field)
case "permission":
@@ -10297,6 +10283,10 @@ func (ec *executionContext) fieldContext_ProfileEdge_node(_ context.Context, fie
return ec.fieldContext_Profile_id(ctx, field)
case "fullName":
return ec.fieldContext_Profile_fullName(ctx, field)
case "source":
return ec.fieldContext_Profile_source(ctx, field)
case "state":
return ec.fieldContext_Profile_state(ctx, field)
case "additionalEmailAddresses":
return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field)
case "kind":
@@ -12488,58 +12478,30 @@ func (ec *executionContext) fieldContext_SCIMEvent_errorMessage(_ context.Contex
return fc, nil
}
func (ec *executionContext) _SCIMEvent_profile(ctx context.Context, field graphql.CollectedField, obj *types.SCIMEvent) (ret graphql.Marshaler) {
func (ec *executionContext) _SCIMEvent_userName(ctx context.Context, field graphql.CollectedField, obj *types.SCIMEvent) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_SCIMEvent_profile,
ec.fieldContext_SCIMEvent_userName,
func(ctx context.Context) (any, error) {
return ec.resolvers.SCIMEvent().Profile(ctx, obj)
return obj.UserName, nil
},
nil,
ec.marshalOProfile2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐProfile,
ec.marshalNString2string,
true,
true,
false,
)
}
func (ec *executionContext) fieldContext_SCIMEvent_profile(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
func (ec *executionContext) fieldContext_SCIMEvent_userName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "SCIMEvent",
Field: field,
IsMethod: true,
IsResolver: true,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_Profile_id(ctx, field)
case "fullName":
return ec.fieldContext_Profile_fullName(ctx, field)
case "additionalEmailAddresses":
return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field)
case "kind":
return ec.fieldContext_Profile_kind(ctx, field)
case "position":
return ec.fieldContext_Profile_position(ctx, field)
case "contractStartDate":
return ec.fieldContext_Profile_contractStartDate(ctx, field)
case "contractEndDate":
return ec.fieldContext_Profile_contractEndDate(ctx, field)
case "createdAt":
return ec.fieldContext_Profile_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_Profile_updatedAt(ctx, field)
case "identity":
return ec.fieldContext_Profile_identity(ctx, field)
case "organization":
return ec.fieldContext_Profile_organization(ctx, field)
case "membership":
return ec.fieldContext_Profile_membership(ctx, field)
case "permission":
return ec.fieldContext_Profile_permission(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Profile", field.Name)
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
@@ -12803,8 +12765,8 @@ func (ec *executionContext) fieldContext_SCIMEventEdge_node(_ context.Context, f
return ec.fieldContext_SCIMEvent_responseBody(ctx, field)
case "errorMessage":
return ec.fieldContext_SCIMEvent_errorMessage(ctx, field)
case "profile":
return ec.fieldContext_SCIMEvent_profile(ctx, field)
case "userName":
return ec.fieldContext_SCIMEvent_userName(ctx, field)
case "ipAddress":
return ec.fieldContext_SCIMEvent_ipAddress(ctx, field)
case "createdAt":
@@ -13585,10 +13547,6 @@ func (ec *executionContext) fieldContext_UpdateMembershipPayload_membership(_ co
return ec.fieldContext_Membership_createdAt(ctx, field)
case "role":
return ec.fieldContext_Membership_role(ctx, field)
case "source":
return ec.fieldContext_Membership_source(ctx, field)
case "state":
return ec.fieldContext_Membership_state(ctx, field)
case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field)
case "permission":
@@ -13691,6 +13649,10 @@ func (ec *executionContext) fieldContext_UpdateProfilePayload_profile(_ context.
return ec.fieldContext_Profile_id(ctx, field)
case "fullName":
return ec.fieldContext_Profile_fullName(ctx, field)
case "source":
return ec.fieldContext_Profile_source(ctx, field)
case "state":
return ec.fieldContext_Profile_state(ctx, field)
case "additionalEmailAddresses":
return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field)
case "kind":
@@ -18019,16 +17981,6 @@ func (ec *executionContext) _Membership(ctx context.Context, sel ast.SelectionSe
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "source":
out.Values[i] = ec._Membership_source(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "state":
out.Values[i] = ec._Membership_state(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "lastSession":
field := field
@@ -19023,6 +18975,16 @@ func (ec *executionContext) _Profile(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "source":
out.Values[i] = ec._Profile_source(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "state":
out.Values[i] = ec._Profile_state(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "additionalEmailAddresses":
out.Values[i] = ec._Profile_additionalEmailAddresses(ctx, field, obj)
if out.Values[i] == graphql.Null {
@@ -20471,39 +20433,11 @@ func (ec *executionContext) _SCIMEvent(ctx context.Context, sel ast.SelectionSet
out.Values[i] = ec._SCIMEvent_responseBody(ctx, field, obj)
case "errorMessage":
out.Values[i] = ec._SCIMEvent_errorMessage(ctx, field, obj)
case "profile":
field := field
innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._SCIMEvent_profile(ctx, field, obj)
return res
case "userName":
out.Values[i] = ec._SCIMEvent_userName(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
if field.Deferrable != nil {
dfs, ok := deferred[field.Deferrable.Label]
di := 0
if ok {
dfs.AddField(field)
di = len(dfs.Values) - 1
} else {
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
deferred[field.Deferrable.Label] = dfs
}
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
return innerFunc(ctx, dfs)
})
// don't run the out.Concurrently() call below
out.Values[i] = graphql.Null
continue
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "ipAddress":
out.Values[i] = ec._SCIMEvent_ipAddress(ctx, field, obj)
if out.Values[i] == graphql.Null {
@@ -22073,64 +22007,6 @@ var (
}
)
func (ec *executionContext) unmarshalNMembershipSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipSource(ctx context.Context, v any) (coredata.MembershipSource, error) {
tmp, err := graphql.UnmarshalString(v)
res := unmarshalNMembershipSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipSource[tmp]
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNMembershipSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipSource(ctx context.Context, sel ast.SelectionSet, v coredata.MembershipSource) graphql.Marshaler {
_ = sel
res := graphql.MarshalString(marshalNMembershipSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipSource[v])
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow")
}
}
return res
}
var (
unmarshalNMembershipSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipSource = map[string]coredata.MembershipSource{
"MANUAL": coredata.MembershipSourceManual,
"SAML": coredata.MembershipSourceSAML,
"SCIM": coredata.MembershipSourceSCIM,
}
marshalNMembershipSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipSource = map[coredata.MembershipSource]string{
coredata.MembershipSourceManual: "MANUAL",
coredata.MembershipSourceSAML: "SAML",
coredata.MembershipSourceSCIM: "SCIM",
}
)
func (ec *executionContext) unmarshalNMembershipState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipState(ctx context.Context, v any) (coredata.MembershipState, error) {
tmp, err := graphql.UnmarshalString(v)
res := unmarshalNMembershipState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipState[tmp]
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNMembershipState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipState(ctx context.Context, sel ast.SelectionSet, v coredata.MembershipState) graphql.Marshaler {
_ = sel
res := graphql.MarshalString(marshalNMembershipState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipState[v])
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow")
}
}
return res
}
var (
unmarshalNMembershipState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipState = map[string]coredata.MembershipState{
"ACTIVE": coredata.MembershipStateActive,
"INACTIVE": coredata.MembershipStateInactive,
}
marshalNMembershipState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipState = map[coredata.MembershipState]string{
coredata.MembershipStateActive: "ACTIVE",
coredata.MembershipStateInactive: "INACTIVE",
}
)
func (ec *executionContext) unmarshalNOrderDirection2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐOrderDirection(ctx context.Context, v any) (page.OrderDirection, error) {
tmp, err := graphql.UnmarshalString(v)
res := unmarshalNOrderDirection2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐOrderDirection[tmp]
@@ -22363,6 +22239,34 @@ var (
}
)
func (ec *executionContext) unmarshalNProfileState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState(ctx context.Context, v any) (coredata.ProfileState, error) {
tmp, err := graphql.UnmarshalString(v)
res := unmarshalNProfileState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState[tmp]
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNProfileState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState(ctx context.Context, sel ast.SelectionSet, v coredata.ProfileState) graphql.Marshaler {
_ = sel
res := graphql.MarshalString(marshalNProfileState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState[v])
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow")
}
}
return res
}
var (
unmarshalNProfileState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState = map[string]coredata.ProfileState{
"ACTIVE": coredata.ProfileStateActive,
"INACTIVE": coredata.ProfileStateInactive,
}
marshalNProfileState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState = map[coredata.ProfileState]string{
coredata.ProfileStateActive: "ACTIVE",
coredata.ProfileStateInactive: "INACTIVE",
}
)
func (ec *executionContext) unmarshalNReauthenticationReason2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐReauthenticationReason(ctx context.Context, v any) (types.ReauthenticationReason, error) {
var res types.ReauthenticationReason
err := res.UnmarshalGQL(v)

View File

@@ -46,13 +46,13 @@ type (
}
scimRequestContext struct {
ctx context.Context
config *coredata.SCIMConfiguration
ipAddress net.IP
method string
path string
membershipID *gid.GID
handler *scimResourceHandler
ctx context.Context
config *coredata.SCIMConfiguration
ipAddress net.IP
method string
path string
userName string
handler *scimResourceHandler
}
)
@@ -145,23 +145,23 @@ func (rc *scimRequestContext) logAndWrapError(err error, logMsg string) error {
if errors.As(err, &scimErr) {
errMsg := scimErr.Detail
// Don't reference membershipID for 404 errors - the resource doesn't exist
membershipID := rc.membershipID
// Don't reference profileID for 404 errors - the resource doesn't exist
userName := rc.userName
if scimErr.Status == http.StatusNotFound {
membershipID = nil
userName = ""
}
rc.handler.handler.iam.SCIMService.LogEvent(rc.ctx, rc.config, rc.method, rc.path, membershipID, rc.ipAddress, scimErr.Status, &errMsg)
rc.handler.handler.iam.SCIMService.LogEvent(rc.ctx, rc.config, rc.method, rc.path, userName, rc.ipAddress, scimErr.Status, &errMsg)
return err
}
rc.handler.handler.logger.ErrorCtx(rc.ctx, logMsg, log.Error(err))
errMsg := "internal server error"
rc.handler.handler.iam.SCIMService.LogEvent(rc.ctx, rc.config, rc.method, rc.path, rc.membershipID, rc.ipAddress, 500, &errMsg)
rc.handler.handler.iam.SCIMService.LogEvent(rc.ctx, rc.config, rc.method, rc.path, rc.userName, rc.ipAddress, 500, &errMsg)
return scimerrors.ScimErrorInternal
}
func (rc *scimRequestContext) logSuccess(statusCode int) {
rc.handler.handler.iam.SCIMService.LogEvent(rc.ctx, rc.config, rc.method, rc.path, rc.membershipID, rc.ipAddress, statusCode, nil)
rc.handler.handler.iam.SCIMService.LogEvent(rc.ctx, rc.config, rc.method, rc.path, rc.userName, rc.ipAddress, statusCode, nil)
}
func (h *scimResourceHandler) Create(r *http.Request, attributes scim.ResourceAttributes) (scim.Resource, error) {
@@ -179,9 +179,10 @@ func (h *scimResourceHandler) Create(r *http.Request, attributes scim.ResourceAt
return scim.Resource{}, rc.logAndWrapError(err, "cannot create user")
}
membershipID, _ := gid.ParseGID(resource.ID)
rc.membershipID = &membershipID
rc.userName = resource.Attributes["userName"].(string)
rc.logSuccess(201)
return resource, nil
}
@@ -195,18 +196,20 @@ func (h *scimResourceHandler) Get(r *http.Request, id string) (scim.Resource, er
handler: h,
}
membershipID, err := gid.ParseGID(id)
profileID, err := gid.ParseGID(id)
if err != nil {
return scim.Resource{}, rc.logAndWrapError(scimerrors.ScimErrorResourceNotFound(id), "invalid membership ID")
return scim.Resource{}, rc.logAndWrapError(scimerrors.ScimErrorResourceNotFound(id), "invalid profile ID")
}
rc.membershipID = &membershipID
resource, err := h.handler.iam.SCIMService.GetUser(rc.ctx, rc.config, membershipID)
resource, err := h.handler.iam.SCIMService.GetUser(rc.ctx, rc.config, profileID)
if err != nil {
return scim.Resource{}, rc.logAndWrapError(err, "cannot get user")
}
rc.userName = resource.Attributes["userName"].(string)
rc.logSuccess(200)
return resource, nil
}
@@ -256,18 +259,20 @@ func (h *scimResourceHandler) Replace(r *http.Request, id string, attributes sci
handler: h,
}
membershipID, err := gid.ParseGID(id)
profileID, err := gid.ParseGID(id)
if err != nil {
return scim.Resource{}, rc.logAndWrapError(scimerrors.ScimErrorResourceNotFound(id), "invalid membership ID")
return scim.Resource{}, rc.logAndWrapError(scimerrors.ScimErrorResourceNotFound(id), "invalid profile ID")
}
rc.membershipID = &membershipID
resource, err := h.handler.iam.SCIMService.ReplaceUser(rc.ctx, rc.config, membershipID, attributes)
resource, err := h.handler.iam.SCIMService.ReplaceUser(rc.ctx, rc.config, profileID, attributes)
if err != nil {
return scim.Resource{}, rc.logAndWrapError(err, "cannot update user")
}
rc.userName = resource.Attributes["userName"].(string)
rc.logSuccess(200)
return resource, nil
}
@@ -281,18 +286,20 @@ func (h *scimResourceHandler) Patch(r *http.Request, id string, operations []sci
handler: h,
}
membershipID, err := gid.ParseGID(id)
profileID, err := gid.ParseGID(id)
if err != nil {
return scim.Resource{}, rc.logAndWrapError(scimerrors.ScimErrorResourceNotFound(id), "invalid membership ID")
return scim.Resource{}, rc.logAndWrapError(scimerrors.ScimErrorResourceNotFound(id), "invalid profile ID")
}
rc.membershipID = &membershipID
resource, err := h.handler.iam.SCIMService.PatchUser(rc.ctx, rc.config, membershipID, operations)
resource, err := h.handler.iam.SCIMService.PatchUser(rc.ctx, rc.config, profileID, operations)
if err != nil {
return scim.Resource{}, rc.logAndWrapError(err, "cannot patch user")
}
rc.userName = resource.Attributes["userName"].(string)
rc.logSuccess(200)
return resource, nil
}
@@ -306,19 +313,20 @@ func (h *scimResourceHandler) Delete(r *http.Request, id string) error {
handler: h,
}
membershipID, err := gid.ParseGID(id)
profileID, err := gid.ParseGID(id)
if err != nil {
return rc.logAndWrapError(scimerrors.ScimErrorResourceNotFound(id), "invalid membership ID")
return rc.logAndWrapError(scimerrors.ScimErrorResourceNotFound(id), "invalid profile ID")
}
rc.membershipID = &membershipID
err = h.handler.iam.SCIMService.DeleteUser(rc.ctx, rc.config, membershipID)
err = h.handler.iam.SCIMService.DeleteUser(rc.ctx, rc.config, profileID)
if err != nil {
return rc.logAndWrapError(err, "cannot delete user")
}
rc.membershipID = nil
rc.userName = ""
rc.logSuccess(204)
return nil
}

View File

@@ -23,7 +23,5 @@ func NewMembership(membership *coredata.Membership) *Membership {
ID: membership.ID,
CreatedAt: membership.CreatedAt,
Role: membership.Role,
Source: membership.Source,
State: membership.State,
}
}

View File

@@ -66,6 +66,8 @@ func NewProfile(profile *coredata.MembershipProfile) *Profile {
return &Profile{
ID: profile.ID,
FullName: profile.FullName,
Source: profile.Source.String(),
State: profile.State,
AdditionalEmailAddresses: profile.AdditionalEmailAddresses,
Kind: profile.Kind,
Position: profile.Position,

View File

@@ -64,6 +64,7 @@ func NewSCIMEvent(scimEvent *coredata.SCIMEvent) *SCIMEvent {
ID: scimEvent.ID,
Method: scimEvent.Method,
Path: scimEvent.Path,
UserName: scimEvent.UserName,
StatusCode: scimEvent.StatusCode,
RequestBody: scimEvent.RequestBody,
ResponseBody: scimEvent.ResponseBody,
@@ -72,12 +73,5 @@ func NewSCIMEvent(scimEvent *coredata.SCIMEvent) *SCIMEvent {
CreatedAt: scimEvent.CreatedAt,
}
// FIXME: profile
// if scimEvent.MembershipID != nil {
// event.Membership = &Membership{
// ID: *scimEvent.MembershipID,
// }
// }
return event
}

View File

@@ -220,13 +220,11 @@ type InviteMemberPayload struct {
}
type Membership struct {
ID gid.GID `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Role coredata.MembershipRole `json:"role"`
Source coredata.MembershipSource `json:"source"`
State coredata.MembershipState `json:"state"`
LastSession *Session `json:"lastSession,omitempty"`
Permission bool `json:"permission"`
ID gid.GID `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Role coredata.MembershipRole `json:"role"`
LastSession *Session `json:"lastSession,omitempty"`
Permission bool `json:"permission"`
}
func (Membership) IsNode() {}
@@ -298,6 +296,8 @@ type PersonalAPIKeyEdge struct {
type Profile struct {
ID gid.GID `json:"id"`
FullName string `json:"fullName"`
Source string `json:"source"`
State coredata.ProfileState `json:"state"`
AdditionalEmailAddresses []mail.Addr `json:"additionalEmailAddresses"`
Kind coredata.MembershipProfileKind `json:"kind"`
Position *string `json:"position,omitempty"`
@@ -452,7 +452,7 @@ type SCIMEvent struct {
RequestBody *string `json:"requestBody,omitempty"`
ResponseBody *string `json:"responseBody,omitempty"`
ErrorMessage *string `json:"errorMessage,omitempty"`
Profile *Profile `json:"profile,omitempty"`
UserName string `json:"userName"`
IPAddress string `json:"ipAddress"`
CreatedAt time.Time `json:"createdAt"`
Permission bool `json:"permission"`

View File

@@ -360,9 +360,9 @@ func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput)
if err != nil {
// Here session middleware already took care of expired/nil root session so we only handle membership related errors
var errMembershipNotFound *iam.ErrMembershipNotFound
var errMembershipInactive *iam.ErrMembershipInactive
var errUserInactive *iam.ErrUserInactive
if errors.As(err, &errMembershipNotFound) || errors.As(err, &errMembershipInactive) {
if errors.As(err, &errMembershipNotFound) || errors.As(err, &errUserInactive) {
return nil, gqlutils.Forbiddenf(ctx, "forbidden")
}
@@ -890,13 +890,13 @@ func (r *mutationResolver) InviteMember(ctx context.Context, input types.InviteM
)
if err != nil {
var errOrganizationNotFound *iam.ErrOrganizationNotFound
var errMembershipAlreadyExists *iam.ErrMembershipAlreadyExists
var errUserAlreadyExists *iam.ErrUserAlreadyExists
if errors.As(err, &errOrganizationNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if errors.As(err, &errMembershipAlreadyExists) {
if errors.As(err, &errUserAlreadyExists) {
return nil, gqlutils.Conflict(ctx, err)
}
@@ -994,7 +994,7 @@ func (r *mutationResolver) RemoveMember(ctx context.Context, input types.RemoveM
err := r.iam.OrganizationService.RemoveMember(ctx, input.OrganizationID, input.MembershipID)
if err != nil {
var errManagedBySCIM *iam.ErrMembershipManagedBySCIM
var errManagedBySCIM *iam.ErrUserManagedBySCIM
var errLastActiveOwner *iam.ErrLastActiveOwner
if errors.As(err, &errManagedBySCIM) {
@@ -1889,11 +1889,6 @@ func (r *sCIMConfigurationResolver) Permission(ctx context.Context, obj *types.S
return r.Resolver.Permission(ctx, obj, action)
}
// Profile is the resolver for the profile field.
func (r *sCIMEventResolver) Profile(ctx context.Context, obj *types.SCIMEvent) (*types.Profile, error) {
panic(fmt.Errorf("not implemented: Profile - profile"))
}
// Permission is the resolver for the permission field.
func (r *sCIMEventResolver) Permission(ctx context.Context, obj *types.SCIMEvent, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)