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
* @nogrep
*/
@@ -15,11 +15,6 @@ export type MembershipCardFragment$data = {
readonly expiresAt: string;
readonly id: string;
} | null | undefined;
readonly organization: {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
};
readonly " $fragmentType": "MembershipCardFragment";
};
export type MembershipCardFragment$key = {
@@ -27,15 +22,7 @@ export type MembershipCardFragment$key = {
readonly " $fragmentSpreads": FragmentRefs<"MembershipCardFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
@@ -49,7 +36,13 @@ return {
"name": "lastSession",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -59,43 +52,12 @@ return {
}
],
"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",
"abstractKey": null
};
})();
(node as any).hash = "5a2532aed755a94df49c2f9ce0929a1f";
(node as any).hash = "3da5f340850d352daaa553ca7b78aa37";
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
* @nogrep
*/
@@ -16,11 +16,6 @@ export type MembershipsDropdownMenuItemFragment$data = {
readonly expiresAt: string;
readonly id: string;
} | null | undefined;
readonly organization: {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
};
readonly " $fragmentType": "MembershipsDropdownMenuItemFragment";
};
export type MembershipsDropdownMenuItemFragment$key = {
@@ -61,36 +56,6 @@ return {
}
],
"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",
@@ -98,6 +63,6 @@ return {
};
})();
(node as any).hash = "915a6fb0071b4890010401a56f3affa6";
(node as any).hash = "4f40e957597476f8e6179f6df68d386e";
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
* @nogrep
*/
@@ -29,7 +29,7 @@ export type MembershipsPageQuery$data = {
readonly id: string;
readonly membership: {
readonly " $fragmentSpreads": FragmentRefs<"MembershipCardFragment">;
} | null | undefined;
};
readonly organization: {
readonly name: string;
readonly " $fragmentSpreads": FragmentRefs<"MembershipCard_organizationFragment">;
@@ -191,20 +191,24 @@ return {
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "membership",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "MembershipCardFragment"
}
],
"storageKey": null
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "membership",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "MembershipCardFragment"
}
],
"storageKey": null
},
"action": "THROW"
},
{
"kind": "RequiredField",
@@ -530,6 +534,6 @@ return {
};
})();
(node as any).hash = "05ac7c8fa236be3a01eeb28fc14ceb9d";
(node as any).hash = "84f5ab240bec94caf75e9ae163117e3d";
export default node;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -708,6 +708,42 @@ INNER JOIN signatories ON p.id = signatories.signed_by_profile_id
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(
ctx context.Context,
conn pg.Conn,

View File

@@ -843,3 +843,56 @@ func (s AccountService) CountSAMLConfigurationsForEmail(
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",
// Users can view their own profiles
policy.Allow(ActionMembershipProfileGet).
policy.Allow(
ActionMembershipProfileGet,
ActionMembershipProfileList,
).
WithSID("view-own-profiles").
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,
func(conn pg.Conn) error {
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)
}

View File

@@ -23,7 +23,6 @@ import (
"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/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/types/cursor"
)
@@ -34,8 +33,40 @@ func (r *connectorResolver) Permission(ctx context.Context, obj *types.Connector
}
// 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) {
panic(fmt.Errorf("not implemented: Profiles - profiles"))
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) {
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.
@@ -1211,8 +1242,40 @@ func (r *organizationResolver) HorizontalLogoURL(ctx context.Context, obj *types
}
// 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) {
panic(fmt.Errorf("not implemented: Profiles - profiles"))
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) {
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.
@@ -1305,21 +1368,26 @@ func (r *organizationResolver) ScimConfiguration(ctx context.Context, obj *types
return types.NewSCIMConfiguration(config), nil
}
// ViewerMembership is the resolver for the viewerMembership field.
func (r *organizationResolver) ViewerMembership(ctx context.Context, obj *types.Organization) (*types.Membership, error) {
if err := r.authorize(ctx, obj.ID, iam.ActionMembershipGet, authz.WithSkipAssumptionCheck()); err != nil {
// Viewer is the resolver for the viewer field.
func (r *organizationResolver) Viewer(ctx context.Context, obj *types.Organization) (*types.Profile, error) {
if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileGet); err != nil {
return nil, err
}
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 {
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 types.NewMembership(membership), nil
return types.NewProfile(profile), nil
}
// 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.
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.
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.
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.
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.
func (r *profileConnectionResolver) TotalCount(ctx context.Context, obj *types1.ProfileConnection) (int, error) {
panic(fmt.Errorf("not implemented: TotalCount - totalCount"))
}
func (r *profileConnectionResolver) TotalCount(ctx context.Context, obj *types.ProfileConnection) (*int, error) {
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.
func (r *profileConnectionResolver) Edges(ctx context.Context, obj *types1.ProfileConnection) ([]*types.ProfileEdge, error) {
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"))
r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver))
return nil, gqlutils.Internal(ctx)
}
// 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
errIdentityNotFound *iam.ErrIdentityNotFound
errSessionNotFound *iam.ErrSessionNotFound
errProfileNotFound *iam.ErrProfileNotFound
errMembershipNotFound *iam.ErrMembershipNotFound
errInvitationNotFound *iam.ErrInvitationNotFound
isNotFoundErr = errors.As(err, &errOrganizationNotFound) ||
errors.As(err, &errIdentityNotFound) ||
errors.As(err, &errSessionNotFound) ||
errors.As(err, &errProfileNotFound) ||
errors.As(err, &errMembershipNotFound) ||
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)
}
// Membership is the resolver for the membership field.
func (r *sCIMEventResolver) Membership(ctx context.Context, obj *types.SCIMEvent) (*types.Membership, error) {
if obj.Membership == nil {
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
// 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.