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 * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -222,6 +222,20 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v12/*: any*/), (v12/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -259,20 +273,6 @@ return {
"name": "role", "name": "role",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{ {
"alias": "canUpdate", "alias": "canUpdate",
"args": [ "args": [
@@ -430,12 +430,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "25aa17e466fdec627ba3d24d5230301b", "cacheID": "cc77d44877ae736c858e7458b525d75a",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PeopleListFragment_RefetchQuery", "name": "PeopleListFragment_RefetchQuery",
"operationKind": "query", "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 * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -10,9 +10,8 @@
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 MembershipState = "ACTIVE" | "INACTIVE";
export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT";
export type ProfileState = "ACTIVE" | "INACTIVE";
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type PeopleListItemFragment$data = { export type PeopleListItemFragment$data = {
readonly canUpdate: boolean; readonly canUpdate: boolean;
@@ -28,10 +27,10 @@ export type PeopleListItemFragment$data = {
readonly canUpdate: boolean; readonly canUpdate: boolean;
readonly id: string; readonly id: string;
readonly role: MembershipRole; readonly role: MembershipRole;
readonly source: MembershipSource;
readonly state: MembershipState;
}; };
readonly position: string | null | undefined; readonly position: string | null | undefined;
readonly source: string;
readonly state: ProfileState;
readonly " $fragmentType": "PeopleListItemFragment"; readonly " $fragmentType": "PeopleListItemFragment";
}; };
export type PeopleListItemFragment$key = { export type PeopleListItemFragment$key = {
@@ -54,6 +53,20 @@ return {
"name": "PeopleListItemFragment", "name": "PeopleListItemFragment",
"selections": [ "selections": [
(v0/*: any*/), (v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -93,20 +106,6 @@ return {
"name": "role", "name": "role",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{ {
"alias": "canUpdate", "alias": "canUpdate",
"args": [ "args": [
@@ -186,6 +185,6 @@ return {
}; };
})(); })();
(node as any).hash = "89702fbdf02294212269adb22efb65fe"; (node as any).hash = "ad9ed05edf606ec084c6f53462530b8b";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<e628337d33e88ca5cfe6e45a55eebe32>> * @generated SignedSource<<7f025767ad277b8b8de6cbe9b1490312>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -345,6 +345,20 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v10/*: any*/), (v10/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
(v11/*: any*/), (v11/*: any*/),
{ {
"alias": null, "alias": null,
@@ -370,20 +384,6 @@ return {
"selections": [ "selections": [
(v10/*: any*/), (v10/*: any*/),
(v12/*: 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", "alias": "canUpdate",
"args": [ "args": [
@@ -552,12 +552,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "4303ec6c2fa72181d747bb9053446aaa", "cacheID": "b67de1f40d556a5e4a6d78104e2eb224",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PeoplePageQuery", "name": "PeoplePageQuery",
"operationKind": "query", "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 * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -16,10 +16,8 @@ export type SCIMEventListItemFragment$data = {
readonly ipAddress: string; readonly ipAddress: string;
readonly method: string; readonly method: string;
readonly path: string; readonly path: string;
readonly profile: {
readonly fullName: string;
} | null | undefined;
readonly statusCode: number; readonly statusCode: number;
readonly userName: string;
readonly " $fragmentType": "SCIMEventListItemFragment"; readonly " $fragmentType": "SCIMEventListItemFragment";
}; };
export type SCIMEventListItemFragment$key = { export type SCIMEventListItemFragment$key = {
@@ -78,19 +76,8 @@ const node: ReaderFragment = {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "Profile", "kind": "ScalarField",
"kind": "LinkedField", "name": "userName",
"name": "profile",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null "storageKey": null
} }
], ],
@@ -98,6 +85,6 @@ const node: ReaderFragment = {
"abstractKey": null "abstractKey": null
}; };
(node as any).hash = "b38af9ac9b660d3aab75e00e2c55dfb6"; (node as any).hash = "9df2e986643ab90589ebc6e4631cde24";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<22d63f81020d1e99f7cdb84fc23913a4>> * @generated SignedSource<<08f3ffe97dd35388d5ce01977170773b>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -224,20 +224,8 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "Profile", "kind": "ScalarField",
"kind": "LinkedField", "name": "userName",
"name": "profile",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
(v8/*: any*/)
],
"storageKey": null "storageKey": null
}, },
(v7/*: any*/) (v7/*: any*/)
@@ -315,12 +303,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "58b36de1b4c1f54de6433a95b297b570", "cacheID": "5e80fcd5f3479d5b6bce65b59364d3f7",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "SCIMEventListPaginationQuery", "name": "SCIMEventListPaginationQuery",
"operationKind": "query", "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 * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -277,20 +277,8 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "Profile", "kind": "ScalarField",
"kind": "LinkedField", "name": "userName",
"name": "profile",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null "storageKey": null
}, },
(v2/*: any*/) (v2/*: any*/)
@@ -404,12 +392,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "3d49152bd3fd384a0b1de3d012268ebe", "cacheID": "b98c546a2c8fc038c121c21f3b495a7d",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "SCIMSettingsPageQuery", "name": "SCIMSettingsPageQuery",
"operationKind": "query", "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` const fragment = graphql`
fragment PeopleListItemFragment on Profile { fragment PeopleListItemFragment on Profile {
id id
source
state
fullName fullName
kind kind
position position
membership @required(action: THROW) { membership @required(action: THROW) {
id id
role role
source
state
canUpdate: permission(action: "iam:membership:update") canUpdate: permission(action: "iam:membership:update")
canDelete: permission(action: "iam:membership-profile:delete") canDelete: permission(action: "iam:membership-profile:delete")
} }
@@ -68,7 +68,7 @@ export function MemberListItem(props: {
const profile = useFragment<PeopleListItemFragment$key>(fragment, fKey); const profile = useFragment<PeopleListItemFragment$key>(fragment, fKey);
const isInactive = profile.membership.state === "INACTIVE"; const isInactive = profile.state === "INACTIVE";
const [removeMembership, isRemoving] = useMutationWithToasts( const [removeMembership, isRemoving] = useMutationWithToasts(
removeMemberMutation, removeMemberMutation,
@@ -118,7 +118,7 @@ export function MemberListItem(props: {
<Td> <Td>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{profile.identity.email} {profile.identity.email}
<Badge variant="info">{profile.membership.source}</Badge> <Badge variant="info">{profile.source}</Badge>
</div> </div>
</Td> </Td>
<Td>{profile.kind}</Td> <Td>{profile.kind}</Td>

View File

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

View File

@@ -25,24 +25,17 @@ import (
"github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
) )
type ( type (
Membership struct { Membership struct {
ID gid.GID `db:"id"` ID gid.GID `db:"id"`
IdentityID gid.GID `db:"identity_id"` IdentityID gid.GID `db:"identity_id"`
OrganizationID gid.GID `db:"organization_id"` OrganizationID gid.GID `db:"organization_id"`
Role MembershipRole `db:"role"` Role MembershipRole `db:"role"`
Source MembershipSource `db:"source"` CreatedAt time.Time `db:"created_at"`
State MembershipState `db:"state"` UpdatedAt time.Time `db:"updated_at"`
// 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"`
} }
Memberships []*Membership Memberships []*Membership
@@ -66,8 +59,6 @@ SELECT
identity_id, identity_id,
organization_id, organization_id,
role, role,
source,
state,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -109,8 +100,6 @@ INSERT INTO
identity_id, identity_id,
organization_id, organization_id,
role, role,
source,
state,
created_at, created_at,
updated_at updated_at
) )
@@ -120,8 +109,6 @@ VALUES (
@identity_id, @identity_id,
@organization_id, @organization_id,
@role, @role,
@source,
@state,
@created_at, @created_at,
@updated_at @updated_at
); );
@@ -133,8 +120,6 @@ VALUES (
"identity_id": m.IdentityID, "identity_id": m.IdentityID,
"organization_id": m.OrganizationID, "organization_id": m.OrganizationID,
"role": m.Role, "role": m.Role,
"source": m.Source,
"state": m.State,
"created_at": m.CreatedAt, "created_at": m.CreatedAt,
"updated_at": m.UpdatedAt, "updated_at": m.UpdatedAt,
} }
@@ -168,8 +153,6 @@ SELECT
identity_id, identity_id,
organization_id, organization_id,
role, role,
source,
state,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -209,8 +192,7 @@ func (m *Membership) AuthorizationAttributes(ctx context.Context, conn pg.Conn)
SELECT SELECT
identity_id, identity_id,
organization_id, organization_id,
role, role
source
FROM FROM
iam_memberships iam_memberships
WHERE WHERE
@@ -221,12 +203,10 @@ LIMIT 1;
var identityID gid.GID var identityID gid.GID
var organizationID gid.GID var organizationID gid.GID
var role MembershipRole var role MembershipRole
var source MembershipSource
if err := conn.QueryRow(ctx, q, m.ID).Scan( if err := conn.QueryRow(ctx, q, m.ID).Scan(
&identityID, &identityID,
&organizationID, &organizationID,
&role, &role,
&source,
); err != nil { ); err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrResourceNotFound return nil, ErrResourceNotFound
@@ -238,7 +218,6 @@ LIMIT 1;
"identity_id": identityID.String(), "identity_id": identityID.String(),
"organization_id": organizationID.String(), "organization_id": organizationID.String(),
"role": role.String(), "role": role.String(),
"source": source.String(),
}, nil }, nil
} }
@@ -255,8 +234,6 @@ SELECT
identity_id, identity_id,
organization_id, organization_id,
role, role,
source,
state,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -299,8 +276,6 @@ UPDATE
iam_memberships iam_memberships
SET SET
role = @role, role = @role,
source = @source,
state = @state,
updated_at = @updated_at updated_at = @updated_at
WHERE WHERE
id = @id id = @id
@@ -312,8 +287,6 @@ WHERE
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"id": m.ID, "id": m.ID,
"role": m.Role, "role": m.Role,
"source": m.Source,
"state": m.State,
"updated_at": m.UpdatedAt, "updated_at": m.UpdatedAt,
} }
maps.Copy(args, scope.SQLArguments()) maps.Copy(args, scope.SQLArguments())
@@ -358,180 +331,51 @@ WHERE
return nil return nil
} }
func (m *Memberships) LoadByOrganizationID( func (m *Membership) LoadActiveByIdentityIDAndOrganizationID(
ctx context.Context, ctx context.Context,
conn pg.Conn, conn pg.Conn,
scope Scoper, identityID gid.GID,
organizationID gid.GID, organizationID gid.GID,
cursor *page.Cursor[MembershipOrderField],
filter *MembershipFilter,
) error { ) error {
query := ` q := `
SELECT SELECT
id, m.id,
identity_id, m.identity_id,
organization_id, m.organization_id,
role, m.role,
source, m.created_at,
state, m.updated_at
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(*)
FROM FROM
iam_memberships m iam_memberships m
JOIN INNER JOIN iam_membership_profiles p
identities i ON m.identity_id = i.id ON p.identity_id = m.identity_id AND p.organization_id = m.organization_id
WHERE WHERE
m.organization_id = @organization_id p.state = @state
AND m.%s AND m.identity_id = @identity_id
AND %s AND m.organization_id = @organization_id
LIMIT 1
` `
query = fmt.Sprintf(query, scope.SQLFragment(), filter.SQLFragment())
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"state": ProfileStateActive,
"identity_id": identityID,
"organization_id": organizationID, "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( rows, err := conn.Query(ctx, q, args)
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)
if err != nil { if err != nil {
return fmt.Errorf("cannot query memberships: %w", err) 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 { if err != nil {
return fmt.Errorf("cannot collect memberships: %w", err) if errors.Is(err, pgx.ErrNoRows) {
} return ErrResourceNotFound
}
*m = memberships
return nil return fmt.Errorf("cannot collect membership: %w", err)
}
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)
} }
*m = *membership
return nil 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"` IdentityID gid.GID `db:"identity_id"`
OrganizationID gid.GID `db:"organization_id"` OrganizationID gid.GID `db:"organization_id"`
EmailAddress mail.Addr `db:"email_address"` EmailAddress mail.Addr `db:"email_address"`
Source ProfileSource `db:"source"`
State ProfileState `db:"state"`
FullName string `db:"full_name"` FullName string `db:"full_name"`
Kind MembershipProfileKind `db:"kind"` Kind MembershipProfileKind `db:"kind"`
AdditionalEmailAddresses mail.Addrs `db:"additional_email_addresses"` AdditionalEmailAddresses mail.Addrs `db:"additional_email_addresses"`
@@ -93,6 +95,8 @@ SELECT
p.identity_id, p.identity_id,
p.organization_id, p.organization_id,
i.email_address, i.email_address,
p.source,
p.state,
p.full_name, p.full_name,
p.kind, p.kind,
p.additional_email_addresses, p.additional_email_addresses,
@@ -149,6 +153,8 @@ SELECT
p.identity_id, p.identity_id,
p.organization_id, p.organization_id,
i.email_address, i.email_address,
p.source,
p.state,
p.full_name, p.full_name,
p.kind, p.kind,
p.additional_email_addresses, p.additional_email_addresses,
@@ -208,6 +214,8 @@ SELECT
p.identity_id, p.identity_id,
p.organization_id, p.organization_id,
i.email_address, i.email_address,
p.source,
p.state,
p.full_name, p.full_name,
p.kind, p.kind,
p.additional_email_addresses, p.additional_email_addresses,
@@ -257,30 +265,35 @@ func (p *MembershipProfiles) LoadByOrganizationID(
q := ` q := `
WITH profiles AS ( WITH profiles AS (
SELECT SELECT
id, p.id,
identity_id, p.identity_id,
organization_id, p.organization_id,
full_name, i.email_address,
kind, p.source,
additional_email_addresses, p.state,
position, p.full_name,
contract_start_date, p.kind,
contract_end_date, p.additional_email_addresses,
created_at, p.position,
updated_at p.contract_start_date,
p.contract_end_date,
p.created_at,
p.updated_at
FROM FROM
iam_membership_profiles iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE WHERE
%s p.%s
AND organization_id = @organization_id AND p.organization_id = @organization_id
AND %s
AND %s AND %s
) )
SELECT SELECT
p.id, p.id,
p.identity_id, p.identity_id,
p.organization_id, p.organization_id,
i.email_address, p.email_address,
p.source,
p.state,
p.full_name, p.full_name,
p.kind, p.kind,
p.additional_email_addresses, p.additional_email_addresses,
@@ -291,7 +304,8 @@ SELECT
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM profiles p 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()) 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( func (p *MembershipProfiles) LoadByIdentityID(
ctx context.Context, ctx context.Context,
conn pg.Conn, conn pg.Conn,
scope Scoper,
identityID gid.GID, identityID gid.GID,
cursor *page.Cursor[MembershipProfileOrderField], cursor *page.Cursor[MembershipProfileOrderField],
filter *MembershipProfileFilter, filter *MembershipProfileFilter,
@@ -325,29 +338,34 @@ func (p *MembershipProfiles) LoadByIdentityID(
q := ` q := `
WITH profiles AS ( WITH profiles AS (
SELECT SELECT
id, p.id,
identity_id, p.identity_id,
organization_id, p.organization_id,
full_name, i.email_address,
kind, p.source,
additional_email_addresses, p.state,
position, p.full_name,
contract_start_date, p.kind,
contract_end_date, p.additional_email_addresses,
created_at, p.position,
updated_at p.contract_start_date,
p.contract_end_date,
p.created_at,
p.updated_at
FROM FROM
iam_membership_profiles iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE WHERE
%s p.identity_id = @identity_id
AND identity_id = @identity_id
AND %s AND %s
) )
SELECT SELECT
p.id, p.id,
p.identity_id, p.identity_id,
p.organization_id, p.organization_id,
i.email_address, p.email_address,
p.source,
p.state,
p.full_name, p.full_name,
p.kind, p.kind,
p.additional_email_addresses, p.additional_email_addresses,
@@ -358,16 +376,14 @@ SELECT
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM profiles p FROM profiles p
INNER JOIN identities i ON i.id = p.identity_id
INNER JOIN organizations o ON o.id = p.organization_id INNER JOIN organizations o ON o.id = p.organization_id
WHERE WHERE
%s %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} args := pgx.NamedArgs{"identity_id": identityID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args) rows, err := conn.Query(ctx, q, args)
if err != nil { if err != nil {
@@ -600,6 +616,8 @@ WITH attendees AS (
p.identity_id, p.identity_id,
p.organization_id, p.organization_id,
i.email_address, i.email_address,
p.source,
p.state,
p.full_name, p.full_name,
p.kind, p.kind,
p.additional_email_addresses, p.additional_email_addresses,
@@ -624,6 +642,8 @@ SELECT
organization_id, organization_id,
kind, kind,
email_address, email_address,
source,
state,
full_name, full_name,
additional_email_addresses, additional_email_addresses,
position, position,
@@ -684,6 +704,8 @@ SELECT
p.kind, p.kind,
p.full_name, p.full_name,
i.email_address, i.email_address,
p.source,
p.state,
p.additional_email_addresses, p.additional_email_addresses,
p.position, p.position,
p.contract_start_date, p.contract_start_date,
@@ -725,10 +747,11 @@ func (p *MembershipProfiles) CountByIdentityID(
SELECT SELECT
COUNT(*) COUNT(*)
FROM FROM
iam_membership_profiles iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE WHERE
%s %s
AND identity_id = @identity_id AND p.identity_id = @identity_id
` `
q = fmt.Sprintf(q, filter.SQLFragment()) q = fmt.Sprintf(q, filter.SQLFragment())
@@ -758,11 +781,12 @@ func (p *MembershipProfiles) CountByOrganizationID(
SELECT SELECT
COUNT(*) COUNT(*)
FROM FROM
iam_membership_profiles iam_membership_profiles p
INNER JOIN identities i ON i.id = p.identity_id
WHERE WHERE
%s p.%s
AND %s AND %s
AND organization_id = @organization_id AND p.organization_id = @organization_id
` `
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
@@ -782,6 +806,45 @@ WHERE
return count, nil 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( func (p *MembershipProfile) Insert(
ctx context.Context, ctx context.Context,
conn pg.Conn, conn pg.Conn,
@@ -793,6 +856,8 @@ INSERT INTO
id, id,
identity_id, identity_id,
organization_id, organization_id,
source,
state,
full_name, full_name,
kind, kind,
additional_email_addresses, additional_email_addresses,
@@ -807,6 +872,8 @@ VALUES (
@id, @id,
@identity_id, @identity_id,
@organization_id, @organization_id,
@source,
@state,
@full_name, @full_name,
@kind, @kind,
COALESCE(@additional_email_addresses, '{}'::CITEXT[]), COALESCE(@additional_email_addresses, '{}'::CITEXT[]),
@@ -823,6 +890,8 @@ VALUES (
"id": p.ID, "id": p.ID,
"identity_id": p.IdentityID, "identity_id": p.IdentityID,
"organization_id": p.OrganizationID, "organization_id": p.OrganizationID,
"source": p.Source,
"state": p.State,
"full_name": p.FullName, "full_name": p.FullName,
"kind": p.Kind, "kind": p.Kind,
"additional_email_addresses": p.AdditionalEmailAddresses, "additional_email_addresses": p.AdditionalEmailAddresses,
@@ -850,6 +919,8 @@ func (p *MembershipProfile) Update(
UPDATE UPDATE
iam_membership_profiles iam_membership_profiles
SET SET
source = @source,
state = @state,
full_name = @full_name, full_name = @full_name,
kind = @kind, kind = @kind,
additional_email_addresses = @additional_email_addresses, additional_email_addresses = @additional_email_addresses,
@@ -866,6 +937,8 @@ WHERE
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"id": p.ID, "id": p.ID,
"source": p.Source,
"state": p.State,
"full_name": p.FullName, "full_name": p.FullName,
"kind": p.Kind, "kind": p.Kind,
"additional_email_addresses": p.AdditionalEmailAddresses, "additional_email_addresses": p.AdditionalEmailAddresses,
@@ -888,6 +961,38 @@ WHERE
return nil 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( func (p *MembershipProfile) Delete(
ctx context.Context, ctx context.Context,
conn pg.Conn, conn pg.Conn,

View File

@@ -18,12 +18,16 @@ import (
"time" "time"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"go.probo.inc/probo/pkg/mail"
) )
type ( type (
MembershipProfileFilter struct { MembershipProfileFilter struct {
excludeContractEnded *bool excludeContractEnded *bool
currentDate time.Time 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 { func (f *MembershipProfileFilter) SQLArguments() pgx.StrictNamedArgs {
return pgx.StrictNamedArgs{ return pgx.StrictNamedArgs{
"filter_email": f.email,
"exclude_contract_ended": f.excludeContractEnded, "exclude_contract_ended": f.excludeContractEnded,
"current_date": f.currentDate, "current_date": f.currentDate,
"filter_state": f.state,
"filter_source": f.source,
} }
} }
func (f *MembershipProfileFilter) SQLFragment() string { func (f *MembershipProfileFilter) SQLFragment() string {
return ` return `
( (
CASE
WHEN @filter_email::text IS NOT NULL THEN
i.email_address = @filter_email::text
ELSE TRUE
END
)
AND (
CASE CASE
WHEN @exclude_contract_ended::boolean IS NOT NULL AND @exclude_contract_ended::boolean = true THEN 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 ELSE TRUE
END 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 ALTER TABLE
iam_membership_profiles DROP COLUMN membership_id; 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" "fmt"
) )
type MembershipSource string type ProfileSource string
const ( const (
MembershipSourceManual MembershipSource = "MANUAL" ProfileSourceManual ProfileSource = "MANUAL"
MembershipSourceSAML MembershipSource = "SAML" ProfileSourceSAML ProfileSource = "SAML"
MembershipSourceSCIM MembershipSource = "SCIM" ProfileSourceSCIM ProfileSource = "SCIM"
) )
func (s MembershipSource) String() string { func (s ProfileSource) String() string {
return string(s) return string(s)
} }
func (s *MembershipSource) Scan(value any) error { func (s *ProfileSource) Scan(value any) error {
var str string var str string
switch v := value.(type) { switch v := value.(type) {
case string: case string:
@@ -39,22 +39,22 @@ func (s *MembershipSource) Scan(value any) error {
case []byte: case []byte:
str = string(v) str = string(v)
default: default:
return fmt.Errorf("unsupported type for MembershipSource: %T", value) return fmt.Errorf("unsupported type for ProfileSource: %T", value)
} }
switch str { switch str {
case "MANUAL": case "MANUAL":
*s = MembershipSourceManual *s = ProfileSourceManual
case "SAML": case "SAML":
*s = MembershipSourceSAML *s = ProfileSourceSAML
case "SCIM": case "SCIM":
*s = MembershipSourceSCIM *s = ProfileSourceSCIM
default: default:
return fmt.Errorf("invalid MembershipSource value: %q", str) return fmt.Errorf("invalid ProfileSource value: %q", str)
} }
return nil return nil
} }
func (s MembershipSource) Value() (driver.Value, error) { func (s ProfileSource) Value() (driver.Value, error) {
return s.String(), nil return s.String(), nil
} }

View File

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

View File

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

View File

@@ -188,6 +188,7 @@ func (s *AccountService) AcceptInvitation(
) (*coredata.Invitation, *coredata.Membership, error) { ) (*coredata.Invitation, *coredata.Membership, error) {
var ( var (
now = time.Now() now = time.Now()
profile = &coredata.MembershipProfile{}
membership = &coredata.Membership{} membership = &coredata.Membership{}
invitation = &coredata.Invitation{} invitation = &coredata.Invitation{}
) )
@@ -228,47 +229,24 @@ func (s *AccountService) AcceptInvitation(
tenantID := invitation.OrganizationID.TenantID() tenantID := invitation.OrganizationID.TenantID()
scope := coredata.NewScope(invitation.OrganizationID.TenantID()) scope := coredata.NewScope(invitation.OrganizationID.TenantID())
existingMembership := &coredata.Membership{} existingProfile := &coredata.MembershipProfile{}
if err := existingMembership.LoadByIdentityAndOrg( if err := existingProfile.LoadByIdentityIDAndOrganizationID(
ctx, ctx,
tx, tx,
scope, scope,
identityID, identityID,
invitation.OrganizationID, invitation.OrganizationID,
); err != nil && err != coredata.ErrResourceNotFound { ); err != nil {
return fmt.Errorf("cannot load existing membership: %w", err) if !errors.Is(err, coredata.ErrResourceNotFound) {
} return fmt.Errorf("cannot load existing profile: %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)
} }
membership = existingMembership profile = &coredata.MembershipProfile{
} 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{
ID: gid.New(tenantID, coredata.MembershipProfileEntityType), ID: gid.New(tenantID, coredata.MembershipProfileEntityType),
IdentityID: identity.ID, IdentityID: identity.ID,
OrganizationID: invitation.OrganizationID, OrganizationID: invitation.OrganizationID,
Source: coredata.ProfileSourceManual,
State: coredata.ProfileStateActive,
FullName: identity.FullName, FullName: identity.FullName,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
@@ -277,6 +255,51 @@ func (s *AccountService) AcceptInvitation(
if err := profile.Insert(ctx, tx); err != nil { if err := profile.Insert(ctx, tx); err != nil {
return fmt.Errorf("cannot insert profile: %w", err) 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 invitation.AcceptedAt = &now
@@ -384,32 +407,6 @@ func (s *AccountService) CountPendingInvitations(
return count, nil 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 { func (s AccountService) ChangePassword(ctx context.Context, identityID gid.GID, req *ChangePasswordRequest) error {
if err := req.Validate(); err != nil { if err := req.Validate(); err != nil {
return fmt.Errorf("invalid request: %w", err) return fmt.Errorf("invalid request: %w", err)
@@ -857,7 +854,7 @@ func (s *AccountService) ListProfilesForIdentity(
err := s.pg.WithConn( err := s.pg.WithConn(
ctx, ctx,
func(conn pg.Conn) error { 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) 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"] resourceOrgID := resourceAttrs["organization_id"]
// Find role for resource's organization // 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 { if err != nil {
return fmt.Errorf("cannot load memberships for principal: %w", err) 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 // Check whether the viewer is currently assuming the org of the accessed resource
if membership != nil && params.Session != nil { 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) return NewInsufficientPermissionsError(params.Principal, params.Resource, params.Action)
} }
func (a *Authorizer) loadMemberships(ctx context.Context, conn pg.Conn, principalID gid.GID) (coredata.Memberships, error) { func (a *Authorizer) loadMembership(
var memberships coredata.Memberships ctx context.Context,
if err := memberships.LoadAllByIdentityID(ctx, conn, principalID); err != nil { conn pg.Conn,
return nil, fmt.Errorf("cannot load memberships: %w", err) 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( func (a *Authorizer) getActiveChildSessionForMembership(
@@ -240,13 +258,3 @@ func (a *Authorizer) buildPoliciesForRole(role string) []*policy.Policy {
return policies 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) return fmt.Sprintf("membership %q not found", e.MembershipID)
} }
type ErrMembershipInactive struct { type ErrUserInactive struct {
MembershipID gid.GID ProfileID gid.GID
} }
func NewMembershipInactiveError(membershipID gid.GID) error { func NewUserInactiveError(profileID gid.GID) error {
return &ErrMembershipInactive{MembershipID: membershipID} return &ErrUserInactive{ProfileID: profileID}
} }
func (e ErrMembershipInactive) Error() string { func (e ErrUserInactive) Error() string {
return fmt.Sprintf("membership %q is inactive", e.MembershipID) return fmt.Sprintf("user %q is inactive", e.ProfileID)
} }
type ErrMembershipManagedBySCIM struct { type ErrUserManagedBySCIM struct {
MembershipID gid.GID ProfileID gid.GID
} }
func NewMembershipManagedBySCIMError(membershipID gid.GID) error { func NewUserManagedBySCIMError(profileID gid.GID) error {
return &ErrMembershipManagedBySCIM{MembershipID: membershipID} return &ErrUserManagedBySCIM{ProfileID: profileID}
} }
func (e ErrMembershipManagedBySCIM) Error() string { func (e ErrUserManagedBySCIM) Error() string {
return fmt.Sprintf("membership %q is managed by SCIM and cannot be deleted manually", e.MembershipID) return fmt.Sprintf("user %q is managed by SCIM and cannot be deleted manually", e.ProfileID)
} }
type ErrLastActiveOwner struct { type ErrLastActiveOwner struct {
@@ -156,7 +156,7 @@ func NewLastActiveOwnerError(membershipID gid.GID) error {
} }
func (e ErrLastActiveOwner) Error() string { 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 } type ErrOrganizationNotFound struct{ OrganizationID gid.GID }
@@ -220,17 +220,17 @@ func (e ErrSessionExpired) Error() string {
return fmt.Sprintf("session %q expired", e.SessionID) return fmt.Sprintf("session %q expired", e.SessionID)
} }
type ErrMembershipAlreadyExists struct { type ErrUserAlreadyExists struct {
IdentityID gid.GID IdentityID gid.GID
OrganizationID gid.GID OrganizationID gid.GID
} }
func NewMembershipAlreadyExistsError(identityID gid.GID, organizationID gid.GID) error { func NewUserAlreadyExistsError(identityID gid.GID, organizationID gid.GID) error {
return &ErrMembershipAlreadyExists{IdentityID: identityID, OrganizationID: organizationID} return &ErrUserAlreadyExists{IdentityID: identityID, OrganizationID: organizationID}
} }
func (e ErrMembershipAlreadyExists) Error() string { func (e ErrUserAlreadyExists) Error() string {
return fmt.Sprintf("membership already exists for identity %q in organization %q", e.IdentityID, e.OrganizationID) return fmt.Sprintf("user already exists for identity %q in organization %q", e.IdentityID, e.OrganizationID)
} }
type ErrSAMLConfigurationNotFound struct{ ConfigID gid.GID } type ErrSAMLConfigurationNotFound struct{ ConfigID gid.GID }

View File

@@ -212,33 +212,6 @@ func NewOrganizationService(svc *Service) *OrganizationService {
return &OrganizationService{Service: svc} 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( func (s *OrganizationService) UpdateMempership(
ctx context.Context, ctx context.Context,
organizationID gid.GID, organizationID gid.GID,
@@ -282,48 +255,53 @@ func (s *OrganizationService) UpdateMempership(
func (s *OrganizationService) RemoveMember( func (s *OrganizationService) RemoveMember(
ctx context.Context, ctx context.Context,
organizationID gid.GID, organizationID gid.GID,
membershipID gid.GID, profileID gid.GID,
) error { ) error {
scope := coredata.NewScopeFromObjectID(organizationID) scope := coredata.NewScopeFromObjectID(organizationID)
return s.pg.WithTx( return s.pg.WithTx(
ctx, ctx,
func(tx pg.Conn) error { 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 { 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) return fmt.Errorf("cannot load membership: %w", err)
} }
if membership.OrganizationID != organizationID { if membership.Role == coredata.MembershipRoleOwner && profile.State == coredata.ProfileStateActive {
return NewMembershipNotFoundError(membership.ID) profiles := coredata.MembershipProfiles{}
} count, err := profiles.CountActiveOwnerByOrganizationID(ctx, tx, scope, organizationID)
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 err != nil { if err != nil {
return fmt.Errorf("cannot count active owners: %w", err) return fmt.Errorf("cannot count active owners: %w", err)
} }
if count <= 1 { if count <= 1 {
return NewLastActiveOwnerError(membershipID) return NewLastActiveOwnerError(profileID)
} }
} }
err := membership.Delete(ctx, tx, scope, membershipID) if err := profile.Delete(ctx, tx, scope, profileID); err != nil {
if 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) return fmt.Errorf("cannot delete membership: %w", err)
} }
@@ -466,14 +444,14 @@ func (s *OrganizationService) InviteMember(
identityExists := identity.ID != gid.Nil identityExists := identity.ID != gid.Nil
if identityExists { if identityExists {
membership := &coredata.Membership{} profile := &coredata.MembershipProfile{}
err = membership.LoadByIdentityAndOrg(ctx, tx, scope, identity.ID, organizationID) err = profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, identity.ID, organizationID)
if err != nil && err != coredata.ErrResourceNotFound { 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 { if profile.ID != gid.Nil && profile.State == coredata.ProfileStateActive {
return NewMembershipAlreadyExistsError(identity.ID, organizationID) return NewUserAlreadyExistsError(identity.ID, organizationID)
} }
} }
@@ -549,13 +527,22 @@ func (s *OrganizationService) CreateOrganization(
UpdatedAt: now, 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{ membership = &coredata.Membership{
ID: gid.New(tenantID, coredata.MembershipEntityType), ID: gid.New(tenantID, coredata.MembershipEntityType),
IdentityID: identityID, IdentityID: identityID,
OrganizationID: organizationID, OrganizationID: organizationID,
Role: coredata.MembershipRoleOwner, Role: coredata.MembershipRoleOwner,
Source: coredata.MembershipSourceManual,
State: coredata.MembershipStateActive,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
@@ -688,25 +675,16 @@ func (s *OrganizationService) CreateOrganization(
organization.HorizontalLogoFileID = &horizontalLogoFile.ID 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) err = profile.Insert(ctx, tx)
if err != nil { if err != nil {
return fmt.Errorf("cannot insert profile: %w", err) 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 { if err := organizationContext.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert organization context: %w", err) return fmt.Errorf("cannot insert organization context: %w", err)
} }
@@ -1508,10 +1486,10 @@ func (s OrganizationService) DeleteSCIMConfiguration(
return scim.NewSCIMConfigurationNotFoundError(configID) return scim.NewSCIMConfigurationNotFoundError(configID)
} }
memberships := &coredata.Memberships{} profiles := &coredata.MembershipProfiles{}
err = memberships.ResetSCIMSources(ctx, tx, scope, config.OrganizationID) err = profiles.ResetSCIMSources(ctx, tx, scope, config.OrganizationID)
if err != nil { 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 // 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) 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 { func NewUserInactiveError(profileID gid.GID) error {
return &ErrMembershipInactive{MembershipID: membershipID} return &ErrUserInactive{ProfileID: profileID}
} }
func (e ErrMembershipInactive) Error() string { func (e ErrUserInactive) Error() string {
return fmt.Sprintf("membership %q is inactive", e.MembershipID) return fmt.Sprintf("user %q is inactive", e.ProfileID)
} }

View File

@@ -296,16 +296,23 @@ func (s *Service) HandleAssertion(
scope := coredata.NewScopeFromObjectID(config.OrganizationID) scope := coredata.NewScopeFromObjectID(config.OrganizationID)
err = profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, identity.ID, config.OrganizationID) if err := profile.LoadByIdentityIDAndOrganizationID(
if err != nil && err != coredata.ErrResourceNotFound { ctx,
return fmt.Errorf("cannot load profile: %w", err) 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{ profile = &coredata.MembershipProfile{
ID: gid.New(membership.ID.TenantID(), coredata.MembershipProfileEntityType), ID: gid.New(membership.ID.TenantID(), coredata.MembershipProfileEntityType),
IdentityID: identity.ID, IdentityID: identity.ID,
OrganizationID: config.OrganizationID, OrganizationID: config.OrganizationID,
Source: coredata.ProfileSourceSAML,
State: coredata.ProfileStateActive,
FullName: fullname, FullName: fullname,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
@@ -315,25 +322,28 @@ func (s *Service) HandleAssertion(
if err != nil { if err != nil {
return fmt.Errorf("cannot insert membership profile: %w", err) 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 := membership.LoadByIdentityAndOrg(
if err != nil && err != coredata.ErrResourceNotFound { ctx,
return fmt.Errorf("cannot load membership: %w", err) 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{ membership = &coredata.Membership{
ID: gid.New(config.ID.TenantID(), coredata.MembershipEntityType), ID: gid.New(config.ID.TenantID(), coredata.MembershipEntityType),
IdentityID: identity.ID, IdentityID: identity.ID,
OrganizationID: config.OrganizationID, OrganizationID: config.OrganizationID,
Role: coredata.MembershipRoleEmployee, Role: coredata.MembershipRoleEmployee,
Source: coredata.MembershipSourceSAML,
State: coredata.MembershipStateActive,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
@@ -358,34 +368,26 @@ func (s *Service) HandleAssertion(
} }
} }
if membership.Source != coredata.MembershipSourceSCIM { if profile.Source != coredata.ProfileSourceSCIM {
needsUpdate := false 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 { if role != nil {
membership.Role = *role membership.Role = *role
membership.UpdatedAt = now 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) 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)
} }
} }
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 return nil

View File

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

View File

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

View File

@@ -324,6 +324,7 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
now = time.Now() now = time.Now()
rootSession = &coredata.Session{} rootSession = &coredata.Session{}
identity = &coredata.Identity{} identity = &coredata.Identity{}
profile = &coredata.MembershipProfile{}
membership = &coredata.Membership{} membership = &coredata.Membership{}
childSession = &coredata.Session{} childSession = &coredata.Session{}
scope = coredata.NewScopeFromObjectID(organizationID) scope = coredata.NewScopeFromObjectID(organizationID)
@@ -353,6 +354,18 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
return fmt.Errorf("cannot load identity: %w", err) 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) err = membership.LoadByIdentityInOrganization(ctx, tx, rootSession.IdentityID, organizationID)
if err != nil { if err != nil {
if err == coredata.ErrResourceNotFound { if err == coredata.ErrResourceNotFound {
@@ -361,10 +374,6 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
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),
@@ -417,6 +426,7 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
now = time.Now() now = time.Now()
rootSession = &coredata.Session{} rootSession = &coredata.Session{}
identity = &coredata.Identity{} identity = &coredata.Identity{}
profile = &coredata.MembershipProfile{}
membership = &coredata.Membership{} membership = &coredata.Membership{}
childSession = &coredata.Session{} childSession = &coredata.Session{}
scope = coredata.NewScopeFromObjectID(organizationID) scope = coredata.NewScopeFromObjectID(organizationID)
@@ -446,6 +456,18 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
return fmt.Errorf("cannot load identity: %w", err) 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) err = membership.LoadByIdentityInOrganization(ctx, tx, rootSession.IdentityID, organizationID)
if err != nil { if err != nil {
if err == coredata.ErrResourceNotFound { if err == coredata.ErrResourceNotFound {
@@ -454,10 +476,6 @@ 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),
@@ -497,6 +515,7 @@ func (s SessionService) AssumeOrganizationSession(
now = time.Now() now = time.Now()
rootSession = &coredata.Session{} rootSession = &coredata.Session{}
identity = &coredata.Identity{} identity = &coredata.Identity{}
profile = &coredata.MembershipProfile{}
membership = &coredata.Membership{} membership = &coredata.Membership{}
childSession = &coredata.Session{} childSession = &coredata.Session{}
scope = coredata.NewScopeFromObjectID(organizationID) scope = coredata.NewScopeFromObjectID(organizationID)
@@ -524,6 +543,17 @@ func (s SessionService) AssumeOrganizationSession(
return fmt.Errorf("cannot load identity: %w", err) 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 := membership.LoadByIdentityInOrganization(ctx, tx, rootSession.IdentityID, organizationID); err != nil {
if err == coredata.ErrResourceNotFound { if err == coredata.ErrResourceNotFound {
return NewMembershipNotFoundError(organizationID) return NewMembershipNotFoundError(organizationID)
@@ -531,10 +561,6 @@ 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,

View File

@@ -181,6 +181,8 @@ type Identity implements Node {
type Profile implements Node { type Profile implements Node {
id: ID! id: ID!
fullName: String! fullName: String!
source: String!
state: ProfileState!
additionalEmailAddresses: [EmailAddr!]! additionalEmailAddresses: [EmailAddr!]!
kind: ProfileKind! kind: ProfileKind!
position: String position: String
@@ -198,6 +200,32 @@ type Profile implements Node {
@session(required: PRESENT) @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 { type Organization implements Node {
id: ID! id: ID!
name: String! name: String!
@@ -243,17 +271,6 @@ type Organization implements Node {
@session(required: PRESENT) @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 enum MembershipRole
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") { @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") {
OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner") 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") @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 { type Membership implements Node {
id: ID! id: ID!
createdAt: Datetime! createdAt: Datetime!
role: MembershipRole! role: MembershipRole!
source: MembershipSource!
state: MembershipState!
lastSession: Session @goField(forceResolver: true) lastSession: Session @goField(forceResolver: true)
@@ -440,7 +440,7 @@ type SCIMEvent implements Node {
requestBody: String requestBody: String
responseBody: String responseBody: String
errorMessage: String errorMessage: String
profile: Profile @goField(forceResolver: true) userName: String!
ipAddress: String! ipAddress: String!
createdAt: Datetime! createdAt: Datetime!

View File

@@ -192,8 +192,6 @@ type ComplexityRoot struct {
LastSession func(childComplexity int) int LastSession func(childComplexity int) int
Permission func(childComplexity int, action string) int Permission func(childComplexity int, action string) int
Role func(childComplexity int) int Role func(childComplexity int) int
Source func(childComplexity int) int
State func(childComplexity int) int
} }
Mutation struct { Mutation struct {
@@ -299,6 +297,8 @@ type ComplexityRoot struct {
Organization func(childComplexity int) int Organization func(childComplexity int) int
Permission func(childComplexity int, action string) int Permission func(childComplexity int, action string) int
Position func(childComplexity int) int Position func(childComplexity int) int
Source func(childComplexity int) int
State func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
} }
@@ -414,10 +414,10 @@ type ComplexityRoot struct {
Method func(childComplexity int) int Method func(childComplexity int) int
Path func(childComplexity int) int Path func(childComplexity int) int
Permission func(childComplexity int, action string) int Permission func(childComplexity int, action string) int
Profile func(childComplexity int) int
RequestBody func(childComplexity int) int RequestBody func(childComplexity int) int
ResponseBody func(childComplexity int) int ResponseBody func(childComplexity int) int
StatusCode func(childComplexity int) int StatusCode func(childComplexity int) int
UserName func(childComplexity int) int
} }
SCIMEventConnection struct { SCIMEventConnection struct {
@@ -605,8 +605,6 @@ type SCIMConfigurationResolver interface {
Permission(ctx context.Context, obj *types.SCIMConfiguration, action string) (bool, error) Permission(ctx context.Context, obj *types.SCIMConfiguration, action string) (bool, error)
} }
type SCIMEventResolver interface { type SCIMEventResolver interface {
Profile(ctx context.Context, obj *types.SCIMEvent) (*types.Profile, error)
Permission(ctx context.Context, obj *types.SCIMEvent, action string) (bool, error) Permission(ctx context.Context, obj *types.SCIMEvent, action string) (bool, error)
} }
type SCIMEventConnectionResolver interface { type SCIMEventConnectionResolver interface {
@@ -1042,18 +1040,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
} }
return e.complexity.Membership.Role(childComplexity), true 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": case "Mutation.acceptInvitation":
if e.complexity.Mutation.AcceptInvitation == nil { 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 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": case "Profile.updatedAt":
if e.complexity.Profile.UpdatedAt == nil { if e.complexity.Profile.UpdatedAt == nil {
break 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 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": case "SCIMEvent.requestBody":
if e.complexity.SCIMEvent.RequestBody == nil { if e.complexity.SCIMEvent.RequestBody == nil {
break break
@@ -2166,6 +2158,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
} }
return e.complexity.SCIMEvent.StatusCode(childComplexity), true 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": case "SCIMEventConnection.edges":
if e.complexity.SCIMEventConnection.Edges == nil { if e.complexity.SCIMEventConnection.Edges == nil {
@@ -2683,6 +2681,8 @@ type Identity implements Node {
type Profile implements Node { type Profile implements Node {
id: ID! id: ID!
fullName: String! fullName: String!
source: String!
state: ProfileState!
additionalEmailAddresses: [EmailAddr!]! additionalEmailAddresses: [EmailAddr!]!
kind: ProfileKind! kind: ProfileKind!
position: String position: String
@@ -2700,6 +2700,32 @@ type Profile implements Node {
@session(required: PRESENT) @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 { type Organization implements Node {
id: ID! id: ID!
name: String! name: String!
@@ -2745,17 +2771,6 @@ type Organization implements Node {
@session(required: PRESENT) @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 enum MembershipRole
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") { @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") {
OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner") 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") @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 { type Membership implements Node {
id: ID! id: ID!
createdAt: Datetime! createdAt: Datetime!
role: MembershipRole! role: MembershipRole!
source: MembershipSource!
state: MembershipState!
lastSession: Session @goField(forceResolver: true) lastSession: Session @goField(forceResolver: true)
@@ -2942,7 +2940,7 @@ type SCIMEvent implements Node {
requestBody: String requestBody: String
responseBody: String responseBody: String
errorMessage: String errorMessage: String
profile: Profile @goField(forceResolver: true) userName: String!
ipAddress: String! ipAddress: String!
createdAt: Datetime! createdAt: Datetime!
@@ -4335,10 +4333,6 @@ func (ec *executionContext) fieldContext_AcceptInvitationPayload_membership(_ co
return ec.fieldContext_Membership_createdAt(ctx, field) return ec.fieldContext_Membership_createdAt(ctx, field)
case "role": case "role":
return ec.fieldContext_Membership_role(ctx, field) 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": case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field) return ec.fieldContext_Membership_lastSession(ctx, field)
case "permission": case "permission":
@@ -4756,10 +4750,6 @@ func (ec *executionContext) fieldContext_CreateOrganizationPayload_membership(_
return ec.fieldContext_Membership_createdAt(ctx, field) return ec.fieldContext_Membership_createdAt(ctx, field)
case "role": case "role":
return ec.fieldContext_Membership_role(ctx, field) 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": case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field) return ec.fieldContext_Membership_lastSession(ctx, field)
case "permission": case "permission":
@@ -6338,64 +6328,6 @@ func (ec *executionContext) fieldContext_Membership_role(_ context.Context, fiel
return fc, nil 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) { 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,
@@ -8921,6 +8853,10 @@ func (ec *executionContext) fieldContext_Organization_viewer(_ context.Context,
return ec.fieldContext_Profile_id(ctx, field) return ec.fieldContext_Profile_id(ctx, field)
case "fullName": case "fullName":
return ec.fieldContext_Profile_fullName(ctx, field) 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": case "additionalEmailAddresses":
return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field) return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field)
case "kind": case "kind":
@@ -9086,10 +9022,6 @@ func (ec *executionContext) fieldContext_OrganizationSessionCreated_membership(_
return ec.fieldContext_Membership_createdAt(ctx, field) return ec.fieldContext_Membership_createdAt(ctx, field)
case "role": case "role":
return ec.fieldContext_Membership_role(ctx, field) 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": case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field) return ec.fieldContext_Membership_lastSession(ctx, field)
case "permission": case "permission":
@@ -9714,6 +9646,64 @@ func (ec *executionContext) fieldContext_Profile_fullName(_ context.Context, fie
return fc, nil 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) { func (ec *executionContext) _Profile_additionalEmailAddresses(ctx context.Context, field graphql.CollectedField, obj *types.Profile) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -10063,10 +10053,6 @@ func (ec *executionContext) fieldContext_Profile_membership(_ context.Context, f
return ec.fieldContext_Membership_createdAt(ctx, field) return ec.fieldContext_Membership_createdAt(ctx, field)
case "role": case "role":
return ec.fieldContext_Membership_role(ctx, field) 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": case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field) return ec.fieldContext_Membership_lastSession(ctx, field)
case "permission": case "permission":
@@ -10297,6 +10283,10 @@ func (ec *executionContext) fieldContext_ProfileEdge_node(_ context.Context, fie
return ec.fieldContext_Profile_id(ctx, field) return ec.fieldContext_Profile_id(ctx, field)
case "fullName": case "fullName":
return ec.fieldContext_Profile_fullName(ctx, field) 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": case "additionalEmailAddresses":
return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field) return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field)
case "kind": case "kind":
@@ -12488,58 +12478,30 @@ func (ec *executionContext) fieldContext_SCIMEvent_errorMessage(_ context.Contex
return fc, nil 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( return graphql.ResolveField(
ctx, ctx,
ec.OperationContext, ec.OperationContext,
field, field,
ec.fieldContext_SCIMEvent_profile, ec.fieldContext_SCIMEvent_userName,
func(ctx context.Context) (any, error) { func(ctx context.Context) (any, error) {
return ec.resolvers.SCIMEvent().Profile(ctx, obj) return obj.UserName, nil
}, },
nil, nil,
ec.marshalOProfile2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐProfile, ec.marshalNString2string,
true,
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{ fc = &graphql.FieldContext{
Object: "SCIMEvent", Object: "SCIMEvent",
Field: field, Field: field,
IsMethod: true, IsMethod: false,
IsResolver: true, IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name { return nil, errors.New("field of type String does not have child fields")
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 fc, nil return fc, nil
@@ -12803,8 +12765,8 @@ func (ec *executionContext) fieldContext_SCIMEventEdge_node(_ context.Context, f
return ec.fieldContext_SCIMEvent_responseBody(ctx, field) return ec.fieldContext_SCIMEvent_responseBody(ctx, field)
case "errorMessage": case "errorMessage":
return ec.fieldContext_SCIMEvent_errorMessage(ctx, field) return ec.fieldContext_SCIMEvent_errorMessage(ctx, field)
case "profile": case "userName":
return ec.fieldContext_SCIMEvent_profile(ctx, field) return ec.fieldContext_SCIMEvent_userName(ctx, field)
case "ipAddress": case "ipAddress":
return ec.fieldContext_SCIMEvent_ipAddress(ctx, field) return ec.fieldContext_SCIMEvent_ipAddress(ctx, field)
case "createdAt": case "createdAt":
@@ -13585,10 +13547,6 @@ func (ec *executionContext) fieldContext_UpdateMembershipPayload_membership(_ co
return ec.fieldContext_Membership_createdAt(ctx, field) return ec.fieldContext_Membership_createdAt(ctx, field)
case "role": case "role":
return ec.fieldContext_Membership_role(ctx, field) 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": case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field) return ec.fieldContext_Membership_lastSession(ctx, field)
case "permission": case "permission":
@@ -13691,6 +13649,10 @@ func (ec *executionContext) fieldContext_UpdateProfilePayload_profile(_ context.
return ec.fieldContext_Profile_id(ctx, field) return ec.fieldContext_Profile_id(ctx, field)
case "fullName": case "fullName":
return ec.fieldContext_Profile_fullName(ctx, field) 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": case "additionalEmailAddresses":
return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field) return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field)
case "kind": case "kind":
@@ -18019,16 +17981,6 @@ 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 "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": case "lastSession":
field := field field := field
@@ -19023,6 +18975,16 @@ func (ec *executionContext) _Profile(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1) 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": case "additionalEmailAddresses":
out.Values[i] = ec._Profile_additionalEmailAddresses(ctx, field, obj) out.Values[i] = ec._Profile_additionalEmailAddresses(ctx, field, obj)
if out.Values[i] == graphql.Null { 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) out.Values[i] = ec._SCIMEvent_responseBody(ctx, field, obj)
case "errorMessage": case "errorMessage":
out.Values[i] = ec._SCIMEvent_errorMessage(ctx, field, obj) out.Values[i] = ec._SCIMEvent_errorMessage(ctx, field, obj)
case "profile": case "userName":
field := field out.Values[i] = ec._SCIMEvent_userName(ctx, field, obj)
if out.Values[i] == graphql.Null {
innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { atomic.AddUint32(&out.Invalids, 1)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._SCIMEvent_profile(ctx, field, obj)
return res
} }
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": case "ipAddress":
out.Values[i] = ec._SCIMEvent_ipAddress(ctx, field, obj) out.Values[i] = ec._SCIMEvent_ipAddress(ctx, field, obj)
if out.Values[i] == graphql.Null { 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) { 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]
@@ -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) { 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 var res types.ReauthenticationReason
err := res.UnmarshalGQL(v) err := res.UnmarshalGQL(v)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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