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