@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<e01416ecd129c81e31f5da90bf797ff7>>
|
* @generated SignedSource<<7759f335d6dd4b9ca6afbb601aa7794c>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -236,6 +236,13 @@ return {
|
|||||||
"name": "source",
|
"name": "source",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "state",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -396,12 +403,12 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "bdedd349fa3a3dec43d7cb6171d1cc6d",
|
"cacheID": "bb44371bed2eb3032b52ea5bbb3109c1",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "MemberListFragment_RefetchQuery",
|
"name": "MemberListFragment_RefetchQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query MemberListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MembershipOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MemberListFragment_16fISc\n id\n }\n}\n\nfragment MemberListFragment_16fISc on Organization {\n members(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n source\n profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership:delete\")\n}\n"
|
"text": "query MemberListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MembershipOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MemberListFragment_16fISc\n id\n }\n}\n\nfragment MemberListFragment_16fISc on Organization {\n members(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n source\n state\n profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership:delete\")\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<13e173a42e2ea29b7960539683cea087>>
|
* @generated SignedSource<<81a5acfba89528014adf0731f16b9a90>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
import { ReaderFragment } from 'relay-runtime';
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
|
export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
|
||||||
export type MembershipSource = "MANUAL" | "SAML" | "SCIM";
|
export type MembershipSource = "MANUAL" | "SAML" | "SCIM";
|
||||||
|
export type MembershipState = "ACTIVE" | "INACTIVE";
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type MemberListItemFragment$data = {
|
export type MemberListItemFragment$data = {
|
||||||
readonly canDelete: boolean;
|
readonly canDelete: boolean;
|
||||||
@@ -25,6 +26,7 @@ export type MemberListItemFragment$data = {
|
|||||||
};
|
};
|
||||||
readonly role: MembershipRole;
|
readonly role: MembershipRole;
|
||||||
readonly source: MembershipSource;
|
readonly source: MembershipSource;
|
||||||
|
readonly state: MembershipState;
|
||||||
readonly " $fragmentType": "MemberListItemFragment";
|
readonly " $fragmentType": "MemberListItemFragment";
|
||||||
};
|
};
|
||||||
export type MemberListItemFragment$key = {
|
export type MemberListItemFragment$key = {
|
||||||
@@ -59,6 +61,13 @@ const node: ReaderFragment = {
|
|||||||
"name": "source",
|
"name": "source",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "state",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "RequiredField",
|
"kind": "RequiredField",
|
||||||
"field": {
|
"field": {
|
||||||
@@ -141,6 +150,6 @@ const node: ReaderFragment = {
|
|||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
};
|
};
|
||||||
|
|
||||||
(node as any).hash = "573655b969ddacac10dc8beb4585a4a1";
|
(node as any).hash = "ffa3431077faeeb5e5779975d68c82bb";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<bd98e1c1ff5f9393deaec2a4aec1649c>>
|
* @generated SignedSource<<d7435d1c1ee31d5ddc186fd67fa9401f>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -345,6 +345,13 @@ return {
|
|||||||
"name": "source",
|
"name": "source",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "state",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -517,12 +524,12 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "b7d449bd737c6165219640affcc0260a",
|
"cacheID": "9cc9ff1663db9c476c1cd67f097a4ab9",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "MembersPageQuery",
|
"name": "MembersPageQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query MembersPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...InviteUserDialog_currentRoleFragment\n ...MemberListFragment_1jRT0c\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n }\n ...InvitationListFragment_1jRT0c\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n }\n canInviteUser: permission(action: \"iam:invitation:create\")\n }\n id\n }\n}\n\nfragment InvitationListFragment_1jRT0c on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\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 expiresAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment InviteUserDialog_currentRoleFragment on Organization {\n viewerMembership {\n role\n id\n }\n}\n\nfragment MemberListFragment_1jRT0c on Organization {\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n source\n profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership:delete\")\n}\n"
|
"text": "query MembersPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...InviteUserDialog_currentRoleFragment\n ...MemberListFragment_1jRT0c\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n }\n ...InvitationListFragment_1jRT0c\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n }\n canInviteUser: permission(action: \"iam:invitation:create\")\n }\n id\n }\n}\n\nfragment InvitationListFragment_1jRT0c on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\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 expiresAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment InviteUserDialog_currentRoleFragment on Organization {\n viewerMembership {\n role\n id\n }\n}\n\nfragment MemberListFragment_1jRT0c on Organization {\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n source\n state\n profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership:delete\")\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const fragment = graphql`
|
|||||||
id
|
id
|
||||||
role
|
role
|
||||||
source
|
source
|
||||||
|
state
|
||||||
profile @required(action: THROW) {
|
profile @required(action: THROW) {
|
||||||
fullName
|
fullName
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,8 @@ export function MemberListItem(props: {
|
|||||||
const membership = useFragment<MemberListItemFragment$key>(fragment, fKey);
|
const membership = useFragment<MemberListItemFragment$key>(fragment, fKey);
|
||||||
const { role } = use(CurrentUser);
|
const { role } = use(CurrentUser);
|
||||||
|
|
||||||
|
const isInactive = membership.state === "INACTIVE";
|
||||||
|
|
||||||
// Only OWNER can edit OWNER members
|
// Only OWNER can edit OWNER members
|
||||||
const canEditThisRole = membership.role === "OWNER" ? role === "OWNER" : true;
|
const canEditThisRole = membership.role === "OWNER" ? role === "OWNER" : true;
|
||||||
|
|
||||||
@@ -98,9 +101,17 @@ export function MemberListItem(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr className={clsx(isRemoving && "opacity-60 pointer-events-none")}>
|
<Tr
|
||||||
|
className={clsx(
|
||||||
|
isRemoving && "opacity-60 pointer-events-none",
|
||||||
|
isInactive && "opacity-50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="font-semibold">{membership.profile.fullName}</div>
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold">{membership.profile.fullName}</span>
|
||||||
|
{isInactive && <Badge variant="neutral">{__("Inactive")}</Badge>}
|
||||||
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -113,34 +124,36 @@ export function MemberListItem(props: {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td>{new Date(membership.createdAt).toLocaleDateString()}</Td>
|
<Td>{new Date(membership.createdAt).toLocaleDateString()}</Td>
|
||||||
<Td noLink width={160} className="text-end">
|
<Td noLink width={160} className="text-end">
|
||||||
<div
|
{!isInactive && (
|
||||||
className="flex gap-2 justify-end"
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}
|
className="flex gap-2 justify-end"
|
||||||
>
|
onClick={(e) => e.stopPropagation()}
|
||||||
{membership.canUpdate && canEditThisRole && (
|
>
|
||||||
<Button
|
{membership.canUpdate && canEditThisRole && (
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setDialogOpen(true)}
|
|
||||||
disabled={dialogOpen}
|
|
||||||
icon={IconPencil}
|
|
||||||
aria-label={__("Edit role")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isRemoving ? (
|
|
||||||
<Spinner size={16} />
|
|
||||||
) : (
|
|
||||||
membership.canDelete &&
|
|
||||||
canEditThisRole && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="secondary"
|
||||||
onClick={handleRemove}
|
onClick={() => setDialogOpen(true)}
|
||||||
disabled={isRemoving}
|
disabled={dialogOpen}
|
||||||
icon={IconTrashCan}
|
icon={IconPencil}
|
||||||
aria-label={__("Remove member")}
|
aria-label={__("Edit role")}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
)}
|
{isRemoving ? (
|
||||||
</div>
|
<Spinner size={16} />
|
||||||
|
) : (
|
||||||
|
membership.canDelete &&
|
||||||
|
canEditThisRole && (
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={handleRemove}
|
||||||
|
disabled={isRemoving}
|
||||||
|
icon={IconTrashCan}
|
||||||
|
aria-label={__("Remove member")}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type (
|
|||||||
OrganizationID gid.GID `db:"organization_id"`
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
Role MembershipRole `db:"role"`
|
Role MembershipRole `db:"role"`
|
||||||
Source MembershipSource `db:"source"`
|
Source MembershipSource `db:"source"`
|
||||||
|
State MembershipState `db:"state"`
|
||||||
FullName string `db:"full_name"`
|
FullName string `db:"full_name"`
|
||||||
EmailAddress mail.Addr `db:"email_address"`
|
EmailAddress mail.Addr `db:"email_address"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
@@ -69,6 +70,7 @@ WITH mbr AS (
|
|||||||
organization_id,
|
organization_id,
|
||||||
role,
|
role,
|
||||||
source,
|
source,
|
||||||
|
state,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -83,6 +85,7 @@ SELECT
|
|||||||
mbr.organization_id,
|
mbr.organization_id,
|
||||||
mbr.role,
|
mbr.role,
|
||||||
mbr.source,
|
mbr.source,
|
||||||
|
mbr.state,
|
||||||
COALESCE(mp.full_name, i.full_name, '') as full_name,
|
COALESCE(mp.full_name, i.full_name, '') as full_name,
|
||||||
i.email_address,
|
i.email_address,
|
||||||
mbr.created_at,
|
mbr.created_at,
|
||||||
@@ -126,6 +129,7 @@ INSERT INTO
|
|||||||
organization_id,
|
organization_id,
|
||||||
role,
|
role,
|
||||||
source,
|
source,
|
||||||
|
state,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
@@ -136,6 +140,7 @@ VALUES (
|
|||||||
@organization_id,
|
@organization_id,
|
||||||
@role,
|
@role,
|
||||||
@source,
|
@source,
|
||||||
|
@state,
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
);
|
);
|
||||||
@@ -148,6 +153,7 @@ VALUES (
|
|||||||
"organization_id": m.OrganizationID,
|
"organization_id": m.OrganizationID,
|
||||||
"role": m.Role,
|
"role": m.Role,
|
||||||
"source": m.Source,
|
"source": m.Source,
|
||||||
|
"state": m.State,
|
||||||
"created_at": m.CreatedAt,
|
"created_at": m.CreatedAt,
|
||||||
"updated_at": m.UpdatedAt,
|
"updated_at": m.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -183,6 +189,7 @@ WITH mbr AS (
|
|||||||
organization_id,
|
organization_id,
|
||||||
role,
|
role,
|
||||||
source,
|
source,
|
||||||
|
state,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -197,6 +204,7 @@ SELECT
|
|||||||
mbr.organization_id,
|
mbr.organization_id,
|
||||||
mbr.role,
|
mbr.role,
|
||||||
mbr.source,
|
mbr.source,
|
||||||
|
mbr.state,
|
||||||
COALESCE(mp.full_name, i.full_name, '') as full_name,
|
COALESCE(mp.full_name, i.full_name, '') as full_name,
|
||||||
i.email_address,
|
i.email_address,
|
||||||
mbr.created_at,
|
mbr.created_at,
|
||||||
@@ -334,6 +342,7 @@ WITH mbr AS (
|
|||||||
am.organization_id,
|
am.organization_id,
|
||||||
am.role,
|
am.role,
|
||||||
am.source,
|
am.source,
|
||||||
|
am.state,
|
||||||
am.created_at,
|
am.created_at,
|
||||||
am.updated_at
|
am.updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -349,6 +358,7 @@ SELECT
|
|||||||
mbr.organization_id,
|
mbr.organization_id,
|
||||||
mbr.role,
|
mbr.role,
|
||||||
mbr.source,
|
mbr.source,
|
||||||
|
mbr.state,
|
||||||
COALESCE(mp.full_name, i.full_name, '') as full_name,
|
COALESCE(mp.full_name, i.full_name, '') as full_name,
|
||||||
i.email_address,
|
i.email_address,
|
||||||
mbr.created_at,
|
mbr.created_at,
|
||||||
@@ -402,6 +412,7 @@ WITH mbr AS (
|
|||||||
am.organization_id,
|
am.organization_id,
|
||||||
am.role,
|
am.role,
|
||||||
am.source,
|
am.source,
|
||||||
|
am.state,
|
||||||
am.created_at,
|
am.created_at,
|
||||||
am.updated_at
|
am.updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -419,6 +430,7 @@ SELECT
|
|||||||
mbr.organization_id,
|
mbr.organization_id,
|
||||||
mbr.role,
|
mbr.role,
|
||||||
mbr.source,
|
mbr.source,
|
||||||
|
mbr.state,
|
||||||
COALESCE(mp.full_name, i.full_name, '') as full_name,
|
COALESCE(mp.full_name, i.full_name, '') as full_name,
|
||||||
i.email_address,
|
i.email_address,
|
||||||
mbr.created_at,
|
mbr.created_at,
|
||||||
@@ -464,6 +476,7 @@ UPDATE
|
|||||||
SET
|
SET
|
||||||
role = @role,
|
role = @role,
|
||||||
source = @source,
|
source = @source,
|
||||||
|
state = @state,
|
||||||
updated_at = @updated_at
|
updated_at = @updated_at
|
||||||
WHERE
|
WHERE
|
||||||
id = @id
|
id = @id
|
||||||
@@ -476,6 +489,7 @@ WHERE
|
|||||||
"id": m.ID,
|
"id": m.ID,
|
||||||
"role": m.Role,
|
"role": m.Role,
|
||||||
"source": m.Source,
|
"source": m.Source,
|
||||||
|
"state": m.State,
|
||||||
"updated_at": m.UpdatedAt,
|
"updated_at": m.UpdatedAt,
|
||||||
}
|
}
|
||||||
maps.Copy(args, scope.SQLArguments())
|
maps.Copy(args, scope.SQLArguments())
|
||||||
@@ -535,12 +549,14 @@ WITH mbr AS (
|
|||||||
organization_id,
|
organization_id,
|
||||||
role,
|
role,
|
||||||
source,
|
source,
|
||||||
|
state,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
iam_memberships
|
iam_memberships
|
||||||
WHERE
|
WHERE
|
||||||
identity_id = @identity_id
|
identity_id = @identity_id
|
||||||
|
AND state = 'ACTIVE'
|
||||||
AND %s
|
AND %s
|
||||||
ORDER BY
|
ORDER BY
|
||||||
created_at DESC
|
created_at DESC
|
||||||
@@ -551,6 +567,7 @@ SELECT
|
|||||||
mbr.organization_id,
|
mbr.organization_id,
|
||||||
mbr.role,
|
mbr.role,
|
||||||
mbr.source,
|
mbr.source,
|
||||||
|
mbr.state,
|
||||||
COALESCE(mp.full_name, i.full_name, '') as full_name,
|
COALESCE(mp.full_name, i.full_name, '') as full_name,
|
||||||
i.email_address,
|
i.email_address,
|
||||||
mbr.created_at,
|
mbr.created_at,
|
||||||
@@ -603,6 +620,7 @@ WITH membership_with_profile AS (
|
|||||||
m.organization_id,
|
m.organization_id,
|
||||||
m.role,
|
m.role,
|
||||||
m.source,
|
m.source,
|
||||||
|
m.state,
|
||||||
COALESCE(mp.full_name, i.full_name, '') AS full_name,
|
COALESCE(mp.full_name, i.full_name, '') AS full_name,
|
||||||
i.email_address,
|
i.email_address,
|
||||||
m.created_at,
|
m.created_at,
|
||||||
@@ -624,6 +642,7 @@ SELECT
|
|||||||
organization_id,
|
organization_id,
|
||||||
role,
|
role,
|
||||||
source,
|
source,
|
||||||
|
state,
|
||||||
full_name,
|
full_name,
|
||||||
email_address,
|
email_address,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -702,6 +721,7 @@ FROM
|
|||||||
iam_memberships
|
iam_memberships
|
||||||
WHERE
|
WHERE
|
||||||
identity_id = @identity_id
|
identity_id = @identity_id
|
||||||
|
AND state = 'ACTIVE'
|
||||||
`
|
`
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"identity_id": identityID,
|
"identity_id": identityID,
|
||||||
@@ -728,6 +748,7 @@ SELECT
|
|||||||
organization_id,
|
organization_id,
|
||||||
role,
|
role,
|
||||||
source,
|
source,
|
||||||
|
state,
|
||||||
'' as full_name,
|
'' as full_name,
|
||||||
NULL as email_address,
|
NULL as email_address,
|
||||||
created_at,
|
created_at,
|
||||||
|
|||||||
57
pkg/coredata/membership_state.go
Normal file
57
pkg/coredata/membership_state.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// 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 (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MembershipState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MembershipStateActive MembershipState = "ACTIVE"
|
||||||
|
MembershipStateInactive MembershipState = "INACTIVE"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s MembershipState) String() string {
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MembershipState) Scan(value any) error {
|
||||||
|
var str string
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
str = v
|
||||||
|
case []byte:
|
||||||
|
str = string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type for MembershipState: %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch str {
|
||||||
|
case "ACTIVE":
|
||||||
|
*s = MembershipStateActive
|
||||||
|
case "INACTIVE":
|
||||||
|
*s = MembershipStateInactive
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid MembershipState value: %q", str)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MembershipState) Value() (driver.Value, error) {
|
||||||
|
return s.String(), nil
|
||||||
|
}
|
||||||
5
pkg/coredata/migrations/20260105T134456Z.sql
Normal file
5
pkg/coredata/migrations/20260105T134456Z.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
CREATE TYPE membership_state AS ENUM ('ACTIVE', 'INACTIVE');
|
||||||
|
|
||||||
|
ALTER TABLE iam_memberships ADD COLUMN state membership_state NOT NULL DEFAULT 'ACTIVE';
|
||||||
|
CREATE INDEX idx_iam_memberships_state ON iam_memberships(state);
|
||||||
|
|
||||||
@@ -243,32 +243,52 @@ func (s *AccountService) AcceptInvitation(
|
|||||||
tenantID := invitation.OrganizationID.TenantID()
|
tenantID := invitation.OrganizationID.TenantID()
|
||||||
scope := coredata.NewScope(invitation.OrganizationID.TenantID())
|
scope := coredata.NewScope(invitation.OrganizationID.TenantID())
|
||||||
|
|
||||||
membership = &coredata.Membership{
|
existingMembership := &coredata.Membership{}
|
||||||
ID: gid.New(tenantID, coredata.MembershipEntityType),
|
err = existingMembership.LoadByIdentityAndOrg(ctx, tx, scope, identityID, invitation.OrganizationID)
|
||||||
IdentityID: identityID,
|
if err != nil && err != coredata.ErrResourceNotFound {
|
||||||
OrganizationID: invitation.OrganizationID,
|
return fmt.Errorf("cannot load existing membership: %w", err)
|
||||||
Role: invitation.Role,
|
|
||||||
Source: coredata.MembershipSourceManual,
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = membership.Insert(ctx, tx, scope)
|
if existingMembership.ID != gid.Nil && existingMembership.State == coredata.MembershipStateInactive {
|
||||||
if err != nil {
|
existingMembership.State = coredata.MembershipStateActive
|
||||||
return fmt.Errorf("cannot create membership: %w", err)
|
existingMembership.Role = invitation.Role
|
||||||
}
|
existingMembership.UpdatedAt = now
|
||||||
|
|
||||||
profile := &coredata.MembershipProfile{
|
err = existingMembership.Update(ctx, tx, scope)
|
||||||
ID: gid.New(tenantID, coredata.MembershipProfileEntityType),
|
if err != nil {
|
||||||
MembershipID: membership.ID,
|
return fmt.Errorf("cannot reactivate membership: %w", err)
|
||||||
FullName: identity.FullName,
|
}
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = profile.Insert(ctx, tx)
|
membership = existingMembership
|
||||||
if err != nil {
|
} else {
|
||||||
return fmt.Errorf("cannot insert profile: %w", err)
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
MembershipID: membership.ID,
|
||||||
|
FullName: identity.FullName,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = profile.Insert(ctx, tx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert profile: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invitation.AcceptedAt = &now
|
invitation.AcceptedAt = &now
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ func (a *Authorizer) buildPoliciesForRole(role string) []*policy.Policy {
|
|||||||
|
|
||||||
func findRoleForOrg(memberships coredata.Memberships, orgID string) string {
|
func findRoleForOrg(memberships coredata.Memberships, orgID string) string {
|
||||||
for _, m := range memberships {
|
for _, m := range memberships {
|
||||||
if m.OrganizationID.String() == orgID {
|
if m.OrganizationID.String() == orgID && m.State == coredata.MembershipStateActive {
|
||||||
return string(m.Role)
|
return string(m.Role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,18 @@ func (e ErrMembershipNotFound) Error() string {
|
|||||||
return fmt.Sprintf("membership %q not found", e.MembershipID)
|
return fmt.Sprintf("membership %q not found", e.MembershipID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrMembershipInactive struct {
|
||||||
|
MembershipID gid.GID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMembershipInactiveError(membershipID gid.GID) error {
|
||||||
|
return &ErrMembershipInactive{MembershipID: membershipID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrMembershipInactive) Error() string {
|
||||||
|
return fmt.Sprintf("membership %q is inactive", e.MembershipID)
|
||||||
|
}
|
||||||
|
|
||||||
type ErrOrganizationNotFound struct{ OrganizationID gid.GID }
|
type ErrOrganizationNotFound struct{ OrganizationID gid.GID }
|
||||||
|
|
||||||
func NewOrganizationNotFoundError(organizationID gid.GID) error {
|
func NewOrganizationNotFoundError(organizationID gid.GID) error {
|
||||||
|
|||||||
@@ -403,7 +403,7 @@ func (s *OrganizationService) InviteMember(
|
|||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if membership.ID != gid.Nil {
|
if membership.ID != gid.Nil && membership.State == coredata.MembershipStateActive {
|
||||||
return NewMembershipAlreadyExistsError(identity.ID, organizationID)
|
return NewMembershipAlreadyExistsError(identity.ID, organizationID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,6 +493,7 @@ func (s *OrganizationService) CreateOrganization(
|
|||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
Role: coredata.MembershipRoleOwner,
|
Role: coredata.MembershipRoleOwner,
|
||||||
Source: coredata.MembershipSourceManual,
|
Source: coredata.MembershipSourceManual,
|
||||||
|
State: coredata.MembershipStateActive,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,3 +88,13 @@ func NewSAMLAutoSignupDisabledError(configID gid.GID) error {
|
|||||||
func (e ErrSAMLAutoSignupDisabled) Error() string {
|
func (e ErrSAMLAutoSignupDisabled) Error() string {
|
||||||
return fmt.Sprintf("SAML auto-signup is disabled for configuration %q", e.ConfigID)
|
return fmt.Sprintf("SAML auto-signup is disabled for configuration %q", e.ConfigID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrMembershipInactive struct{ MembershipID gid.GID }
|
||||||
|
|
||||||
|
func NewMembershipInactiveError(membershipID gid.GID) error {
|
||||||
|
return &ErrMembershipInactive{MembershipID: membershipID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrMembershipInactive) Error() string {
|
||||||
|
return fmt.Sprintf("membership %q is inactive", e.MembershipID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -291,6 +291,10 @@ func (s *Service) HandleAssertion(
|
|||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if membership.ID != gid.Nil && membership.State == coredata.MembershipStateInactive {
|
||||||
|
return NewMembershipInactiveError(membership.ID)
|
||||||
|
}
|
||||||
|
|
||||||
isMember := membership.ID != gid.Nil
|
isMember := membership.ID != gid.Nil
|
||||||
if !isMember {
|
if !isMember {
|
||||||
membership = &coredata.Membership{
|
membership = &coredata.Membership{
|
||||||
@@ -299,6 +303,7 @@ func (s *Service) HandleAssertion(
|
|||||||
OrganizationID: config.OrganizationID,
|
OrganizationID: config.OrganizationID,
|
||||||
Role: coredata.MembershipRoleViewer,
|
Role: coredata.MembershipRoleViewer,
|
||||||
Source: coredata.MembershipSourceSAML,
|
Source: coredata.MembershipSourceSAML,
|
||||||
|
State: coredata.MembershipStateActive,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,16 +71,19 @@ func (s *Service) ValidateToken(ctx context.Context, token string) (*coredata.SC
|
|||||||
hashedToken := HashToken(token)
|
hashedToken := HashToken(token)
|
||||||
config := &coredata.SCIMConfiguration{}
|
config := &coredata.SCIMConfiguration{}
|
||||||
|
|
||||||
err := s.pg.WithConn(ctx, func(conn pg.Conn) error {
|
err := s.pg.WithConn(
|
||||||
err := config.LoadByHashedToken(ctx, conn, hashedToken)
|
ctx,
|
||||||
if err != nil {
|
func(conn pg.Conn) error {
|
||||||
if err == coredata.ErrResourceNotFound {
|
err := config.LoadByHashedToken(ctx, conn, hashedToken)
|
||||||
return NewSCIMInvalidTokenError()
|
if err != nil {
|
||||||
|
if err == coredata.ErrResourceNotFound {
|
||||||
|
return NewSCIMInvalidTokenError()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load SCIM configuration: %w", err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("cannot load SCIM configuration: %w", err)
|
return nil
|
||||||
}
|
},
|
||||||
return nil
|
)
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -89,7 +92,6 @@ func (s *Service) ValidateToken(ctx context.Context, token string) (*coredata.SC
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user via SCIM provisioning
|
|
||||||
func (s *Service) CreateUser(
|
func (s *Service) CreateUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
config *coredata.SCIMConfiguration,
|
config *coredata.SCIMConfiguration,
|
||||||
@@ -147,6 +149,7 @@ func (s *Service) CreateUser(
|
|||||||
OrganizationID: config.OrganizationID,
|
OrganizationID: config.OrganizationID,
|
||||||
Role: coredata.MembershipRoleViewer,
|
Role: coredata.MembershipRoleViewer,
|
||||||
Source: coredata.MembershipSourceSCIM,
|
Source: coredata.MembershipSourceSCIM,
|
||||||
|
State: coredata.MembershipStateActive,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
@@ -172,8 +175,9 @@ func (s *Service) CreateUser(
|
|||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
} else {
|
} else {
|
||||||
// Update existing membership source to SCIM
|
// Update existing membership - reactivate if inactive, update source to SCIM
|
||||||
membership.Source = coredata.MembershipSourceSCIM
|
membership.Source = coredata.MembershipSourceSCIM
|
||||||
|
membership.State = coredata.MembershipStateActive
|
||||||
membership.UpdatedAt = now
|
membership.UpdatedAt = now
|
||||||
|
|
||||||
err = membership.Update(ctx, tx, scope)
|
err = membership.Update(ctx, tx, scope)
|
||||||
@@ -189,10 +193,9 @@ func (s *Service) CreateUser(
|
|||||||
return scim.Resource{}, err
|
return scim.Resource{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return membershipToResource(membership, true), nil
|
return membershipToResource(membership), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUser gets a user by membership ID
|
|
||||||
func (s *Service) GetUser(
|
func (s *Service) GetUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
config *coredata.SCIMConfiguration,
|
config *coredata.SCIMConfiguration,
|
||||||
@@ -214,7 +217,6 @@ func (s *Service) GetUser(
|
|||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify membership belongs to this organization
|
|
||||||
if membership.OrganizationID != config.OrganizationID {
|
if membership.OrganizationID != config.OrganizationID {
|
||||||
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||||
}
|
}
|
||||||
@@ -227,7 +229,7 @@ func (s *Service) GetUser(
|
|||||||
return scim.Resource{}, err
|
return scim.Resource{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return membershipToResource(membership, true), nil
|
return membershipToResource(membership), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUsers lists all users in an organization, with optional filter support
|
// ListUsers lists all users in an organization, with optional filter support
|
||||||
@@ -248,26 +250,29 @@ func (s *Service) ListUsers(
|
|||||||
var memberships coredata.Memberships
|
var memberships coredata.Memberships
|
||||||
var totalCount int
|
var totalCount int
|
||||||
|
|
||||||
err = s.pg.WithConn(ctx, func(conn pg.Conn) error {
|
err = s.pg.WithConn(
|
||||||
var err error
|
ctx,
|
||||||
totalCount, err = memberships.CountByOrganizationID(ctx, conn, scope, config.OrganizationID, filter)
|
func(conn pg.Conn) error {
|
||||||
if err != nil {
|
var err error
|
||||||
return fmt.Errorf("cannot count memberships: %w", err)
|
totalCount, err = memberships.CountByOrganizationID(ctx, conn, scope, config.OrganizationID, filter)
|
||||||
}
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count memberships: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
orderBy := page.OrderBy[coredata.MembershipOrderField]{
|
orderBy := page.OrderBy[coredata.MembershipOrderField]{
|
||||||
Field: coredata.MembershipOrderFieldCreatedAt,
|
Field: coredata.MembershipOrderFieldCreatedAt,
|
||||||
Direction: page.OrderDirectionDesc,
|
Direction: page.OrderDirectionDesc,
|
||||||
}
|
}
|
||||||
cursor := page.NewCursor(count, nil, page.Head, orderBy)
|
cursor := page.NewCursor(count, nil, page.Head, orderBy)
|
||||||
|
|
||||||
err = memberships.LoadByOrganizationID(ctx, conn, scope, config.OrganizationID, cursor, filter)
|
err = memberships.LoadByOrganizationID(ctx, conn, scope, config.OrganizationID, cursor, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot load memberships: %w", err)
|
return fmt.Errorf("cannot load memberships: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -275,13 +280,12 @@ func (s *Service) ListUsers(
|
|||||||
|
|
||||||
resources := make([]scim.Resource, 0, len(memberships))
|
resources := make([]scim.Resource, 0, len(memberships))
|
||||||
for _, m := range memberships {
|
for _, m := range memberships {
|
||||||
resources = append(resources, membershipToResource(m, true))
|
resources = append(resources, membershipToResource(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
return resources, totalCount, nil
|
return resources, totalCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplaceUser replaces a user via SCIM PUT
|
|
||||||
func (s *Service) ReplaceUser(
|
func (s *Service) ReplaceUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
config *coredata.SCIMConfiguration,
|
config *coredata.SCIMConfiguration,
|
||||||
@@ -289,14 +293,13 @@ func (s *Service) ReplaceUser(
|
|||||||
attributes scim.ResourceAttributes,
|
attributes scim.ResourceAttributes,
|
||||||
) (scim.Resource, error) {
|
) (scim.Resource, error) {
|
||||||
fullName, active := ParseUserFromReplaceAttributes(attributes)
|
fullName, active := ParseUserFromReplaceAttributes(attributes)
|
||||||
membership, deactivated, err := s.updateUser(ctx, config, membershipID, fullName, active)
|
membership, err := s.updateUser(ctx, config, membershipID, fullName, active)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return scim.Resource{}, err
|
return scim.Resource{}, err
|
||||||
}
|
}
|
||||||
return membershipToResource(membership, !deactivated), nil
|
return membershipToResource(membership), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchUser patches a user via SCIM PATCH
|
|
||||||
func (s *Service) PatchUser(
|
func (s *Service) PatchUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
config *coredata.SCIMConfiguration,
|
config *coredata.SCIMConfiguration,
|
||||||
@@ -304,11 +307,11 @@ func (s *Service) PatchUser(
|
|||||||
operations []scim.PatchOperation,
|
operations []scim.PatchOperation,
|
||||||
) (scim.Resource, error) {
|
) (scim.Resource, error) {
|
||||||
fullName, active := ParseUserFromPatchOperations(operations)
|
fullName, active := ParseUserFromPatchOperations(operations)
|
||||||
membership, deactivated, err := s.updateUser(ctx, config, membershipID, fullName, active)
|
membership, err := s.updateUser(ctx, config, membershipID, fullName, active)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return scim.Resource{}, err
|
return scim.Resource{}, err
|
||||||
}
|
}
|
||||||
return membershipToResource(membership, !deactivated), nil
|
return membershipToResource(membership), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) updateUser(
|
func (s *Service) updateUser(
|
||||||
@@ -317,12 +320,11 @@ func (s *Service) updateUser(
|
|||||||
membershipID gid.GID,
|
membershipID gid.GID,
|
||||||
fullName string,
|
fullName string,
|
||||||
active *bool,
|
active *bool,
|
||||||
) (*coredata.Membership, bool, error) {
|
) (*coredata.Membership, error) {
|
||||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
var membership *coredata.Membership
|
var membership *coredata.Membership
|
||||||
var deactivated bool
|
|
||||||
|
|
||||||
err := s.pg.WithTx(ctx, func(tx pg.Conn) error {
|
err := s.pg.WithTx(ctx, func(tx pg.Conn) error {
|
||||||
membership = &coredata.Membership{}
|
membership = &coredata.Membership{}
|
||||||
@@ -334,35 +336,35 @@ func (s *Service) updateUser(
|
|||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify membership belongs to this organization
|
|
||||||
if membership.OrganizationID != config.OrganizationID {
|
if membership.OrganizationID != config.OrganizationID {
|
||||||
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle deactivation - Okta sends PATCH with active=false to deprovision users
|
needsUpdate := false
|
||||||
if active != nil && !*active {
|
|
||||||
err = membership.Delete(ctx, tx, scope, membershipID)
|
if active != nil {
|
||||||
if err != nil {
|
if *active && membership.State == coredata.MembershipStateInactive {
|
||||||
return fmt.Errorf("cannot delete membership: %w", err)
|
membership.State = coredata.MembershipStateActive
|
||||||
|
needsUpdate = true
|
||||||
|
} else if !*active && membership.State == coredata.MembershipStateActive {
|
||||||
|
membership.State = coredata.MembershipStateInactive
|
||||||
|
needsUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivated = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update membership source to SCIM if not already
|
|
||||||
if membership.Source != coredata.MembershipSourceSCIM {
|
if membership.Source != coredata.MembershipSourceSCIM {
|
||||||
membership.Source = coredata.MembershipSourceSCIM
|
membership.Source = coredata.MembershipSourceSCIM
|
||||||
membership.UpdatedAt = now
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsUpdate {
|
||||||
|
membership.UpdatedAt = now
|
||||||
err = membership.Update(ctx, tx, scope)
|
err = membership.Update(ctx, tx, scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot update membership: %w", err)
|
return fmt.Errorf("cannot update membership: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update membership profile
|
|
||||||
profile := &coredata.MembershipProfile{}
|
profile := &coredata.MembershipProfile{}
|
||||||
err = profile.LoadByMembershipID(ctx, tx, scope, membershipID)
|
err = profile.LoadByMembershipID(ctx, tx, scope, membershipID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -381,13 +383,12 @@ func (s *Service) updateUser(
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return membership, deactivated, nil
|
return membership, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser removes a user's membership from the organization
|
|
||||||
func (s *Service) DeleteUser(
|
func (s *Service) DeleteUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
config *coredata.SCIMConfiguration,
|
config *coredata.SCIMConfiguration,
|
||||||
@@ -395,31 +396,32 @@ func (s *Service) DeleteUser(
|
|||||||
) error {
|
) error {
|
||||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||||
|
|
||||||
return s.pg.WithTx(ctx, func(tx pg.Conn) error {
|
return s.pg.WithTx(
|
||||||
membership := &coredata.Membership{}
|
ctx,
|
||||||
err := membership.LoadByID(ctx, tx, scope, membershipID)
|
func(tx pg.Conn) error {
|
||||||
if err != nil {
|
membership := &coredata.Membership{}
|
||||||
if err == coredata.ErrResourceNotFound {
|
err := membership.LoadByID(ctx, tx, scope, membershipID)
|
||||||
|
if err != nil {
|
||||||
|
if err == coredata.ErrResourceNotFound {
|
||||||
|
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if membership.OrganizationID != config.OrganizationID {
|
||||||
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||||
}
|
}
|
||||||
return fmt.Errorf("cannot load membership: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify membership belongs to this organization
|
err = membership.Delete(ctx, tx, scope, membershipID)
|
||||||
if membership.OrganizationID != config.OrganizationID {
|
if err != nil {
|
||||||
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
return fmt.Errorf("cannot delete membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = membership.Delete(ctx, tx, scope, membershipID)
|
return nil
|
||||||
if err != nil {
|
},
|
||||||
return fmt.Errorf("cannot delete membership: %w", err)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogEvent logs a SCIM event
|
|
||||||
func (s *Service) LogEvent(
|
func (s *Service) LogEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
config *coredata.SCIMConfiguration,
|
config *coredata.SCIMConfiguration,
|
||||||
@@ -439,9 +441,16 @@ func (s *Service) LogEvent(
|
|||||||
|
|
||||||
event := s.createEvent(config, method, path, mID, ipAddress, statusCode, errorMessage)
|
event := s.createEvent(config, method, path, mID, ipAddress, statusCode, errorMessage)
|
||||||
|
|
||||||
err := s.pg.WithConn(ctx, func(conn pg.Conn) error {
|
err := s.pg.WithConn(
|
||||||
return event.Insert(ctx, conn, scope)
|
ctx,
|
||||||
})
|
func(conn pg.Conn) error {
|
||||||
|
err := event.Insert(ctx, conn, scope)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert SCIM event: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.ErrorCtx(ctx, "cannot log SCIM event", log.Error(err))
|
s.logger.ErrorCtx(ctx, "cannot log SCIM event", log.Error(err))
|
||||||
@@ -476,22 +485,21 @@ func (s *Service) createEvent(
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseUserFromAttributes extracts user data from SCIM create attributes
|
|
||||||
func ParseUserFromAttributes(attributes scim.ResourceAttributes) (email string, fullName string) {
|
func ParseUserFromAttributes(attributes scim.ResourceAttributes) (email string, fullName string) {
|
||||||
userName, _ := attributes["userName"].(string)
|
userName, _ := attributes["userName"].(string)
|
||||||
displayName, _ := attributes["displayName"].(string)
|
displayName, _ := attributes["displayName"].(string)
|
||||||
|
|
||||||
var givenName, familyName string
|
var givenName, familyName string
|
||||||
if name, ok := attributes["name"].(map[string]interface{}); ok {
|
if name, ok := attributes["name"].(map[string]any); ok {
|
||||||
givenName, _ = name["givenName"].(string)
|
givenName, _ = name["givenName"].(string)
|
||||||
familyName, _ = name["familyName"].(string)
|
familyName, _ = name["familyName"].(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get email from emails array or use userName
|
// Get email from emails array or use userName
|
||||||
email = userName
|
email = userName
|
||||||
if emails, ok := attributes["emails"].([]interface{}); ok && len(emails) > 0 {
|
if emails, ok := attributes["emails"].([]any); ok && len(emails) > 0 {
|
||||||
for _, e := range emails {
|
for _, e := range emails {
|
||||||
if emailMap, ok := e.(map[string]interface{}); ok {
|
if emailMap, ok := e.(map[string]any); ok {
|
||||||
if primary, _ := emailMap["primary"].(bool); primary {
|
if primary, _ := emailMap["primary"].(bool); primary {
|
||||||
if value, ok := emailMap["value"].(string); ok {
|
if value, ok := emailMap["value"].(string); ok {
|
||||||
email = value
|
email = value
|
||||||
@@ -502,7 +510,7 @@ func ParseUserFromAttributes(attributes scim.ResourceAttributes) (email string,
|
|||||||
}
|
}
|
||||||
// If no primary email found, use the first one
|
// If no primary email found, use the first one
|
||||||
if email == userName {
|
if email == userName {
|
||||||
if emailMap, ok := emails[0].(map[string]interface{}); ok {
|
if emailMap, ok := emails[0].(map[string]any); ok {
|
||||||
if value, ok := emailMap["value"].(string); ok {
|
if value, ok := emailMap["value"].(string); ok {
|
||||||
email = value
|
email = value
|
||||||
}
|
}
|
||||||
@@ -522,12 +530,11 @@ func ParseUserFromAttributes(attributes scim.ResourceAttributes) (email string,
|
|||||||
return email, fullName
|
return email, fullName
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseUserFromReplaceAttributes extracts user data from SCIM replace (PUT) attributes
|
|
||||||
func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) (fullName string, active *bool) {
|
func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) (fullName string, active *bool) {
|
||||||
displayName, _ := attributes["displayName"].(string)
|
displayName, _ := attributes["displayName"].(string)
|
||||||
|
|
||||||
var givenName, familyName string
|
var givenName, familyName string
|
||||||
if name, ok := attributes["name"].(map[string]interface{}); ok {
|
if name, ok := attributes["name"].(map[string]any); ok {
|
||||||
givenName, _ = name["givenName"].(string)
|
givenName, _ = name["givenName"].(string)
|
||||||
familyName, _ = name["familyName"].(string)
|
familyName, _ = name["familyName"].(string)
|
||||||
}
|
}
|
||||||
@@ -545,7 +552,6 @@ func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) (fullNam
|
|||||||
return fullName, &activeVal
|
return fullName, &activeVal
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseUserFromPatchOperations extracts user data from SCIM patch operations
|
|
||||||
func ParseUserFromPatchOperations(operations []scim.PatchOperation) (fullName string, active *bool) {
|
func ParseUserFromPatchOperations(operations []scim.PatchOperation) (fullName string, active *bool) {
|
||||||
var givenName, familyName string
|
var givenName, familyName string
|
||||||
|
|
||||||
@@ -555,6 +561,29 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) (fullName st
|
|||||||
if op.Path != nil {
|
if op.Path != nil {
|
||||||
path = op.Path.String()
|
path = op.Path.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle empty path with value map (Okta style)
|
||||||
|
// e.g., { "op": "Replace", "value": { "active": false } }
|
||||||
|
if path == "" {
|
||||||
|
if valueMap, ok := op.Value.(map[string]any); ok {
|
||||||
|
if a, ok := valueMap["active"].(bool); ok {
|
||||||
|
active = &a
|
||||||
|
}
|
||||||
|
if name, ok := valueMap["displayName"].(string); ok {
|
||||||
|
fullName = name
|
||||||
|
}
|
||||||
|
if nameMap, ok := valueMap["name"].(map[string]any); ok {
|
||||||
|
if gn, ok := nameMap["givenName"].(string); ok {
|
||||||
|
givenName = gn
|
||||||
|
}
|
||||||
|
if fn, ok := nameMap["familyName"].(string); ok {
|
||||||
|
familyName = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
switch strings.ToLower(path) {
|
switch strings.ToLower(path) {
|
||||||
case "active":
|
case "active":
|
||||||
if a, ok := op.Value.(bool); ok {
|
if a, ok := op.Value.(bool); ok {
|
||||||
@@ -584,14 +613,14 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) (fullName st
|
|||||||
return fullName, active
|
return fullName, active
|
||||||
}
|
}
|
||||||
|
|
||||||
func membershipToResource(m *coredata.Membership, active bool) scim.Resource {
|
func membershipToResource(m *coredata.Membership) scim.Resource {
|
||||||
return scim.Resource{
|
return scim.Resource{
|
||||||
ID: m.ID.String(),
|
ID: m.ID.String(),
|
||||||
ExternalID: optional.NewString(m.ID.String()),
|
ExternalID: optional.NewString(m.ID.String()),
|
||||||
Attributes: scim.ResourceAttributes{
|
Attributes: scim.ResourceAttributes{
|
||||||
"userName": m.EmailAddress.String(),
|
"userName": m.EmailAddress.String(),
|
||||||
"displayName": m.FullName,
|
"displayName": m.FullName,
|
||||||
"active": active,
|
"active": m.State == coredata.MembershipStateActive,
|
||||||
"name": map[string]any{
|
"name": map[string]any{
|
||||||
"formatted": m.FullName,
|
"formatted": m.FullName,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -367,6 +367,10 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
|
|||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if membership.State == coredata.MembershipStateInactive {
|
||||||
|
return NewMembershipInactiveError(membership.ID)
|
||||||
|
}
|
||||||
|
|
||||||
tenantID := scope.GetTenantID()
|
tenantID := scope.GetTenantID()
|
||||||
childSession = &coredata.Session{
|
childSession = &coredata.Session{
|
||||||
ID: gid.New(tenantID, coredata.SessionEntityType),
|
ID: gid.New(tenantID, coredata.SessionEntityType),
|
||||||
@@ -442,6 +446,10 @@ func (s SessionService) AssumeOrganizationSession(
|
|||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if membership.State == coredata.MembershipStateInactive {
|
||||||
|
return NewMembershipInactiveError(membership.ID)
|
||||||
|
}
|
||||||
|
|
||||||
samlConfig := &coredata.SAMLConfiguration{}
|
samlConfig := &coredata.SAMLConfiguration{}
|
||||||
err = samlConfig.LoadByOrganizationIDAndEmailDomain(
|
err = samlConfig.LoadByOrganizationIDAndEmailDomain(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -254,6 +254,13 @@ enum MembershipSource
|
|||||||
SCIM @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipSourceSCIM")
|
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 {
|
type Membership implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
@@ -262,6 +269,7 @@ type Membership implements Node {
|
|||||||
organization: Organization @goField(forceResolver: true)
|
organization: Organization @goField(forceResolver: true)
|
||||||
role: MembershipRole!
|
role: MembershipRole!
|
||||||
source: MembershipSource!
|
source: MembershipSource!
|
||||||
|
state: MembershipState!
|
||||||
|
|
||||||
lastSession: Session @goField(forceResolver: true) @isViewer
|
lastSession: Session @goField(forceResolver: true) @isViewer
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ type ComplexityRoot struct {
|
|||||||
Profile func(childComplexity int) int
|
Profile func(childComplexity int) int
|
||||||
Role func(childComplexity int) int
|
Role func(childComplexity int) int
|
||||||
Source func(childComplexity int) int
|
Source func(childComplexity int) int
|
||||||
|
State func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
MembershipConnection struct {
|
MembershipConnection struct {
|
||||||
@@ -964,6 +965,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return e.complexity.Membership.Source(childComplexity), true
|
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 "MembershipConnection.edges":
|
case "MembershipConnection.edges":
|
||||||
if e.complexity.MembershipConnection.Edges == nil {
|
if e.complexity.MembershipConnection.Edges == nil {
|
||||||
@@ -2533,6 +2540,13 @@ enum MembershipSource
|
|||||||
SCIM @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipSourceSCIM")
|
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 {
|
type Membership implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
@@ -2541,6 +2555,7 @@ type Membership implements Node {
|
|||||||
organization: Organization @goField(forceResolver: true)
|
organization: Organization @goField(forceResolver: true)
|
||||||
role: MembershipRole!
|
role: MembershipRole!
|
||||||
source: MembershipSource!
|
source: MembershipSource!
|
||||||
|
state: MembershipState!
|
||||||
|
|
||||||
lastSession: Session @goField(forceResolver: true) @isViewer
|
lastSession: Session @goField(forceResolver: true) @isViewer
|
||||||
|
|
||||||
@@ -5832,6 +5847,35 @@ func (ec *executionContext) fieldContext_Membership_source(_ context.Context, fi
|
|||||||
return fc, nil
|
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) {
|
func (ec *executionContext) _Membership_lastSession(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -6092,6 +6136,8 @@ func (ec *executionContext) fieldContext_MembershipEdge_node(_ context.Context,
|
|||||||
return ec.fieldContext_Membership_role(ctx, field)
|
return ec.fieldContext_Membership_role(ctx, field)
|
||||||
case "source":
|
case "source":
|
||||||
return ec.fieldContext_Membership_source(ctx, field)
|
return ec.fieldContext_Membership_source(ctx, field)
|
||||||
|
case "state":
|
||||||
|
return ec.fieldContext_Membership_state(ctx, field)
|
||||||
case "lastSession":
|
case "lastSession":
|
||||||
return ec.fieldContext_Membership_lastSession(ctx, field)
|
return ec.fieldContext_Membership_lastSession(ctx, field)
|
||||||
case "permission":
|
case "permission":
|
||||||
@@ -8622,6 +8668,8 @@ func (ec *executionContext) fieldContext_Organization_viewerMembership(_ context
|
|||||||
return ec.fieldContext_Membership_role(ctx, field)
|
return ec.fieldContext_Membership_role(ctx, field)
|
||||||
case "source":
|
case "source":
|
||||||
return ec.fieldContext_Membership_source(ctx, field)
|
return ec.fieldContext_Membership_source(ctx, field)
|
||||||
|
case "state":
|
||||||
|
return ec.fieldContext_Membership_state(ctx, field)
|
||||||
case "lastSession":
|
case "lastSession":
|
||||||
return ec.fieldContext_Membership_lastSession(ctx, field)
|
return ec.fieldContext_Membership_lastSession(ctx, field)
|
||||||
case "permission":
|
case "permission":
|
||||||
@@ -8777,6 +8825,8 @@ func (ec *executionContext) fieldContext_OrganizationSessionCreated_membership(_
|
|||||||
return ec.fieldContext_Membership_role(ctx, field)
|
return ec.fieldContext_Membership_role(ctx, field)
|
||||||
case "source":
|
case "source":
|
||||||
return ec.fieldContext_Membership_source(ctx, field)
|
return ec.fieldContext_Membership_source(ctx, field)
|
||||||
|
case "state":
|
||||||
|
return ec.fieldContext_Membership_state(ctx, field)
|
||||||
case "lastSession":
|
case "lastSession":
|
||||||
return ec.fieldContext_Membership_lastSession(ctx, field)
|
return ec.fieldContext_Membership_lastSession(ctx, field)
|
||||||
case "permission":
|
case "permission":
|
||||||
@@ -11175,6 +11225,8 @@ func (ec *executionContext) fieldContext_SCIMEvent_membership(_ context.Context,
|
|||||||
return ec.fieldContext_Membership_role(ctx, field)
|
return ec.fieldContext_Membership_role(ctx, field)
|
||||||
case "source":
|
case "source":
|
||||||
return ec.fieldContext_Membership_source(ctx, field)
|
return ec.fieldContext_Membership_source(ctx, field)
|
||||||
|
case "state":
|
||||||
|
return ec.fieldContext_Membership_state(ctx, field)
|
||||||
case "lastSession":
|
case "lastSession":
|
||||||
return ec.fieldContext_Membership_lastSession(ctx, field)
|
return ec.fieldContext_Membership_lastSession(ctx, field)
|
||||||
case "permission":
|
case "permission":
|
||||||
@@ -12326,6 +12378,8 @@ func (ec *executionContext) fieldContext_UpdateMembershipPayload_membership(_ co
|
|||||||
return ec.fieldContext_Membership_role(ctx, field)
|
return ec.fieldContext_Membership_role(ctx, field)
|
||||||
case "source":
|
case "source":
|
||||||
return ec.fieldContext_Membership_source(ctx, field)
|
return ec.fieldContext_Membership_source(ctx, field)
|
||||||
|
case "state":
|
||||||
|
return ec.fieldContext_Membership_state(ctx, field)
|
||||||
case "lastSession":
|
case "lastSession":
|
||||||
return ec.fieldContext_Membership_lastSession(ctx, field)
|
return ec.fieldContext_Membership_lastSession(ctx, field)
|
||||||
case "permission":
|
case "permission":
|
||||||
@@ -16471,6 +16525,11 @@ func (ec *executionContext) _Membership(ctx context.Context, sel ast.SelectionSe
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
atomic.AddUint32(&out.Invalids, 1)
|
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":
|
case "lastSession":
|
||||||
field := field
|
field := field
|
||||||
|
|
||||||
@@ -20219,6 +20278,34 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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) {
|
func (ec *executionContext) unmarshalNOrderDirection2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐOrderDirection(ctx context.Context, v any) (page.OrderDirection, error) {
|
||||||
tmp, err := graphql.UnmarshalString(v)
|
tmp, err := graphql.UnmarshalString(v)
|
||||||
res := unmarshalNOrderDirection2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐOrderDirection[tmp]
|
res := unmarshalNOrderDirection2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐOrderDirection[tmp]
|
||||||
|
|||||||
@@ -74,5 +74,6 @@ func NewMembership(membership *coredata.Membership) *Membership {
|
|||||||
},
|
},
|
||||||
Role: membership.Role,
|
Role: membership.Role,
|
||||||
Source: membership.Source,
|
Source: membership.Source,
|
||||||
|
State: membership.State,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ type Membership struct {
|
|||||||
Organization *Organization `json:"organization,omitempty"`
|
Organization *Organization `json:"organization,omitempty"`
|
||||||
Role coredata.MembershipRole `json:"role"`
|
Role coredata.MembershipRole `json:"role"`
|
||||||
Source coredata.MembershipSource `json:"source"`
|
Source coredata.MembershipSource `json:"source"`
|
||||||
|
State coredata.MembershipState `json:"state"`
|
||||||
LastSession *Session `json:"lastSession,omitempty"`
|
LastSession *Session `json:"lastSession,omitempty"`
|
||||||
Permission bool `json:"permission"`
|
Permission bool `json:"permission"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user