package connect_v1 // This file will be automatically regenerated based on the schema, any resolver // implementations // will be copied through when generating and any unknown code will be moved to the end. // Code generated by github.com/99designs/gqlgen version v0.17.86 import ( "context" "errors" "fmt" "time" "github.com/99designs/gqlgen/graphql" "github.com/vektah/gqlparser/v2/gqlerror" "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/mail" "go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/server/api/authn" "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" "go.probo.inc/probo/pkg/server/gqlutils" "go.probo.inc/probo/pkg/server/gqlutils/types/cursor" ) // Permission is the resolver for the permission field. func (r *connectorResolver) Permission(ctx context.Context, obj *types.Connector, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // Memberships is the resolver for the memberships field. func (r *identityResolver) Memberships(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) { if err := r.authorize(ctx, obj.ID, iam.ActionMembershipList); err != nil { return nil, err } if gqlutils.OnlyTotalCountSelected(ctx) { return &types.MembershipConnection{ Resolver: r, ParentID: obj.ID, }, nil } pageOrderBy := page.OrderBy[coredata.MembershipOrderField]{ Field: coredata.MembershipOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } if orderBy != nil { pageOrderBy = page.OrderBy[coredata.MembershipOrderField]{ Field: orderBy.Field, Direction: orderBy.Direction, } } cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) page, err := r.iam.AccountService.ListMemberships(ctx, obj.ID, cursor) if err != nil { r.logger.ErrorCtx(ctx, "cannot list memberships", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewMembershipConnection(page, r, obj.ID), nil } // PendingInvitations is the resolver for the pendingInvitations field. func (r *identityResolver) PendingInvitations(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrderBy) (*types.InvitationConnection, error) { if err := r.authorize(ctx, obj.ID, iam.ActionInvitationList); err != nil { return nil, err } if gqlutils.OnlyTotalCountSelected(ctx) { return &types.InvitationConnection{ Resolver: r, ParentID: obj.ID, }, nil } pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{ Field: coredata.InvitationOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) page, err := r.iam.AccountService.ListPendingInvitations(ctx, obj.ID, cursor) if err != nil { r.logger.ErrorCtx(ctx, "cannot list pending invitations", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewInvitationConnection(page, r, obj.ID, nil), nil } // Sessions is the resolver for the sessions field. func (r *identityResolver) Sessions(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SessionOrder) (*types.SessionConnection, error) { if err := r.authorize(ctx, obj.ID, iam.ActionSessionList); err != nil { return nil, err } if gqlutils.OnlyTotalCountSelected(ctx) { return &types.SessionConnection{ Resolver: r, ParentID: obj.ID, }, nil } pageOrderBy := page.OrderBy[coredata.SessionOrderField]{ Field: coredata.SessionOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } if orderBy != nil { pageOrderBy = page.OrderBy[coredata.SessionOrderField]{ Field: orderBy.Field, Direction: orderBy.Direction, } } cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) page, err := r.iam.AccountService.ListSessions(ctx, obj.ID, cursor) if err != nil { r.logger.ErrorCtx(ctx, "cannot list sessions", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewSessionConnection(page, r, obj.ID), nil } // PersonalAPIKeys is the resolver for the personalAPIKeys field. func (r *identityResolver) PersonalAPIKeys(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PersonalAPIKeyConnection, error) { if err := r.authorize(ctx, obj.ID, iam.ActionPersonalAPIKeyList); err != nil { return nil, err } if gqlutils.OnlyTotalCountSelected(ctx) { return &types.PersonalAPIKeyConnection{ Resolver: r, ParentID: obj.ID, }, nil } pageOrderBy := page.OrderBy[coredata.PersonalAPIKeyOrderField]{ Field: coredata.PersonalAPIKeyOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) page, err := r.iam.AccountService.ListPersonalAPIKeys(ctx, obj.ID, cursor) if err != nil { r.logger.ErrorCtx(ctx, "cannot list personal api keys", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewPersonalAPIKeyConnection(page, r, obj.ID), nil } // Permission is the resolver for the permission field. func (r *identityResolver) Permission(ctx context.Context, obj *types.Identity, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // Organization is the resolver for the organization field. func (r *invitationResolver) Organization(ctx context.Context, obj *types.Invitation) (*types.Organization, error) { if err := r.authorize(ctx, obj.Organization.ID, iam.ActionOrganizationGet, authz.WithSkipAssumptionCheck()); err != nil { return nil, err } if gqlutils.OnlyIDSelected(ctx) { return &types.Organization{ ID: obj.Organization.ID, }, nil } organization, err := r.iam.OrganizationService.GetOrganizationForInvitation(ctx, obj.ID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get organization for invitation", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewOrganization(organization), nil } // Permission is the resolver for the permission field. func (r *invitationResolver) Permission(ctx context.Context, obj *types.Invitation, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // TotalCount is the resolver for the totalCount field. func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *types.InvitationConnection) (*int, error) { switch obj.Resolver.(type) { case *organizationResolver: if err := r.authorize(ctx, obj.ParentID, iam.ActionInvitationList); err != nil { return nil, err } count, err := r.iam.OrganizationService.CountInvitations(ctx, obj.ParentID, obj.Filters) if err != nil { r.logger.ErrorCtx(ctx, "cannot count invitations", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &count, nil case *identityResolver: if err := r.authorize(ctx, obj.ParentID, iam.ActionInvitationList); err != nil { return nil, err } count, err := r.iam.AccountService.CountPendingInvitations(ctx, obj.ParentID) if err != nil { r.logger.ErrorCtx(ctx, "cannot count invitations", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) return nil, gqlutils.Internal(ctx) } // Identity is the resolver for the identity field. func (r *membershipResolver) Identity(ctx context.Context, obj *types.Membership) (*types.Identity, error) { if err := r.authorize( ctx, obj.Identity.ID, iam.ActionIdentityGet, authz.WithAttr("organization_id", obj.Organization.ID.String()), ); err != nil { return nil, err } if gqlutils.OnlyIDSelected(ctx) { return &types.Identity{ ID: obj.Identity.ID, }, nil } identity, err := r.iam.AccountService.GetIdentityForMembership(ctx, obj.ID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get identity for membership", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewIdentity(identity), nil } // Profile is the resolver for the profile field. func (r *membershipResolver) Profile(ctx context.Context, obj *types.Membership) (*types.MembershipProfile, error) { if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileGet); err != nil { return nil, err } profile, err := r.iam.AccountService.GetProfileForMembership(ctx, obj.ID) if err != nil { var errProfileNotFound *iam.ErrProfileNotFound if errors.As(err, &errProfileNotFound) { return nil, gqlutils.NotFound(ctx, err) } r.logger.ErrorCtx(ctx, "cannot get profile for membership", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewMembershipProfile(profile), nil } // Organization is the resolver for the organization field. func (r *membershipResolver) Organization(ctx context.Context, obj *types.Membership) (*types.Organization, error) { if err := r.authorize(ctx, obj.Organization.ID, iam.ActionOrganizationGet, authz.WithSkipAssumptionCheck()); err != nil { return nil, err } if gqlutils.OnlyIDSelected(ctx) { return &types.Organization{ ID: obj.Organization.ID, }, nil } organization, err := r.iam.OrganizationService.GetOrganizationForMembership(ctx, obj.ID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get organization for membership", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewOrganization(organization), nil } // LastSession is the resolver for the lastSession field. func (r *membershipResolver) LastSession(ctx context.Context, obj *types.Membership) (*types.Session, error) { if err := r.authorize(ctx, obj.ID, iam.ActionMembershipGet, authz.WithSkipAssumptionCheck()); err != nil { return nil, err } session := authn.SessionFromContext(ctx) if session == nil { return nil, nil } childSession, err := r.iam.SessionService.GetActiveSessionForMembership(ctx, session.ID, obj.ID) if err != nil { var errSessionNotFound *iam.ErrSessionNotFound if errors.As(err, &errSessionNotFound) { return nil, nil } r.logger.ErrorCtx(ctx, "cannot get active session for membership", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewSession(childSession), nil } // Permission is the resolver for the permission field. func (r *membershipResolver) Permission(ctx context.Context, obj *types.Membership, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // TotalCount is the resolver for the totalCount field. func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *types.MembershipConnection) (*int, error) { if err := r.authorize(ctx, obj.ParentID, iam.ActionMembershipList); err != nil { return nil, err } switch obj.Resolver.(type) { case *identityResolver: count, err := r.iam.AccountService.CountMemberships(ctx, obj.ParentID) if err != nil { r.logger.ErrorCtx(ctx, "cannot count memberships", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &count, nil case *organizationResolver: count, err := r.iam.OrganizationService.CountMemberships(ctx, obj.ParentID) if err != nil { r.logger.ErrorCtx(ctx, "cannot count memberships", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) return nil, gqlutils.Internal(ctx) } // Permission is the resolver for the permission field. func (r *membershipProfileResolver) Permission(ctx context.Context, obj *types.MembershipProfile, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // SignIn is the resolver for the signIn field. func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) (*types.SignInPayload, error) { identity, err := r.iam.AuthService.CheckCredentials(ctx, input.Email, input.Password) if err != nil { var errInvalidPassword *iam.ErrInvalidPassword if errors.As(err, &errInvalidPassword) { return nil, gqlutils.Invalid(ctx, err) } var errInvalidCredentials *iam.ErrInvalidCredentials if errors.As(err, &errInvalidCredentials) { return nil, &gqlerror.Error{ Message: err.Error(), Extensions: map[string]any{ "code": "INVALID_CREDENTIALS", }, } } r.logger.ErrorCtx(ctx, "cannot check credentials", log.Error(err)) return nil, gqlutils.Internal(ctx) } session := authn.SessionFromContext(ctx) switch { case session == nil: var err error session, err = r.iam.AuthService.OpenSessionWithPassword( ctx, identity.ID, ) if err != nil { r.logger.ErrorCtx(ctx, "cannot create session", log.Error(err)) return nil, gqlutils.Internal(ctx) } case session.IdentityID != identity.ID: if err := r.iam.SessionService.CloseSession(ctx, session.ID); err != nil { r.logger.ErrorCtx(ctx, "cannot close session", log.Error(err)) return nil, gqlutils.Internal(ctx) } session, err = r.iam.AuthService.OpenSessionWithPassword( ctx, identity.ID, ) if err != nil { r.logger.ErrorCtx(ctx, "cannot create session", log.Error(err)) return nil, gqlutils.Internal(ctx) } } if input.OrganizationID != nil { var err error session, _, err = r.iam.SessionService.OpenPasswordChildSessionForOrganization(ctx, session.ID, *input.OrganizationID) if err != nil { // Here session middleware already took care of expired/nil root session so we only handle membership related errors var errMembershipNotFound *iam.ErrMembershipNotFound var errMembershipInactive *iam.ErrMembershipInactive if errors.As(err, errMembershipNotFound) || errors.As(err, errMembershipInactive) { return nil, gqlutils.Forbidden(ctx, err) } r.logger.ErrorCtx(ctx, "cannot assume organization", log.Error(err)) return nil, gqlutils.Internal(ctx) } } w := gqlutils.HTTPResponseWriterFromContext(ctx) r.sessionCookie.Set(w, session) return &types.SignInPayload{ Identity: types.NewIdentity(identity), Session: types.NewSession(session), }, nil } // SignUp is the resolver for the signUp field. func (r *mutationResolver) SignUp(ctx context.Context, input types.SignUpInput) (*types.SignUpPayload, error) { identity, session, err := r.iam.AuthService.CreateIdentityWithPassword( ctx, &iam.CreateIdentityWithPasswordRequest{ Email: input.Email, Password: input.Password, FullName: input.FullName, }, ) if err != nil { var errIdentityAlreadyExists *iam.ErrIdentityAlreadyExists if errors.As(err, &errIdentityAlreadyExists) { return nil, gqlutils.Invalid(ctx, err) } r.logger.ErrorCtx(ctx, "cannot create identity with password", log.Error(err)) return nil, gqlutils.Internal(ctx) } w := gqlutils.HTTPResponseWriterFromContext(ctx) r.sessionCookie.Set(w, session) return &types.SignUpPayload{ Identity: types.NewIdentity(identity), }, nil } // SignOut is the resolver for the signOut field. func (r *mutationResolver) SignOut(ctx context.Context) (*types.SignOutPayload, error) { session := authn.SessionFromContext(ctx) err := r.iam.SessionService.CloseSession(ctx, session.ID) if err != nil { var ErrSessionNotFound *iam.ErrSessionNotFound if errors.As(err, &ErrSessionNotFound) { return &types.SignOutPayload{}, nil } r.logger.ErrorCtx(ctx, "cannot close session", log.Error(err)) return nil, gqlutils.Internal(ctx) } w := gqlutils.HTTPResponseWriterFromContext(ctx) r.sessionCookie.Clear(w) return &types.SignOutPayload{Success: true}, nil } // SignUpFromInvitation is the resolver for the signUpFromInvitation field. func (r *mutationResolver) SignUpFromInvitation(ctx context.Context, input types.SignUpFromInvitationInput) (*types.SignUpFromInvitationPayload, error) { identity, session, err := r.iam.AuthService.CreateIdentityFromInvitation( ctx, &iam.CreateIdentityFromInvitationRequest{ InvitationToken: input.Token, Password: input.Password, FullName: input.FullName, }, ) if err != nil { var ( errInvalidToken *iam.ErrInvalidToken errInvitationNotFound *iam.ErrInvitationNotFound errInvitationAlreadyAccepted *iam.ErrInvitationAlreadyAccepted errInvitationExpired *iam.ErrInvitationExpired errIdentityAlreadyExists *iam.ErrIdentityAlreadyExists isInvalidErr = errors.As(err, &errInvalidToken) || errors.As(err, &errInvitationNotFound) || errors.As(err, &errInvitationAlreadyAccepted) || errors.As(err, &errInvitationExpired) ) if isInvalidErr { return nil, gqlutils.Invalid(ctx, err) } if errors.As(err, &errIdentityAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } r.logger.ErrorCtx(ctx, "cannot create identity from invitation", log.Error(err)) return nil, gqlutils.Internal(ctx) } w := gqlutils.HTTPResponseWriterFromContext(ctx) r.sessionCookie.Set(w, session) return &types.SignUpFromInvitationPayload{ Identity: &types.Identity{ ID: identity.ID, Email: identity.EmailAddress, EmailVerified: identity.EmailAddressVerified, CreatedAt: identity.CreatedAt, UpdatedAt: identity.UpdatedAt, }, }, nil } // ForgotPassword is the resolver for the forgotPassword field. func (r *mutationResolver) ForgotPassword(ctx context.Context, input types.ForgotPasswordInput) (*types.ForgotPasswordPayload, error) { err := r.iam.AuthService.SendPasswordResetInstructionByEmail( ctx, input.Email, ) if err != nil { r.logger.ErrorCtx(ctx, "cannot send password reset instruction by email", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.ForgotPasswordPayload{ Success: true, }, nil } // ResetPassword is the resolver for the resetPassword field. func (r *mutationResolver) ResetPassword(ctx context.Context, input types.ResetPasswordInput) (*types.ResetPasswordPayload, error) { err := r.iam.AuthService.ResetPassword( ctx, &iam.ResetPasswordRequest{ Token: input.Token, Password: input.Password, }, ) if err != nil { var errInvalidToken *iam.ErrInvalidToken if errors.As(err, &errInvalidToken) { return nil, gqlutils.Invalid(ctx, err) } r.logger.ErrorCtx(ctx, "cannot reset password", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.ResetPasswordPayload{ Success: true, }, nil } // VerifyEmail is the resolver for the verifyEmail field. func (r *mutationResolver) VerifyEmail(ctx context.Context, input types.VerifyEmailInput) (*types.VerifyEmailPayload, error) { err := r.iam.AccountService.VerifyEmail(ctx, input.Token) if err != nil { var ( errInvalidToken *iam.ErrInvalidToken errIdentityNotFound *iam.ErrIdentityNotFound errEmailAlreadyVerified *iam.ErrEmailAlreadyVerified errEmailVerificationMismatch *iam.ErrEmailVerificationMismatch isInvalidErr = errors.As(err, &errInvalidToken) || errors.As(err, &errEmailVerificationMismatch) ) if isInvalidErr { return nil, gqlutils.Invalid(ctx, err) } if errors.As(err, &errEmailAlreadyVerified) { return nil, gqlutils.Conflict(ctx, err) } if errors.As(err, &errIdentityNotFound) { return nil, gqlutils.NotFound(ctx, err) } r.logger.ErrorCtx(ctx, "cannot verify email", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.VerifyEmailPayload{ Success: true, }, nil } // ChangePassword is the resolver for the changePassword field. func (r *mutationResolver) ChangePassword(ctx context.Context, input types.ChangePasswordInput) (*types.ChangePasswordPayload, error) { identity := authn.IdentityFromContext(ctx) err := r.iam.AccountService.ChangePassword( ctx, identity.ID, &iam.ChangePasswordRequest{ CurrentPassword: input.CurrentPassword, NewPassword: input.NewPassword, }, ) if err != nil { var ( errInvalidPassword *iam.ErrInvalidPassword errIdentityNotFound *iam.ErrIdentityNotFound ) if errors.As(err, &errInvalidPassword) { return nil, gqlutils.Invalid(ctx, err) } if errors.As(err, &errIdentityNotFound) { return nil, gqlutils.NotFound(ctx, err) } r.logger.ErrorCtx(ctx, "cannot change password", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.ChangePasswordPayload{ Success: true, }, nil } // ChangeEmail is the resolver for the changeEmail field. func (r *mutationResolver) ChangeEmail(ctx context.Context, input types.ChangeEmailInput) (*types.ChangeEmailPayload, error) { identity := authn.IdentityFromContext(ctx) err := r.iam.AccountService.ChangeEmail( ctx, identity.ID, &iam.ChangeEmailRequest{ NewEmail: input.NewEmail, Password: input.Password, }, ) if err != nil { var ( errInvalidPassword *iam.ErrInvalidPassword errIdentityNotFound *iam.ErrIdentityNotFound ) if errors.As(err, &errInvalidPassword) { return nil, gqlutils.Invalid(ctx, err) } if errors.As(err, &errIdentityNotFound) { return nil, gqlutils.NotFound(ctx, err) } r.logger.ErrorCtx(ctx, "cannot change email", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.ChangeEmailPayload{ Success: true, }, nil } // AssumeOrganizationSession is the resolver for the assumeOrganizationSession field. func (r *mutationResolver) AssumeOrganizationSession(ctx context.Context, input types.AssumeOrganizationSessionInput) (*types.AssumeOrganizationSessionPayload, error) { rootSession := authn.SessionFromContext(ctx) childSession, membership, err := r.iam.SessionService.AssumeOrganizationSession(ctx, rootSession.ID, input.OrganizationID) if err != nil { var ( errMembershipNotFound *iam.ErrMembershipNotFound errPasswordRequired *iam.ErrPasswordRequired errSAMLAuthenticationRequired *iam.ErrSAMLAuthenticationRequired ) switch { case errors.As(err, &errMembershipNotFound): return nil, gqlutils.NotFound(ctx, err) case errors.As(err, &errPasswordRequired): return &types.AssumeOrganizationSessionPayload{ Result: types.PasswordRequired{ Reason: types.ReauthenticationReason(errPasswordRequired.Reason), }, }, nil case errors.As(err, &errSAMLAuthenticationRequired): return &types.AssumeOrganizationSessionPayload{ Result: types.SAMLAuthenticationRequired{ Reason: types.ReauthenticationReason(errSAMLAuthenticationRequired.Reason), RedirectURL: errSAMLAuthenticationRequired.RedirectURL, }, }, nil default: r.logger.ErrorCtx(ctx, "cannot assume organization session", log.Error(err)) return nil, gqlutils.Internal(ctx) } } return &types.AssumeOrganizationSessionPayload{ Result: types.OrganizationSessionCreated{ Session: types.NewSession(childSession), Membership: types.NewMembership(membership), }, }, nil } // RevokeSession is the resolver for the revokeSession field. func (r *mutationResolver) RevokeSession(ctx context.Context, input types.RevokeSessionInput) (*types.RevokeSessionPayload, error) { if err := r.authorize(ctx, input.SessionID, iam.ActionSessionRevoke); err != nil { return nil, err } identity := authn.IdentityFromContext(ctx) err := r.iam.SessionService.RevokeSession(ctx, identity.ID, input.SessionID) if err != nil { var ErrSessionExpired *iam.ErrSessionExpired if errors.As(err, &ErrSessionExpired) { return &types.RevokeSessionPayload{Success: true}, nil } r.logger.ErrorCtx(ctx, "cannot revoke session", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.RevokeSessionPayload{Success: true}, nil } // RevokeAllSessions is the resolver for the revokeAllSessions field. func (r *mutationResolver) RevokeAllSessions(ctx context.Context) (*types.RevokeAllSessionsPayload, error) { if err := r.authorize(ctx, authn.SessionFromContext(ctx).ID, iam.ActionSessionRevokeAll); err != nil { return nil, err } session := authn.SessionFromContext(ctx) revokedCount, err := r.iam.SessionService.RevokeAllSessions(ctx, session.ID) if err != nil { r.logger.ErrorCtx(ctx, "cannot revoke all sessions", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.RevokeAllSessionsPayload{RevokedCount: int(revokedCount)}, nil } // CreatePersonalAPIKey is the resolver for the createPersonalAPIKey field. func (r *mutationResolver) CreatePersonalAPIKey(ctx context.Context, input types.CreatePersonalAPIKeyInput) (*types.CreatePersonalAPIKeyPayload, error) { identity := authn.IdentityFromContext(ctx) if err := r.authorize(ctx, identity.ID, iam.ActionPersonalAPIKeyCreate); err != nil { return nil, err } userAPIKey, token, err := r.iam.AccountService.CreatePersonalAPIKey( ctx, identity.ID, input.Name, input.ExpiresAt, ) if err != nil { r.logger.ErrorCtx(ctx, "cannot create personal api key", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.CreatePersonalAPIKeyPayload{ PersonalAPIKeyEdge: types.NewPersonalAPIKeyEdge(userAPIKey, coredata.PersonalAPIKeyOrderFieldCreatedAt), Token: token, }, nil } // RevokePersonalAPIKey is the resolver for the revokePersonalAPIKey field. func (r *mutationResolver) RevokePersonalAPIKey(ctx context.Context, input types.RevokePersonalAPIKeyInput) (*types.RevokePersonalAPIKeyPayload, error) { if err := r.authorize(ctx, input.PersonalAPIKeyID, iam.ActionPersonalAPIKeyDelete); err != nil { return nil, err } identity := authn.IdentityFromContext(ctx) err := r.iam.AccountService.DeletePersonalAPIKey(ctx, identity.ID, input.PersonalAPIKeyID) if err != nil { r.logger.ErrorCtx(ctx, "cannot delete personal api key", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.RevokePersonalAPIKeyPayload{PersonalAPIKeyID: input.PersonalAPIKeyID}, nil } // CreateOrganization is the resolver for the createOrganization field. func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) { identity := authn.IdentityFromContext(ctx) // FIXME check email domain and related IDP config // if ok := r.authorize(ctx, identity.ID, iam.ActionOrganizationCreate); !ok { // return nil, nil // } var ( logoFile *iam.UploadedFile horizontalLogoFile *iam.UploadedFile ) if input.LogoFile != nil { logoFile = &iam.UploadedFile{ Content: input.LogoFile.File, Filename: input.LogoFile.Filename, ContentType: input.LogoFile.ContentType, Size: input.LogoFile.Size, } } if input.HorizontalLogoFile != nil { horizontalLogoFile = &iam.UploadedFile{ Content: input.HorizontalLogoFile.File, Filename: input.HorizontalLogoFile.Filename, ContentType: input.HorizontalLogoFile.ContentType, Size: input.HorizontalLogoFile.Size, } } organization, err := r.iam.OrganizationService.CreateOrganization( ctx, identity.ID, &iam.CreateOrganizationRequest{ Name: input.Name, LogoFile: logoFile, HorizontalLogoFile: horizontalLogoFile, }, ) if err != nil { r.logger.ErrorCtx(ctx, "cannot create organization", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.CreateOrganizationPayload{ Organization: types.NewOrganization(organization), }, nil } // UpdateOrganization is the resolver for the updateOrganization field. func (r *mutationResolver) UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) { if err := r.authorize(ctx, input.OrganizationID, iam.ActionOrganizationUpdate); err != nil { return nil, err } req := &iam.UpdateOrganizationRequest{ Name: input.Name, Description: gqlutils.UnwrapOmittable(input.Description), WebsiteURL: gqlutils.UnwrapOmittable(input.WebsiteURL), Email: gqlutils.UnwrapOmittable(input.Email), HeadquarterAddress: gqlutils.UnwrapOmittable(input.HeadquarterAddress), } if input.LogoFile != nil { req.LogoFile = &iam.UploadedFile{ Filename: input.LogoFile.Filename, ContentType: input.LogoFile.ContentType, Size: input.LogoFile.Size, Content: input.LogoFile.File, } } if input.HorizontalLogoFile != nil { req.HorizontalLogoFile = &iam.UploadedFile{ Filename: input.HorizontalLogoFile.Filename, ContentType: input.HorizontalLogoFile.ContentType, Size: input.HorizontalLogoFile.Size, Content: input.HorizontalLogoFile.File, } } organization, err := r.iam.OrganizationService.UpdateOrganization( ctx, input.OrganizationID, req, ) if err != nil { r.logger.ErrorCtx(ctx, "cannot update organization", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.UpdateOrganizationPayload{ Organization: &types.Organization{ ID: organization.ID, Name: organization.Name, Description: organization.Description, WebsiteURL: organization.WebsiteURL, Email: organization.Email, HeadquarterAddress: organization.HeadquarterAddress, CreatedAt: organization.CreatedAt, UpdatedAt: organization.UpdatedAt, }, }, nil } // DeleteOrganization is the resolver for the deleteOrganization field. func (r *mutationResolver) DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) { if err := r.authorize(ctx, input.OrganizationID, iam.ActionOrganizationDelete); err != nil { return nil, err } err := r.iam.OrganizationService.DeleteOrganization(ctx, input.OrganizationID) if err != nil { r.logger.ErrorCtx(ctx, "cannot delete organization", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.DeleteOrganizationPayload{DeletedOrganizationID: input.OrganizationID}, nil } // DeleteOrganizationHorizontalLogo is the resolver for the deleteOrganizationHorizontalLogo field. func (r *mutationResolver) DeleteOrganizationHorizontalLogo(ctx context.Context, input types.DeleteOrganizationHorizontalLogoInput) (*types.DeleteOrganizationHorizontalLogoPayload, error) { panic(fmt.Errorf("not implemented: DeleteOrganizationHorizontalLogo - deleteOrganizationHorizontalLogo")) } // InviteMember is the resolver for the inviteMember field. func (r *mutationResolver) InviteMember(ctx context.Context, input types.InviteMemberInput) (*types.InviteMemberPayload, error) { if err := r.authorize(ctx, input.OrganizationID, iam.ActionInvitationCreate); err != nil { return nil, err } invitation, err := r.iam.OrganizationService.InviteMember( ctx, input.OrganizationID, &iam.CreateInvitationRequest{ Email: input.Email, FullName: input.FullName, Role: input.Role, }, ) if err != nil { var errOrganizationNotFound *iam.ErrOrganizationNotFound var errMembershipAlreadyExists *iam.ErrMembershipAlreadyExists if errors.As(err, &errOrganizationNotFound) { return nil, gqlutils.NotFound(ctx, err) } if errors.As(err, &errMembershipAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } r.logger.ErrorCtx(ctx, "cannot add member to organization", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.InviteMemberPayload{ InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt), }, nil } // DeleteInvitation is the resolver for the deleteInvitation field. func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error) { if err := r.authorize(ctx, input.OrganizationID, iam.ActionInvitationDelete); err != nil { return nil, err } err := r.iam.OrganizationService.DeleteInvitation(ctx, input.OrganizationID, input.InvitationID) if err != nil { var errInvitationNotFound *iam.ErrInvitationNotFound var errInvitationNotDeleted *iam.ErrInvitationNotDeleted if errors.As(err, &errInvitationNotFound) { return nil, gqlutils.NotFound(ctx, err) } if errors.As(err, &errInvitationNotDeleted) { return nil, gqlutils.Invalid(ctx, err) } r.logger.ErrorCtx(ctx, "cannot delete invitation", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.DeleteInvitationPayload{DeletedInvitationID: input.InvitationID}, nil } // UpdateMembership is the resolver for the updateMembership field. func (r *mutationResolver) UpdateMembership(ctx context.Context, input types.UpdateMembershipInput) (*types.UpdateMembershipPayload, error) { if err := r.authorize(ctx, input.MembershipID, iam.ActionMembershipUpdate); err != nil { return nil, err } if input.Role == coredata.MembershipRoleOwner { if err := r.authorize(ctx, input.MembershipID, iam.ActionMembershipRoleSetOwner); err != nil { return nil, err } } membership, err := r.iam.OrganizationService.UpdateMempership(ctx, input.OrganizationID, input.MembershipID, input.Role) if err != nil { r.logger.ErrorCtx(ctx, "cannot update membership", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.UpdateMembershipPayload{ Membership: types.NewMembership(membership), }, nil } // RemoveMember is the resolver for the removeMember field. func (r *mutationResolver) RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error) { if err := r.authorize(ctx, input.MembershipID, iam.ActionMembershipDelete); err != nil { return nil, err } err := r.iam.OrganizationService.RemoveMember(ctx, input.OrganizationID, input.MembershipID) if err != nil { var errManagedBySCIM *iam.ErrMembershipManagedBySCIM var errLastActiveOwner *iam.ErrLastActiveOwner if errors.As(err, &errManagedBySCIM) { return nil, gqlutils.Conflict(ctx, err) } if errors.As(err, &errLastActiveOwner) { return nil, gqlutils.Conflict(ctx, err) } r.logger.ErrorCtx(ctx, "cannot remove member from organization", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.RemoveMemberPayload{DeletedMembershipID: input.MembershipID}, nil } // AcceptInvitation is the resolver for the acceptInvitation field. func (r *mutationResolver) AcceptInvitation(ctx context.Context, input types.AcceptInvitationInput) (*types.AcceptInvitationPayload, error) { if err := r.authorize(ctx, input.InvitationID, iam.ActionInvitationAccept); err != nil { return nil, err } identity := authn.IdentityFromContext(ctx) invitation, membership, err := r.iam.AccountService.AcceptInvitation(ctx, identity.ID, input.InvitationID) if err != nil { r.logger.ErrorCtx(ctx, "cannot accept invitation", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.AcceptInvitationPayload{ MembershipEdge: types.NewMembershipEdge(membership, coredata.MembershipOrderFieldCreatedAt), Invitation: types.NewInvitation(invitation), }, nil } // CreateSAMLConfiguration is the resolver for the createSAMLConfiguration field. func (r *mutationResolver) CreateSAMLConfiguration(ctx context.Context, input types.CreateSAMLConfigurationInput) (*types.CreateSAMLConfigurationPayload, error) { if err := r.authorize(ctx, input.OrganizationID, iam.ActionSAMLConfigurationCreate); err != nil { return nil, err } req := &iam.CreateSAMLConfigurationRequest{ EmailDomain: input.EmailDomain, IdPEntityID: input.IdpEntityID, IdPSsoURL: input.IdpSsoURL, IdPCertificate: input.IdpCertificate, AutoSignupEnabled: input.AutoSignupEnabled, } if input.AttributeMappings != nil { req.AttributeEmail = input.AttributeMappings.Email req.AttributeFirstname = input.AttributeMappings.FirstName req.AttributeLastname = input.AttributeMappings.LastName req.AttributeRole = input.AttributeMappings.Role } samlConfiguration, err := r.iam.OrganizationService.CreateSAMLConfiguration( ctx, input.OrganizationID, req, ) if err != nil { var errSAMLConfigurationEmailDomainAlreadyExists *iam.ErrSAMLConfigurationEmailDomainAlreadyExists if errors.As(err, &errSAMLConfigurationEmailDomainAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } r.logger.ErrorCtx(ctx, "cannot create saml configuration", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.CreateSAMLConfigurationPayload{ SamlConfigurationEdge: types.NewSAMLConfigurationEdge( samlConfiguration, coredata.SAMLConfigurationOrderFieldCreatedAt, ), }, nil } // UpdateSAMLConfiguration is the resolver for the updateSAMLConfiguration field. func (r *mutationResolver) UpdateSAMLConfiguration(ctx context.Context, input types.UpdateSAMLConfigurationInput) (*types.UpdateSAMLConfigurationPayload, error) { if err := r.authorize(ctx, input.SamlConfigurationID, iam.ActionSAMLConfigurationUpdate); err != nil { return nil, err } req := &iam.UpdateSAMLConfigurationRequest{ IdPEntityID: input.IdpEntityID, IdPSsoURL: input.IdpSsoURL, IdPCertificate: input.IdpCertificate, AutoSignupEnabled: input.AutoSignupEnabled, EnforcementPolicy: &input.EnforcementPolicy, } if input.AttributeMappings != nil { req.AttributeEmail = input.AttributeMappings.Email req.AttributeFirstname = input.AttributeMappings.FirstName req.AttributeLastname = input.AttributeMappings.LastName req.AttributeRole = input.AttributeMappings.Role } samlConfiguration, err := r.iam.OrganizationService.UpdateSAMLConfiguration( ctx, input.OrganizationID, input.SamlConfigurationID, req, ) if err != nil { r.logger.ErrorCtx(ctx, "cannot update saml configuration", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.UpdateSAMLConfigurationPayload{ SamlConfiguration: types.NewSAMLConfiguration(samlConfiguration), }, nil } // DeleteSAMLConfiguration is the resolver for the deleteSAMLConfiguration field. func (r *mutationResolver) DeleteSAMLConfiguration(ctx context.Context, input types.DeleteSAMLConfigurationInput) (*types.DeleteSAMLConfigurationPayload, error) { if err := r.authorize(ctx, input.OrganizationID, iam.ActionSAMLConfigurationDelete); err != nil { return nil, err } err := r.iam.OrganizationService.DeleteSAMLConfiguration(ctx, input.OrganizationID, input.SamlConfigurationID) if err != nil { r.logger.ErrorCtx(ctx, "cannot delete saml configuration", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.DeleteSAMLConfigurationPayload{DeletedSamlConfigurationID: input.SamlConfigurationID}, nil } // CreateSCIMConfiguration is the resolver for the createSCIMConfiguration field. func (r *mutationResolver) CreateSCIMConfiguration(ctx context.Context, input types.CreateSCIMConfigurationInput) (*types.CreateSCIMConfigurationPayload, error) { if err := r.authorize(ctx, input.OrganizationID, iam.ActionSCIMConfigurationCreate); err != nil { return nil, err } config, token, err := r.iam.OrganizationService.CreateSCIMConfiguration(ctx, input.OrganizationID) if err != nil { r.logger.ErrorCtx(ctx, "cannot create scim configuration", log.Error(err)) return nil, gqlutils.Internal(ctx) } var bridge *types.SCIMBridge if input.ConnectorID != nil { scimBridge, err := r.iam.OrganizationService.CreateSCIMBridge(ctx, input.OrganizationID, config.ID, *input.ConnectorID) if err != nil { r.logger.ErrorCtx(ctx, "cannot create scim bridge", log.Error(err)) return nil, gqlutils.Internal(ctx) } bridge = types.NewSCIMBridge(scimBridge) } payload := &types.CreateSCIMConfigurationPayload{ ScimConfiguration: types.NewSCIMConfiguration(config), ScimBridge: bridge, Token: token, } return payload, nil } // DeleteSCIMConfiguration is the resolver for the deleteSCIMConfiguration field. func (r *mutationResolver) DeleteSCIMConfiguration(ctx context.Context, input types.DeleteSCIMConfigurationInput) (*types.DeleteSCIMConfigurationPayload, error) { if err := r.authorize(ctx, input.OrganizationID, iam.ActionSCIMConfigurationDelete); err != nil { return nil, err } err := r.iam.OrganizationService.DeleteSCIMConfiguration(ctx, input.OrganizationID, input.ScimConfigurationID) if err != nil { r.logger.ErrorCtx(ctx, "cannot delete scim configuration", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.DeleteSCIMConfigurationPayload{DeletedScimConfigurationID: input.ScimConfigurationID}, nil } // RegenerateSCIMToken is the resolver for the regenerateSCIMToken field. func (r *mutationResolver) RegenerateSCIMToken(ctx context.Context, input types.RegenerateSCIMTokenInput) (*types.RegenerateSCIMTokenPayload, error) { if err := r.authorize(ctx, input.ScimConfigurationID, iam.ActionSCIMConfigurationUpdate); err != nil { return nil, err } config, token, err := r.iam.OrganizationService.RegenerateSCIMToken(ctx, input.OrganizationID, input.ScimConfigurationID) if err != nil { r.logger.ErrorCtx(ctx, "cannot regenerate scim token", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.RegenerateSCIMTokenPayload{ ScimConfiguration: types.NewSCIMConfiguration(config), Token: token, }, nil } // UpdateSCIMBridge is the resolver for the updateSCIMBridge field. func (r *mutationResolver) UpdateSCIMBridge(ctx context.Context, input types.UpdateSCIMBridgeInput) (*types.UpdateSCIMBridgePayload, error) { if err := r.authorize(ctx, input.ScimBridgeID, iam.ActionSCIMBridgeUpdate); err != nil { return nil, err } bridge, err := r.iam.OrganizationService.UpdateSCIMBridge(ctx, input.OrganizationID, input.ScimBridgeID, input.ExcludedUserNames) if err != nil { r.logger.ErrorCtx(ctx, "cannot update scim bridge excluded user names", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &types.UpdateSCIMBridgePayload{ ScimBridge: types.NewSCIMBridge(bridge), }, nil } // LogoURL is the resolver for the logoUrl field. func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) { if err := r.authorize(ctx, obj.ID, iam.ActionOrganizationGet, authz.WithSkipAssumptionCheck()); err != nil { return nil, err } presignedURL, err := r.iam.OrganizationService.GenerateLogoURL(ctx, obj.ID, 1*time.Hour) if err != nil { r.logger.ErrorCtx(ctx, "cannot generate logo URL", log.Error(err)) return nil, gqlutils.Internal(ctx) } return presignedURL, nil } // HorizontalLogoURL is the resolver for the horizontalLogoUrl field. func (r *organizationResolver) HorizontalLogoURL(ctx context.Context, obj *types.Organization) (*string, error) { if err := r.authorize(ctx, obj.ID, iam.ActionOrganizationGet); err != nil { return nil, err } presignedURL, err := r.iam.OrganizationService.GenerateHorizontalLogoURL(ctx, obj.ID, 1*time.Hour) if err != nil { r.logger.ErrorCtx(ctx, "cannot generate horizontal logo URL", log.Error(err)) return nil, gqlutils.Internal(ctx) } return presignedURL, nil } // Members is the resolver for the members field. func (r *organizationResolver) Members(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) { if err := r.authorize(ctx, obj.ID, iam.ActionMembershipList); err != nil { return nil, err } if gqlutils.OnlyTotalCountSelected(ctx) { return &types.MembershipConnection{ Resolver: r, ParentID: obj.ID, }, nil } pageOrderBy := page.OrderBy[coredata.MembershipOrderField]{ Field: coredata.MembershipOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } if orderBy != nil { pageOrderBy.Field = coredata.MembershipOrderField(orderBy.Field) pageOrderBy.Direction = page.OrderDirection(orderBy.Direction) } cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) page, err := r.iam.OrganizationService.ListMembers(ctx, obj.ID, cursor) if err != nil { r.logger.ErrorCtx(ctx, "cannot list memberships", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewMembershipConnection(page, r, obj.ID), nil } // Invitations is the resolver for the invitations field. func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus, orderBy *types.InvitationOrderBy) (*types.InvitationConnection, error) { if err := r.authorize(ctx, obj.ID, iam.ActionInvitationList); err != nil { return nil, err } filters := coredata.NewInvitationFilter(nil) if status != nil { filters = coredata.NewInvitationFilter([]coredata.InvitationStatus{*status}) } if gqlutils.OnlyTotalCountSelected(ctx) { return &types.InvitationConnection{ Resolver: r, ParentID: obj.ID, Filters: filters, }, nil } pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{ Field: coredata.InvitationOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } if orderBy != nil { pageOrderBy = page.OrderBy[coredata.InvitationOrderField]{ Field: orderBy.Field, Direction: orderBy.Direction, } } cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) page, err := r.iam.OrganizationService.ListInvitations(ctx, obj.ID, cursor, filters) if err != nil { r.logger.ErrorCtx(ctx, "cannot list invitations", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewInvitationConnection(page, r, obj.ID, filters), nil } // SamlConfigurations is the resolver for the samlConfigurations field. func (r *organizationResolver) SamlConfigurations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.SAMLConfigurationConnection, error) { if err := r.authorize(ctx, obj.ID, iam.ActionSAMLConfigurationList); err != nil { return nil, err } if gqlutils.OnlyTotalCountSelected(ctx) { return &types.SAMLConfigurationConnection{ Resolver: r, ParentID: obj.ID, }, nil } pageOrderBy := page.OrderBy[coredata.SAMLConfigurationOrderField]{ Field: coredata.SAMLConfigurationOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) page, err := r.iam.OrganizationService.ListSAMLConfigurations(ctx, obj.ID, cursor) if err != nil { r.logger.ErrorCtx(ctx, "cannot list saml configurations", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewSAMLConfigurationConnection(page, r, obj.ID), nil } // ScimConfiguration is the resolver for the scimConfiguration field. func (r *organizationResolver) ScimConfiguration(ctx context.Context, obj *types.Organization) (*types.SCIMConfiguration, error) { if err := r.authorize(ctx, obj.ID, iam.ActionSCIMConfigurationGet); err != nil { return nil, err } config, err := r.iam.OrganizationService.GetSCIMConfiguration(ctx, obj.ID) if err != nil { var notFound *iam.ErrNoSCIMConfigurationFound if errors.As(err, ¬Found) { return nil, nil } r.logger.ErrorCtx(ctx, "cannot get scim configuration", log.Error(err)) return nil, gqlutils.Internal(ctx) } 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 { return nil, err } identity := authn.IdentityFromContext(ctx) membership, err := r.iam.AccountService.GetMembershipForOrganization(ctx, identity.ID, obj.ID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get membership for organization", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewMembership(membership), nil } // Permission is the resolver for the permission field. func (r *organizationResolver) Permission(ctx context.Context, obj *types.Organization, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // Token is the resolver for the token field. func (r *personalAPIKeyResolver) Token(ctx context.Context, obj *types.PersonalAPIKey) (*string, error) { if err := r.authorize(ctx, obj.ID, iam.ActionPersonalAPIKeyGet); err != nil { return nil, err } identity := authn.IdentityFromContext(ctx) token, err := r.iam.AccountService.RevealPersonalAPIKeyToken(ctx, identity.ID, obj.ID) if err != nil { r.logger.ErrorCtx(ctx, "cannot reveal personal api key token", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &token, nil } // Permission is the resolver for the permission field. func (r *personalAPIKeyResolver) Permission(ctx context.Context, obj *types.PersonalAPIKey, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // TotalCount is the resolver for the totalCount field. func (r *personalAPIKeyConnectionResolver) TotalCount(ctx context.Context, obj *types.PersonalAPIKeyConnection) (*int, error) { switch obj.Resolver.(type) { case *identityResolver: if err := r.authorize(ctx, obj.ParentID, iam.ActionPersonalAPIKeyList); err != nil { return nil, err } count, err := r.iam.AccountService.CountPersonalAPIKeys(ctx, obj.ParentID) if err != nil { r.logger.ErrorCtx(ctx, "cannot count personal api keys", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) return nil, gqlutils.Internal(ctx) } // Node is the resolver for the node field. func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) { var ( loadNode func(ctx context.Context, id gid.GID) (types.Node, error) action string ) switch id.EntityType() { case coredata.OrganizationEntityType: action = iam.ActionOrganizationGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { organization, err := r.iam.OrganizationService.GetOrganization(ctx, id) if err != nil { return nil, err } return types.NewOrganization(organization), nil } case coredata.IdentityEntityType: action = iam.ActionIdentityGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { identity, err := r.iam.AccountService.GetIdentity(ctx, id) if err != nil { return nil, err } return types.NewIdentity(identity), nil } case coredata.SessionEntityType: action = iam.ActionSessionGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { session, err := r.iam.GetSession(ctx, id) if err != nil { return nil, err } return types.NewSession(session), nil } case coredata.MembershipEntityType: action = iam.ActionMembershipGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { membership, err := r.iam.GetMembership(ctx, id) if err != nil { return nil, err } return types.NewMembership(membership), nil } case coredata.InvitationEntityType: action = iam.ActionInvitationGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { invitation, err := r.iam.GetInvitation(ctx, id) if err != nil { return nil, err } return types.NewInvitation(invitation), nil } case coredata.SAMLConfigurationEntityType: action = iam.ActionSAMLConfigurationGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { samlConfiguration, err := r.iam.GetSAMLconfiguration(ctx, id) if err != nil { return nil, err } return types.NewSAMLConfiguration(samlConfiguration), nil } case coredata.PersonalAPIKeyEntityType: action = iam.ActionPersonalAPIKeyGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { personalAPIKey, err := r.iam.GetPersonalAPIKey(ctx, id) if err != nil { return nil, err } return types.NewPersonalAPIKey(personalAPIKey), nil } case coredata.SCIMConfigurationEntityType: action = iam.ActionSCIMConfigurationGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { scimConfiguration, err := r.iam.GetSCIMConfiguration(ctx, id) if err != nil { return nil, err } return types.NewSCIMConfiguration(scimConfiguration), nil } case coredata.SCIMEventEntityType: action = iam.ActionSCIMEventGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { scimEvent, err := r.iam.GetSCIMEvent(ctx, id) if err != nil { return nil, err } return types.NewSCIMEvent(scimEvent), nil } default: return nil, fmt.Errorf("unsupported entity type: %d", id.EntityType()) } if err := r.authorize(ctx, id, action); err != nil { return nil, err } node, err := loadNode(ctx, id) if err != nil { var ( errOrganizationNotFound *iam.ErrOrganizationNotFound errIdentityNotFound *iam.ErrIdentityNotFound errSessionNotFound *iam.ErrSessionNotFound errMembershipNotFound *iam.ErrMembershipNotFound errInvitationNotFound *iam.ErrInvitationNotFound isNotFoundErr = errors.As(err, &errOrganizationNotFound) || errors.As(err, &errIdentityNotFound) || errors.As(err, &errSessionNotFound) || errors.As(err, &errMembershipNotFound) || errors.As(err, &errInvitationNotFound) ) if isNotFoundErr { return nil, gqlutils.NotFound(ctx, err) } r.logger.ErrorCtx(ctx, "cannot load node", log.Error(err)) return nil, gqlutils.Internal(ctx) } return node, nil } // Viewer is the resolver for the viewer field. func (r *queryResolver) Viewer(ctx context.Context) (*types.Identity, error) { identity := authn.IdentityFromContext(ctx) return &types.Identity{ ID: identity.ID, Email: identity.EmailAddress, EmailVerified: identity.EmailAddressVerified, FullName: identity.FullName, CreatedAt: identity.CreatedAt, UpdatedAt: identity.UpdatedAt, }, nil } // SsoLoginURL is the resolver for the ssoLoginURL field. func (r *queryResolver) SsoLoginURL(ctx context.Context, email mail.Addr) (*string, error) { count, err := r.iam.AccountService.CountSAMLConfigurationsForEmail(ctx, email) if err != nil { r.logger.ErrorCtx(ctx, "cannot count SAML configurations for email", log.Error(err)) return nil, gqlutils.Internal(ctx) } if count != 1 { if count == 0 { return nil, graphql.ErrorOnPath( ctx, fmt.Errorf("no SAML configuration for email"), ) } return nil, graphql.ErrorOnPath( ctx, fmt.Errorf("multiple SSO configurations found for this domain. Please use your organization-specific SSO login URL"), ) } samlConfigs, err := r.iam.AccountService.ListSAMLConfigurationsForEmail(ctx, email) if err != nil { r.logger.ErrorCtx(ctx, "cannot list SAML configurations for email", log.Error(err)) return nil, gqlutils.Internal(ctx) } samlConfig := samlConfigs[0] loginURL := r.baseURL.WithPath("/api/connect/v1/saml/2.0/" + samlConfig.ID.String()).MustString() return &loginURL, nil } // TestLoginURL is the resolver for the testLoginUrl field. func (r *sAMLConfigurationResolver) TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error) { return r.baseURL.WithPath("/api/connect/v1/saml/2.0/" + obj.ID.String()).MustString(), nil } // Permission is the resolver for the permission field. func (r *sAMLConfigurationResolver) Permission(ctx context.Context, obj *types.SAMLConfiguration, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // TotalCount is the resolver for the totalCount field. func (r *sAMLConfigurationConnectionResolver) TotalCount(ctx context.Context, obj *types.SAMLConfigurationConnection) (*int, error) { switch obj.Resolver.(type) { case *organizationResolver: count, err := r.iam.OrganizationService.CountSAMLConfigurations(ctx, obj.ParentID) if err != nil { r.logger.ErrorCtx(ctx, "cannot count saml configurations", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) return nil, gqlutils.Internal(ctx) } // ScimConfiguration is the resolver for the scimConfiguration field. func (r *sCIMBridgeResolver) ScimConfiguration(ctx context.Context, obj *types.SCIMBridge) (*types.SCIMConfiguration, error) { if err := r.authorize(ctx, obj.ScimConfiguration.ID, iam.ActionSCIMConfigurationGet); err != nil { return nil, err } if gqlutils.OnlyIDSelected(ctx) { return &types.SCIMConfiguration{ ID: obj.ScimConfiguration.ID, }, nil } scimConfiguration, err := r.iam.GetSCIMConfiguration(ctx, obj.ScimConfiguration.ID) if err != nil { var errNoSCIMConfigurationFound *iam.ErrNoSCIMConfigurationFound if errors.As(err, &errNoSCIMConfigurationFound) { return nil, nil } return nil, err } return types.NewSCIMConfiguration(scimConfiguration), nil } // Connector is the resolver for the connector field. func (r *sCIMBridgeResolver) Connector(ctx context.Context, obj *types.SCIMBridge) (*types.Connector, error) { if obj.Connector == nil { return nil, nil } // Authorize based on the SCIM configuration (connector accessed via bridge is a sub-resource) if err := r.authorize(ctx, obj.ScimConfiguration.ID, iam.ActionSCIMConfigurationGet); err != nil { return nil, err } if gqlutils.OnlyIDSelected(ctx) { return &types.Connector{ ID: obj.Connector.ID, }, nil } // Use metadata-only loading since we don't need the encrypted connection data connector, err := r.iam.OrganizationService.GetConnectorMetadataByID(ctx, obj.Connector.ID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get connector", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewConnector(connector), nil } // Permission is the resolver for the permission field. func (r *sCIMBridgeResolver) Permission(ctx context.Context, obj *types.SCIMBridge, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // EndpointURL is the resolver for the endpointUrl field. func (r *sCIMConfigurationResolver) EndpointURL(ctx context.Context, obj *types.SCIMConfiguration) (string, error) { return r.baseURL.WithPath("/api/connect/v1/scim/2.0").MustString(), nil } // Organization is the resolver for the organization field. func (r *sCIMConfigurationResolver) Organization(ctx context.Context, obj *types.SCIMConfiguration) (*types.Organization, error) { if err := r.authorize(ctx, obj.Organization.ID, iam.ActionOrganizationGet); err != nil { return nil, err } if gqlutils.OnlyIDSelected(ctx) { return &types.Organization{ ID: obj.Organization.ID, }, nil } organization, err := r.iam.OrganizationService.GetOrganization(ctx, obj.Organization.ID) if err != nil { var errOrganizationNotFound *iam.ErrOrganizationNotFound if errors.As(err, &errOrganizationNotFound) { return nil, nil } r.logger.ErrorCtx(ctx, "cannot get organization for scim configuration", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewOrganization(organization), nil } // Bridge is the resolver for the bridge field. func (r *sCIMConfigurationResolver) Bridge(ctx context.Context, obj *types.SCIMConfiguration) (*types.SCIMBridge, error) { if obj.Bridge == nil { return nil, nil } if err := r.authorize(ctx, obj.ID, iam.ActionSCIMConfigurationGet); err != nil { return nil, err } bridge, err := r.iam.OrganizationService.GetSCIMBridgeByID(ctx, obj.Bridge.ID) if err != nil { var errSCIMBridgeNotFound *iam.ErrSCIMBridgeNotFound if errors.As(err, &errSCIMBridgeNotFound) { return nil, nil } r.logger.ErrorCtx(ctx, "cannot get scim bridge", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewSCIMBridge(bridge), nil } // Events is the resolver for the events field. func (r *sCIMConfigurationResolver) Events(ctx context.Context, obj *types.SCIMConfiguration, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SCIMEventOrderBy) (*types.SCIMEventConnection, error) { if err := r.authorize(ctx, obj.ID, iam.ActionSCIMEventList); err != nil { return nil, err } pageOrderBy := page.OrderBy[coredata.SCIMEventOrderField]{ Field: coredata.SCIMEventOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } if orderBy != nil { pageOrderBy.Field = coredata.SCIMEventOrderField(orderBy.Field) pageOrderBy.Direction = page.OrderDirection(orderBy.Direction) } cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) events, err := r.iam.OrganizationService.ListSCIMEventsByConfigID(ctx, obj.ID, cursor) if err != nil { r.logger.ErrorCtx(ctx, "cannot list scim events", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewSCIMEventConnection(events, r, obj.ID), nil } // Permission is the resolver for the permission field. func (r *sCIMConfigurationResolver) Permission(ctx context.Context, obj *types.SCIMConfiguration, action string) (bool, error) { 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 } // Permission is the resolver for the permission field. func (r *sCIMEventResolver) Permission(ctx context.Context, obj *types.SCIMEvent, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // TotalCount is the resolver for the totalCount field. func (r *sCIMEventConnectionResolver) TotalCount(ctx context.Context, obj *types.SCIMEventConnection) (*int, error) { if err := r.authorize(ctx, obj.ParentID, iam.ActionSCIMEventList); err != nil { return nil, err } switch obj.Resolver.(type) { case *sCIMConfigurationResolver: count, err := r.iam.OrganizationService.CountSCIMEvents(ctx, obj.ParentID) if err != nil { r.logger.ErrorCtx(ctx, "cannot count scim events", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) return nil, gqlutils.Internal(ctx) } // Identity is the resolver for the identity field. func (r *sessionResolver) Identity(ctx context.Context, obj *types.Session) (*types.Identity, error) { if gqlutils.OnlyIDSelected(ctx) { return &types.Identity{ ID: obj.Identity.ID, }, nil } identity, err := r.iam.AccountService.GetIdentity(ctx, obj.Identity.ID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get identity for session", log.Error(err)) return nil, gqlutils.Internal(ctx) } return types.NewIdentity(identity), nil } // Permission is the resolver for the permission field. func (r *sessionResolver) Permission(ctx context.Context, obj *types.Session, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) } // TotalCount is the resolver for the totalCount field. func (r *sessionConnectionResolver) TotalCount(ctx context.Context, obj *types.SessionConnection) (*int, error) { switch obj.Resolver.(type) { case *identityResolver: count, err := r.iam.AccountService.CountSessions(ctx, obj.ParentID) if err != nil { r.logger.ErrorCtx(ctx, "cannot count sessions", log.Error(err)) return nil, gqlutils.Internal(ctx) } return &count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) return nil, gqlutils.Internal(ctx) } // Connector returns schema.ConnectorResolver implementation. func (r *Resolver) Connector() schema.ConnectorResolver { return &connectorResolver{r} } // Identity returns schema.IdentityResolver implementation. func (r *Resolver) Identity() schema.IdentityResolver { return &identityResolver{r} } // Invitation returns schema.InvitationResolver implementation. func (r *Resolver) Invitation() schema.InvitationResolver { return &invitationResolver{r} } // InvitationConnection returns schema.InvitationConnectionResolver implementation. func (r *Resolver) InvitationConnection() schema.InvitationConnectionResolver { return &invitationConnectionResolver{r} } // Membership returns schema.MembershipResolver implementation. func (r *Resolver) Membership() schema.MembershipResolver { return &membershipResolver{r} } // MembershipConnection returns schema.MembershipConnectionResolver implementation. func (r *Resolver) MembershipConnection() schema.MembershipConnectionResolver { return &membershipConnectionResolver{r} } // MembershipProfile returns schema.MembershipProfileResolver implementation. func (r *Resolver) MembershipProfile() schema.MembershipProfileResolver { return &membershipProfileResolver{r} } // Mutation returns schema.MutationResolver implementation. func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} } // Organization returns schema.OrganizationResolver implementation. func (r *Resolver) Organization() schema.OrganizationResolver { return &organizationResolver{r} } // PersonalAPIKey returns schema.PersonalAPIKeyResolver implementation. func (r *Resolver) PersonalAPIKey() schema.PersonalAPIKeyResolver { return &personalAPIKeyResolver{r} } // PersonalAPIKeyConnection returns schema.PersonalAPIKeyConnectionResolver implementation. func (r *Resolver) PersonalAPIKeyConnection() schema.PersonalAPIKeyConnectionResolver { return &personalAPIKeyConnectionResolver{r} } // Query returns schema.QueryResolver implementation. func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} } // SAMLConfiguration returns schema.SAMLConfigurationResolver implementation. func (r *Resolver) SAMLConfiguration() schema.SAMLConfigurationResolver { return &sAMLConfigurationResolver{r} } // SAMLConfigurationConnection returns schema.SAMLConfigurationConnectionResolver implementation. func (r *Resolver) SAMLConfigurationConnection() schema.SAMLConfigurationConnectionResolver { return &sAMLConfigurationConnectionResolver{r} } // SCIMBridge returns schema.SCIMBridgeResolver implementation. func (r *Resolver) SCIMBridge() schema.SCIMBridgeResolver { return &sCIMBridgeResolver{r} } // SCIMConfiguration returns schema.SCIMConfigurationResolver implementation. func (r *Resolver) SCIMConfiguration() schema.SCIMConfigurationResolver { return &sCIMConfigurationResolver{r} } // SCIMEvent returns schema.SCIMEventResolver implementation. func (r *Resolver) SCIMEvent() schema.SCIMEventResolver { return &sCIMEventResolver{r} } // SCIMEventConnection returns schema.SCIMEventConnectionResolver implementation. func (r *Resolver) SCIMEventConnection() schema.SCIMEventConnectionResolver { return &sCIMEventConnectionResolver{r} } // Session returns schema.SessionResolver implementation. func (r *Resolver) Session() schema.SessionResolver { return &sessionResolver{r} } // SessionConnection returns schema.SessionConnectionResolver implementation. func (r *Resolver) SessionConnection() schema.SessionConnectionResolver { return &sessionConnectionResolver{r} } type connectorResolver struct{ *Resolver } type identityResolver struct{ *Resolver } type invitationResolver struct{ *Resolver } type invitationConnectionResolver struct{ *Resolver } type membershipResolver struct{ *Resolver } type membershipConnectionResolver struct{ *Resolver } type membershipProfileResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type organizationResolver struct{ *Resolver } type personalAPIKeyResolver struct{ *Resolver } type personalAPIKeyConnectionResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type sAMLConfigurationResolver struct{ *Resolver } type sAMLConfigurationConnectionResolver struct{ *Resolver } type sCIMBridgeResolver struct{ *Resolver } type sCIMConfigurationResolver struct{ *Resolver } type sCIMEventResolver struct{ *Resolver } type sCIMEventConnectionResolver struct{ *Resolver } type sessionResolver struct{ *Resolver } type sessionConnectionResolver struct{ *Resolver }