Implement resolvers

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-13 11:54:14 +04:00
parent c115e2c9dc
commit baeb758a4f
16 changed files with 507 additions and 259 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<b545f1a231e042bd94c84430f3015112>> * @generated SignedSource<<f6caf8d14eaaea74987cd5f91fedaed3>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -15,11 +15,6 @@ export type MembershipCardFragment$data = {
readonly expiresAt: string; readonly expiresAt: string;
readonly id: string; readonly id: string;
} | null | undefined; } | null | undefined;
readonly organization: {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
};
readonly " $fragmentType": "MembershipCardFragment"; readonly " $fragmentType": "MembershipCardFragment";
}; };
export type MembershipCardFragment$key = { export type MembershipCardFragment$key = {
@@ -27,15 +22,7 @@ export type MembershipCardFragment$key = {
readonly " $fragmentSpreads": FragmentRefs<"MembershipCardFragment">; readonly " $fragmentSpreads": FragmentRefs<"MembershipCardFragment">;
}; };
const node: ReaderFragment = (function(){ const node: ReaderFragment = {
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [], "argumentDefinitions": [],
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
@@ -49,7 +36,13 @@ return {
"name": "lastSession", "name": "lastSession",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -59,43 +52,12 @@ return {
} }
], ],
"storageKey": null "storageKey": null
},
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
} }
], ],
"type": "Membership", "type": "Membership",
"abstractKey": null "abstractKey": null
}; };
})();
(node as any).hash = "5a2532aed755a94df49c2f9ce0929a1f"; (node as any).hash = "3da5f340850d352daaa553ca7b78aa37";
export default node; export default node;

View File

@@ -0,0 +1,58 @@
/**
* @generated SignedSource<<b3dda685c5f3688ead238ddd46d12dd3>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MembershipCard_organizationFragment$data = {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
readonly " $fragmentType": "MembershipCard_organizationFragment";
};
export type MembershipCard_organizationFragment$key = {
readonly " $data"?: MembershipCard_organizationFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"MembershipCard_organizationFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "MembershipCard_organizationFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
};
(node as any).hash = "a02d28730634e510e7e0803d5d1a366f";
export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<e907a3f8c0610d5deed37b1a47500377>> * @generated SignedSource<<fe0de6febc77244c92f8d2413bbb0ca2>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -16,11 +16,6 @@ export type MembershipsDropdownMenuItemFragment$data = {
readonly expiresAt: string; readonly expiresAt: string;
readonly id: string; readonly id: string;
} | null | undefined; } | null | undefined;
readonly organization: {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
};
readonly " $fragmentType": "MembershipsDropdownMenuItemFragment"; readonly " $fragmentType": "MembershipsDropdownMenuItemFragment";
}; };
export type MembershipsDropdownMenuItemFragment$key = { export type MembershipsDropdownMenuItemFragment$key = {
@@ -61,36 +56,6 @@ return {
} }
], ],
"storageKey": null "storageKey": null
},
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
} }
], ],
"type": "Membership", "type": "Membership",
@@ -98,6 +63,6 @@ return {
}; };
})(); })();
(node as any).hash = "915a6fb0071b4890010401a56f3affa6"; (node as any).hash = "4f40e957597476f8e6179f6df68d386e";
export default node; export default node;

View File

@@ -0,0 +1,58 @@
/**
* @generated SignedSource<<a6754adeb9fc43ac3d85ed2e9c2c8aad>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MembershipsDropdownMenuItem_organizationFragment$data = {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
readonly " $fragmentType": "MembershipsDropdownMenuItem_organizationFragment";
};
export type MembershipsDropdownMenuItem_organizationFragment$key = {
readonly " $data"?: MembershipsDropdownMenuItem_organizationFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"MembershipsDropdownMenuItem_organizationFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "MembershipsDropdownMenuItem_organizationFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
};
(node as any).hash = "799a0d538f5d667112f4c9ba8c0d3623";
export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<e639ada9f797274b710cc71198ec4089>> * @generated SignedSource<<d7b957d5b207e241e695d6592091dc2d>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -29,7 +29,7 @@ export type MembershipsPageQuery$data = {
readonly id: string; readonly id: string;
readonly membership: { readonly membership: {
readonly " $fragmentSpreads": FragmentRefs<"MembershipCardFragment">; readonly " $fragmentSpreads": FragmentRefs<"MembershipCardFragment">;
} | null | undefined; };
readonly organization: { readonly organization: {
readonly name: string; readonly name: string;
readonly " $fragmentSpreads": FragmentRefs<"MembershipCard_organizationFragment">; readonly " $fragmentSpreads": FragmentRefs<"MembershipCard_organizationFragment">;
@@ -191,6 +191,8 @@ return {
"selections": [ "selections": [
(v1/*: any*/), (v1/*: any*/),
{ {
"kind": "RequiredField",
"field": {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "Membership", "concreteType": "Membership",
@@ -206,6 +208,8 @@ return {
], ],
"storageKey": null "storageKey": null
}, },
"action": "THROW"
},
{ {
"kind": "RequiredField", "kind": "RequiredField",
"field": { "field": {
@@ -530,6 +534,6 @@ return {
}; };
})(); })();
(node as any).hash = "05ac7c8fa236be3a01eeb28fc14ceb9d"; (node as any).hash = "84f5ab240bec94caf75e9ae163117e3d";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<ff8dc842880d57321b274bff2ee91007>> * @generated SignedSource<<2d667e47f47ffe754960a9336fc4b8dc>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -12,7 +12,7 @@ import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type PeopleListFragment$data = { export type PeopleListFragment$data = {
readonly id: string; readonly id: string;
readonly members: { readonly profiles: {
readonly __id: string; readonly __id: string;
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
@@ -33,7 +33,7 @@ import PeopleListFragment_RefetchQuery_graphql from './PeopleListFragment_Refetc
const node: ReaderFragment = (function(){ const node: ReaderFragment = (function(){
var v0 = [ var v0 = [
"members" "profiles"
], ],
v1 = { v1 = {
"alias": null, "alias": null,
@@ -110,7 +110,7 @@ return {
{ {
"kind": "RequiredField", "kind": "RequiredField",
"field": { "field": {
"alias": "members", "alias": "profiles",
"args": [ "args": [
{ {
"kind": "Variable", "kind": "Variable",
@@ -118,9 +118,9 @@ return {
"variableName": "order" "variableName": "order"
} }
], ],
"concreteType": "MembershipConnection", "concreteType": "ProfileConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "__PeopleListFragment_members_connection", "name": "__PeopleListFragment_profiles_connection",
"plural": false, "plural": false,
"selections": [ "selections": [
{ {
@@ -135,7 +135,7 @@ return {
"field": { "field": {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "MembershipEdge", "concreteType": "ProfileEdge",
"kind": "LinkedField", "kind": "LinkedField",
"name": "edges", "name": "edges",
"plural": true, "plural": true,
@@ -143,7 +143,7 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "Membership", "concreteType": "Profile",
"kind": "LinkedField", "kind": "LinkedField",
"name": "node", "name": "node",
"plural": false, "plural": false,
@@ -239,6 +239,6 @@ return {
}; };
})(); })();
(node as any).hash = "c62c77bc2ef0e36b7eaf4553cc721e0c"; (node as any).hash = "ee3aeca404063a0660eeb9ff27592e86";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<6739397aaeb3d7890fd697ab4323736a>> * @generated SignedSource<<b56389d57c3d4cb31dd873feaeff2bb3>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -18,19 +18,20 @@ export type PeopleListItemFragment$data = {
readonly canDelete: boolean; readonly canDelete: boolean;
readonly canUpdate: boolean; readonly canUpdate: boolean;
readonly createdAt: string; readonly createdAt: string;
readonly fullName: string;
readonly id: string; readonly id: string;
readonly identity: { readonly identity: {
readonly email: string; readonly email: string;
}; };
readonly profile: {
readonly fullName: string;
readonly id: string;
readonly kind: ProfileKind; readonly kind: ProfileKind;
readonly position: string | null | undefined; readonly membership: {
}; readonly canUpdate: boolean;
readonly id: string;
readonly role: MembershipRole; readonly role: MembershipRole;
readonly source: MembershipSource; readonly source: MembershipSource;
readonly state: MembershipState; readonly state: MembershipState;
};
readonly position: string | null | undefined;
readonly " $fragmentType": "PeopleListItemFragment"; readonly " $fragmentType": "PeopleListItemFragment";
}; };
export type PeopleListItemFragment$key = { export type PeopleListItemFragment$key = {
@@ -51,6 +52,38 @@ return {
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "PeopleListItemFragment", "name": "PeopleListItemFragment",
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "kind",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "position",
"storageKey": null
},
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "membership",
"plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v0/*: any*/),
{ {
@@ -75,36 +108,17 @@ return {
"storageKey": null "storageKey": null
}, },
{ {
"kind": "RequiredField", "alias": "canUpdate",
"field": { "args": [
"alias": null,
"args": null,
"concreteType": "MembershipProfile",
"kind": "LinkedField",
"name": "profile",
"plural": false,
"selections": [
(v0/*: any*/),
{ {
"alias": null, "kind": "Literal",
"args": null, "name": "action",
"value": "iam:membership:update"
}
],
"kind": "ScalarField", "kind": "ScalarField",
"name": "fullName", "name": "permission",
"storageKey": null "storageKey": "permission(action:\"iam:membership:update\")"
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "kind",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "position",
"storageKey": null
} }
], ],
"storageKey": null "storageKey": null
@@ -146,12 +160,12 @@ return {
{ {
"kind": "Literal", "kind": "Literal",
"name": "action", "name": "action",
"value": "iam:membership:update" "value": "iam:membership-profile:update"
} }
], ],
"kind": "ScalarField", "kind": "ScalarField",
"name": "permission", "name": "permission",
"storageKey": "permission(action:\"iam:membership:update\")" "storageKey": "permission(action:\"iam:membership-profile:update\")"
}, },
{ {
"alias": "canDelete", "alias": "canDelete",
@@ -159,19 +173,19 @@ return {
{ {
"kind": "Literal", "kind": "Literal",
"name": "action", "name": "action",
"value": "iam:membership:delete" "value": "iam:membership-profile:delete"
} }
], ],
"kind": "ScalarField", "kind": "ScalarField",
"name": "permission", "name": "permission",
"storageKey": "permission(action:\"iam:membership:delete\")" "storageKey": "permission(action:\"iam:membership-profile:delete\")"
} }
], ],
"type": "Membership", "type": "Profile",
"abstractKey": null "abstractKey": null
}; };
})(); })();
(node as any).hash = "cf1683d15b13975d8f739c3a7aa38d7e"; (node as any).hash = "5f45ee7c58d50b0a6569c3bd22ffc83f";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<f4e0f213a6bd3df7841650d2f57fd254>> * @generated SignedSource<<53070e63046901877444d5de41cc8624>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -22,7 +22,7 @@ export type PeoplePageQuery$data = {
readonly totalCount: number | null | undefined; readonly totalCount: number | null | undefined;
}; };
readonly profiles: { readonly profiles: {
readonly totalCount: number; readonly totalCount: number | null | undefined;
}; };
readonly " $fragmentSpreads": FragmentRefs<"InvitationListFragment" | "PeopleListFragment">; readonly " $fragmentSpreads": FragmentRefs<"InvitationListFragment" | "PeopleListFragment">;
} | { } | {

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<1deba62d68487df7458534fef4bed9ff>> * @generated SignedSource<<6ee89b2ab0c55ac9498bf9186e78ee90>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -96,10 +96,10 @@ const node: ReaderFragment = {
"storageKey": "permission(action:\"iam:membership-profile:update\")" "storageKey": "permission(action:\"iam:membership-profile:update\")"
} }
], ],
"type": "MembershipProfile", "type": "Profile",
"abstractKey": null "abstractKey": null
}; };
(node as any).hash = "ba9398e81e237b983ce37f05436439ee"; (node as any).hash = "9ecc41c74271a3202bbde08b63c92b73";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<69f50c950d76ce3e882f94f29333caad>> * @generated SignedSource<<6bc4cdb83f5825b5d606fe579fa8f7a8>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -13,16 +13,12 @@ import { FragmentRefs } from "relay-runtime";
export type SCIMEventListItemFragment$data = { export type SCIMEventListItemFragment$data = {
readonly createdAt: string; readonly createdAt: string;
readonly errorMessage: string | null | undefined; readonly errorMessage: string | null | undefined;
readonly id: string;
readonly ipAddress: string; readonly ipAddress: string;
readonly membership: { readonly method: string;
readonly id: string; readonly path: string;
readonly profile: { readonly profile: {
readonly fullName: string; readonly fullName: string;
} | null | undefined; } | null | undefined;
} | null | undefined;
readonly method: string;
readonly path: string;
readonly statusCode: number; readonly statusCode: number;
readonly " $fragmentType": "SCIMEventListItemFragment"; readonly " $fragmentType": "SCIMEventListItemFragment";
}; };
@@ -31,21 +27,12 @@ export type SCIMEventListItemFragment$key = {
readonly " $fragmentSpreads": FragmentRefs<"SCIMEventListItemFragment">; readonly " $fragmentSpreads": FragmentRefs<"SCIMEventListItemFragment">;
}; };
const node: ReaderFragment = (function(){ const node: ReaderFragment = {
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [], "argumentDefinitions": [],
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "SCIMEventListItemFragment", "name": "SCIMEventListItemFragment",
"selections": [ "selections": [
(v0/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -91,16 +78,7 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "Membership", "concreteType": "Profile",
"kind": "LinkedField",
"name": "membership",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "MembershipProfile",
"kind": "LinkedField", "kind": "LinkedField",
"name": "profile", "name": "profile",
"plural": false, "plural": false,
@@ -116,14 +94,10 @@ return {
"storageKey": null "storageKey": null
} }
], ],
"storageKey": null
}
],
"type": "SCIMEvent", "type": "SCIMEvent",
"abstractKey": null "abstractKey": null
}; };
})();
(node as any).hash = "0c9c2bc445531d874b8bceae95f227b8"; (node as any).hash = "b38af9ac9b660d3aab75e00e2c55dfb6";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<a87fd05a4f80d2d7094647468d695967>> * @generated SignedSource<<d0e62302bc1d00dffb0c61f56777472b>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -11,14 +11,12 @@
import { ReaderFragment } from 'relay-runtime'; import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type ViewerMembershipDropdownFragment$data = { export type ViewerMembershipDropdownFragment$data = {
readonly viewerMembership: { readonly viewer: {
readonly fullName: string;
readonly identity: { readonly identity: {
readonly canListAPIKeys: boolean; readonly canListAPIKeys: boolean;
readonly email: string; readonly email: string;
}; };
readonly profile: {
readonly fullName: string;
};
}; };
readonly " $fragmentType": "ViewerMembershipDropdownFragment"; readonly " $fragmentType": "ViewerMembershipDropdownFragment";
}; };
@@ -38,11 +36,18 @@ const node: ReaderFragment = {
"field": { "field": {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "Membership", "concreteType": "Profile",
"kind": "LinkedField", "kind": "LinkedField",
"name": "viewerMembership", "name": "viewer",
"plural": false, "plural": false,
"selections": [ "selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{ {
"kind": "RequiredField", "kind": "RequiredField",
"field": { "field": {
@@ -77,28 +82,6 @@ const node: ReaderFragment = {
"storageKey": null "storageKey": null
}, },
"action": "THROW" "action": "THROW"
},
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "MembershipProfile",
"kind": "LinkedField",
"name": "profile",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
} }
], ],
"storageKey": null "storageKey": null
@@ -110,6 +93,6 @@ const node: ReaderFragment = {
"abstractKey": null "abstractKey": null
}; };
(node as any).hash = "82c179a1ec2b90ce5f12c0959f97150f"; (node as any).hash = "fd5df361935e3e2bf4eaac1b2b4c5528";
export default node; export default node;

View File

@@ -708,6 +708,42 @@ INNER JOIN signatories ON p.id = signatories.signed_by_profile_id
return nil return nil
} }
func (p *MembershipProfiles) CountByIdentityID(
ctx context.Context,
conn pg.Conn,
identityID gid.GID,
filter *MembershipProfileFilter,
) (int, error) {
q := `
SELECT
COUNT(*)
FROM
iam_membership_profiles
WHERE
%s
AND %s
AND identity_id = @identity_id
`
q = fmt.Sprintf(q, filter.SQLFragment())
args := pgx.StrictNamedArgs{"identity_id": identityID}
maps.Copy(args, filter.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return 0, fmt.Errorf("cannot query iam_membership_profiles: %w", err)
}
var count int
err = rows.Scan(&count)
if err != nil {
return 0, fmt.Errorf("cannot collect count: %w", err)
}
return count, nil
}
func (p *MembershipProfiles) CountByOrganizationID( func (p *MembershipProfiles) CountByOrganizationID(
ctx context.Context, ctx context.Context,
conn pg.Conn, conn pg.Conn,

View File

@@ -843,3 +843,56 @@ func (s AccountService) CountSAMLConfigurationsForEmail(
return count, nil return count, nil
} }
func (s *AccountService) ListProfilesForIdentity(
ctx context.Context,
identityID gid.GID,
cursor *page.Cursor[coredata.MembershipProfileOrderField],
filter *coredata.MembershipProfileFilter,
) (*page.Page[*coredata.MembershipProfile, coredata.MembershipProfileOrderField], error) {
var (
profiles = coredata.MembershipProfiles{}
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := profiles.LoadByIdentityID(ctx, conn, coredata.NewNoScope(), identityID, cursor, filter); err != nil {
return fmt.Errorf("cannot load profiles: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(profiles, cursor), nil
}
func (s AccountService) CountProfiles(
ctx context.Context,
identityID gid.GID,
filter *coredata.MembershipProfileFilter,
) (int, error) {
var (
count int
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) (err error) {
profiles := coredata.MembershipProfiles{}
count, err = profiles.CountByIdentityID(ctx, conn, identityID, filter)
if err != nil {
return fmt.Errorf("cannot count profiles: %w", err)
}
return nil
},
)
return count, err
}

View File

@@ -90,7 +90,10 @@ var IAMSelfManageProfilePolicy = policy.NewPolicy(
"Self-Manage Profiles", "Self-Manage Profiles",
// Users can view their own profiles // Users can view their own profiles
policy.Allow(ActionMembershipProfileGet). policy.Allow(
ActionMembershipProfileGet,
ActionMembershipProfileList,
).
WithSID("view-own-profiles"). WithSID("view-own-profiles").
When(policy.Equals("principal.id", "resource.identity_id")), When(policy.Equals("principal.id", "resource.identity_id")),
). ).

View File

@@ -1002,6 +1002,41 @@ func (s *OrganizationService) GetProfile(ctx context.Context, profileID gid.GID)
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {
if err := profile.LoadByID(ctx, conn, coredata.NewScopeFromObjectID(profileID), profileID); err != nil { if err := profile.LoadByID(ctx, conn, coredata.NewScopeFromObjectID(profileID), profileID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return NewProfileNotFoundError(profileID)
}
return fmt.Errorf("cannot load profile: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return profile, nil
}
func (s *OrganizationService) GetProfileForIdentityAndOrganization(ctx context.Context, identityID gid.GID, organizationID gid.GID) (*coredata.MembershipProfile, error) {
profile := &coredata.MembershipProfile{}
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := profile.LoadByIdentityIDAndOrganizationID(
ctx,
conn,
coredata.NewScopeFromObjectID(organizationID),
identityID,
organizationID,
); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return NewProfileNotFoundError(gid.Nil)
}
return fmt.Errorf("cannot load profile: %w", err) return fmt.Errorf("cannot load profile: %w", err)
} }

View File

@@ -23,7 +23,6 @@ import (
"go.probo.inc/probo/pkg/server/api/authz" "go.probo.inc/probo/pkg/server/api/authz"
"go.probo.inc/probo/pkg/server/api/connect/v1/schema" "go.probo.inc/probo/pkg/server/api/connect/v1/schema"
"go.probo.inc/probo/pkg/server/api/connect/v1/types" "go.probo.inc/probo/pkg/server/api/connect/v1/types"
types1 "go.probo.inc/probo/pkg/server/api/console/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils" "go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/server/gqlutils/types/cursor" "go.probo.inc/probo/pkg/server/gqlutils/types/cursor"
) )
@@ -34,8 +33,40 @@ func (r *connectorResolver) Permission(ctx context.Context, obj *types.Connector
} }
// Profiles is the resolver for the profiles field. // Profiles is the resolver for the profiles field.
func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types1.ProfileOrderBy) (*types1.ProfileConnection, error) { func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy) (*types.ProfileConnection, error) {
panic(fmt.Errorf("not implemented: Profiles - profiles")) if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileList, authz.WithSkipAssumptionCheck()); err != nil {
return nil, err
}
if gqlutils.OnlyTotalCountSelected(ctx) {
return &types.ProfileConnection{
Resolver: r,
ParentID: obj.ID,
}, nil
}
filter := coredata.NewMembershipProfileFilter(nil)
pageOrderBy := page.OrderBy[coredata.MembershipProfileOrderField]{
Field: coredata.MembershipProfileOrderFieldFullName,
Direction: page.OrderDirectionAsc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.MembershipProfileOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.iam.AccountService.ListProfilesForIdentity(ctx, obj.ID, cursor, filter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list profiles", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewProfileConnection(page, r, obj.ID), nil
} }
// PendingInvitations is the resolver for the pendingInvitations field. // PendingInvitations is the resolver for the pendingInvitations field.
@@ -1211,8 +1242,40 @@ func (r *organizationResolver) HorizontalLogoURL(ctx context.Context, obj *types
} }
// Profiles is the resolver for the profiles field. // Profiles is the resolver for the profiles field.
func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types1.ProfileOrderBy) (*types1.ProfileConnection, error) { func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy) (*types.ProfileConnection, error) {
panic(fmt.Errorf("not implemented: Profiles - profiles")) if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileList); err != nil {
return nil, err
}
if gqlutils.OnlyTotalCountSelected(ctx) {
return &types.ProfileConnection{
Resolver: r,
ParentID: obj.ID,
}, nil
}
filter := coredata.NewMembershipProfileFilter(nil)
pageOrderBy := page.OrderBy[coredata.MembershipProfileOrderField]{
Field: coredata.MembershipProfileOrderFieldFullName,
Direction: page.OrderDirectionAsc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.MembershipProfileOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.iam.OrganizationService.ListProfiles(ctx, obj.ID, cursor, filter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list profiles", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewProfileConnection(page, r, obj.ID), nil
} }
// Invitations is the resolver for the invitations field. // Invitations is the resolver for the invitations field.
@@ -1305,21 +1368,26 @@ func (r *organizationResolver) ScimConfiguration(ctx context.Context, obj *types
return types.NewSCIMConfiguration(config), nil return types.NewSCIMConfiguration(config), nil
} }
// ViewerMembership is the resolver for the viewerMembership field. // Viewer is the resolver for the viewer field.
func (r *organizationResolver) ViewerMembership(ctx context.Context, obj *types.Organization) (*types.Membership, error) { func (r *organizationResolver) Viewer(ctx context.Context, obj *types.Organization) (*types.Profile, error) {
if err := r.authorize(ctx, obj.ID, iam.ActionMembershipGet, authz.WithSkipAssumptionCheck()); err != nil { if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileGet); err != nil {
return nil, err return nil, err
} }
identity := authn.IdentityFromContext(ctx) identity := authn.IdentityFromContext(ctx)
membership, err := r.iam.AccountService.GetMembershipForOrganization(ctx, identity.ID, obj.ID) profile, err := r.iam.OrganizationService.GetProfileForIdentityAndOrganization(ctx, identity.ID, obj.ID)
if err != nil { if err != nil {
r.logger.ErrorCtx(ctx, "cannot get membership for organization", log.Error(err)) var errNotFound *iam.ErrProfileNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get profile", log.Error(err))
return nil, gqlutils.Internal(ctx) return nil, gqlutils.Internal(ctx)
} }
return types.NewMembership(membership), nil return types.NewProfile(profile), nil
} }
// Permission is the resolver for the permission field. // Permission is the resolver for the permission field.
@@ -1372,37 +1440,95 @@ func (r *personalAPIKeyConnectionResolver) TotalCount(ctx context.Context, obj *
// Identity is the resolver for the identity field. // Identity is the resolver for the identity field.
func (r *profileResolver) Identity(ctx context.Context, obj *types.Profile) (*types.Identity, error) { func (r *profileResolver) Identity(ctx context.Context, obj *types.Profile) (*types.Identity, error) {
panic(fmt.Errorf("not implemented: Identity - identity")) if err := r.authorize(
ctx,
obj.ID,
iam.ActionMembershipProfileGet,
authz.WithSkipAssumptionCheck(),
); err != nil {
return nil, err
}
identity, err := r.iam.AccountService.GetIdentity(ctx, obj.Identity.ID)
if err != nil {
var errNotFound *iam.ErrIdentityNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get identity", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewIdentity(identity), nil
} }
// Organization is the resolver for the organization field. // Organization is the resolver for the organization field.
func (r *profileResolver) Organization(ctx context.Context, obj *types.Profile) (*types.Organization, error) { func (r *profileResolver) Organization(ctx context.Context, obj *types.Profile) (*types.Organization, error) {
panic(fmt.Errorf("not implemented: Organization - organization")) if err := r.authorize(ctx, obj.ID, iam.ActionOrganizationGet, authz.WithSkipAssumptionCheck()); err != nil {
return nil, err
}
organization, err := r.iam.OrganizationService.GetOrganization(ctx, obj.Organization.ID)
if err != nil {
var errNotFound *iam.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewOrganization(organization), nil
} }
// Membership is the resolver for the membership field. // Membership is the resolver for the membership field.
func (r *profileResolver) Membership(ctx context.Context, obj *types.Profile) (*types.Membership, error) { func (r *profileResolver) Membership(ctx context.Context, obj *types.Profile) (*types.Membership, error) {
panic(fmt.Errorf("not implemented: Membership - membership")) if err := r.authorize(ctx, obj.ID, iam.ActionMembershipGet, authz.WithSkipAssumptionCheck()); err != nil {
return nil, err
}
membership, err := r.iam.AccountService.GetMembershipForOrganization(ctx, obj.Identity.ID, obj.Organization.ID)
if err != nil {
var errNotFound *iam.ErrMembershipNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get membership", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewMembership(membership), nil
} }
// Permission is the resolver for the permission field. // Permission is the resolver for the permission field.
func (r *profileResolver) Permission(ctx context.Context, obj *types.Profile, action string) (bool, error) { func (r *profileResolver) Permission(ctx context.Context, obj *types.Profile, action string) (bool, error) {
panic(fmt.Errorf("not implemented: Permission - permission")) return r.Resolver.Permission(ctx, obj, action)
} }
// TotalCount is the resolver for the totalCount field. // TotalCount is the resolver for the totalCount field.
func (r *profileConnectionResolver) TotalCount(ctx context.Context, obj *types1.ProfileConnection) (int, error) { func (r *profileConnectionResolver) TotalCount(ctx context.Context, obj *types.ProfileConnection) (*int, error) {
panic(fmt.Errorf("not implemented: TotalCount - totalCount")) switch obj.Resolver.(type) {
case *identityResolver:
count, err := r.iam.AccountService.CountProfiles(ctx, obj.ParentID, coredata.NewMembershipProfileFilter(nil))
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count sprofiles", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &count, nil
case *organizationResolver:
count, err := r.iam.OrganizationService.CountProfiles(ctx, obj.ParentID, coredata.NewMembershipProfileFilter(nil))
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count profiles", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &count, nil
} }
// Edges is the resolver for the edges field. r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver))
func (r *profileConnectionResolver) Edges(ctx context.Context, obj *types1.ProfileConnection) ([]*types.ProfileEdge, error) { return nil, gqlutils.Internal(ctx)
panic(fmt.Errorf("not implemented: Edges - edges"))
}
// PageInfo is the resolver for the pageInfo field.
func (r *profileConnectionResolver) PageInfo(ctx context.Context, obj *types1.ProfileConnection) (*types.PageInfo, error) {
panic(fmt.Errorf("not implemented: PageInfo - pageInfo"))
} }
// Node is the resolver for the node field. // Node is the resolver for the node field.
@@ -1523,12 +1649,14 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
errOrganizationNotFound *iam.ErrOrganizationNotFound errOrganizationNotFound *iam.ErrOrganizationNotFound
errIdentityNotFound *iam.ErrIdentityNotFound errIdentityNotFound *iam.ErrIdentityNotFound
errSessionNotFound *iam.ErrSessionNotFound errSessionNotFound *iam.ErrSessionNotFound
errProfileNotFound *iam.ErrProfileNotFound
errMembershipNotFound *iam.ErrMembershipNotFound errMembershipNotFound *iam.ErrMembershipNotFound
errInvitationNotFound *iam.ErrInvitationNotFound errInvitationNotFound *iam.ErrInvitationNotFound
isNotFoundErr = errors.As(err, &errOrganizationNotFound) || isNotFoundErr = errors.As(err, &errOrganizationNotFound) ||
errors.As(err, &errIdentityNotFound) || errors.As(err, &errIdentityNotFound) ||
errors.As(err, &errSessionNotFound) || errors.As(err, &errSessionNotFound) ||
errors.As(err, &errProfileNotFound) ||
errors.As(err, &errMembershipNotFound) || errors.As(err, &errMembershipNotFound) ||
errors.As(err, &errInvitationNotFound) errors.As(err, &errInvitationNotFound)
) )
@@ -1761,34 +1889,9 @@ func (r *sCIMConfigurationResolver) Permission(ctx context.Context, obj *types.S
return r.Resolver.Permission(ctx, obj, action) return r.Resolver.Permission(ctx, obj, action)
} }
// Membership is the resolver for the membership field. // Profile is the resolver for the profile field.
func (r *sCIMEventResolver) Membership(ctx context.Context, obj *types.SCIMEvent) (*types.Membership, error) { func (r *sCIMEventResolver) Profile(ctx context.Context, obj *types.SCIMEvent) (*types.Profile, error) {
if obj.Membership == nil { panic(fmt.Errorf("not implemented: Profile - profile"))
return nil, nil
}
if err := r.authorize(ctx, obj.Membership.ID, iam.ActionMembershipGet); err != nil {
return nil, err
}
if gqlutils.OnlyIDSelected(ctx) {
return &types.Membership{
ID: obj.Membership.ID,
}, nil
}
membership, err := r.iam.GetMembership(ctx, obj.Membership.ID)
if err != nil {
var errMembershipNotFound *iam.ErrMembershipNotFound
if errors.As(err, &errMembershipNotFound) {
return nil, nil
}
r.logger.ErrorCtx(ctx, "cannot get membership for scim event", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewMembership(membership), nil
} }
// Permission is the resolver for the permission field. // Permission is the resolver for the permission field.