Add ISC license headers to all .go, .ts, .tsx, and .sql files using each file's git history to determine the correct copyright year or year range. Trademarked icons (brand logos, vendor logos, compliance framework logos) are excluded. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2116 lines
71 KiB
Go
2116 lines
71 KiB
Go
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
//
|
|
// Permission to use, copy, modify, and/or distribute this software for any
|
|
// purpose with or without fee is hereby granted, provided that the above
|
|
// copyright notice and this permission notice appear in all copies.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
// PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
package 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.87
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"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"
|
|
)
|
|
|
|
// Organization is the resolver for the organization field.
|
|
func (r *auditLogEntryResolver) Organization(ctx context.Context, obj *types.AuditLogEntry) (*types.Organization, error) {
|
|
return obj.Organization, nil
|
|
}
|
|
|
|
// Permission is the resolver for the permission field.
|
|
func (r *auditLogEntryResolver) Permission(ctx context.Context, obj *types.AuditLogEntry, action string) (bool, error) {
|
|
return r.Resolver.Permission(ctx, obj, action)
|
|
}
|
|
|
|
// TotalCount is the resolver for the totalCount field.
|
|
func (r *auditLogEntryConnectionResolver) TotalCount(ctx context.Context, obj *types.AuditLogEntryConnection) (int, error) {
|
|
filter := coredata.NewAuditLogEntryFilter()
|
|
if obj.Filter != nil {
|
|
filter = obj.Filter
|
|
}
|
|
|
|
count, err := r.iam.OrganizationService.CountAuditLogEntries(ctx, obj.ParentID, filter)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot count audit log entries", log.Error(err))
|
|
return 0, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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 *types.ProfileOrderBy, filter *types.ProfileFilter) (*types.ProfileConnection, error) {
|
|
if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileList, authz.WithSkipAssumptionCheck()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filters := coredata.NewMembershipProfileFilter(nil).WithMembership()
|
|
if filter != nil {
|
|
filters = coredata.NewMembershipProfileFilter(filter.ExcludeContractEnded).WithMembership()
|
|
if filter.State != nil {
|
|
filters.WithState(*filter.State)
|
|
}
|
|
}
|
|
|
|
if gqlutils.OnlyTotalCountSelected(ctx) {
|
|
return &types.ProfileConnection{
|
|
Resolver: r,
|
|
ParentID: obj.ID,
|
|
Filters: filters,
|
|
}, 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, filters)
|
|
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, filters), 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
|
|
}
|
|
|
|
// SsoLoginURL is the resolver for the ssoLoginURL field.
|
|
func (r *identityResolver) SsoLoginURL(ctx context.Context, obj *types.Identity) (*string, error) {
|
|
if err := r.authorize(ctx, obj.ID, iam.ActionIdentityGet); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
identity := authn.IdentityFromContext(ctx)
|
|
|
|
count, err := r.iam.AccountService.CountSAMLConfigurationsForEmail(ctx, identity.EmailAddress)
|
|
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, identity.EmailAddress)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot list SAML configurations for email", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
if len(samlConfigs) == 0 {
|
|
r.logger.ErrorCtx(ctx, "cannot find SAML config")
|
|
return nil, gqlutils.NotFoundf(ctx, "cannot find SAML config")
|
|
}
|
|
samlConfig := samlConfigs[0]
|
|
|
|
loginURL := r.SSOLoginURL(samlConfig.ID)
|
|
return &loginURL, 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
w := gqlutils.HTTPResponseWriterFromContext(ctx)
|
|
r.sessionCookie.Set(w, session)
|
|
|
|
if input.OrganizationID != nil {
|
|
var err error
|
|
_, _, 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 errUserInactive *iam.ErrUserInactive
|
|
|
|
if errors.As(err, &errMembershipNotFound) || errors.As(err, &errUserInactive) {
|
|
return nil, gqlutils.Forbiddenf(ctx, "forbidden")
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot assume organization", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// ActivateAccount is the resolver for the activateAccount field.
|
|
func (r *mutationResolver) ActivateAccount(ctx context.Context, input types.ActivateAccountInput) (*types.ActivateAccountPayload, error) {
|
|
session := authn.SessionFromContext(ctx)
|
|
|
|
if session != nil {
|
|
// Sign out any other account before activating a new one
|
|
err := r.iam.SessionService.CloseSession(ctx, session.ID)
|
|
if err != nil {
|
|
var ErrSessionNotFound *iam.ErrSessionNotFound
|
|
if !errors.As(err, &ErrSessionNotFound) {
|
|
r.logger.ErrorCtx(ctx, "cannot close session", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
}
|
|
|
|
w := gqlutils.HTTPResponseWriterFromContext(ctx)
|
|
r.sessionCookie.Clear(w)
|
|
}
|
|
|
|
identity, user, err := r.iam.AuthService.ActivateAccount(
|
|
ctx,
|
|
&iam.ActivateAccountRequest{
|
|
InvitationToken: input.Token,
|
|
},
|
|
)
|
|
if err != nil {
|
|
var (
|
|
errInvalidToken *iam.ErrInvalidToken
|
|
errInvitationNotFound *iam.ErrInvitationNotFound
|
|
errInvitationExpired *iam.ErrInvitationExpired
|
|
|
|
isInvalidErr = errors.As(err, &errInvalidToken) ||
|
|
errors.As(err, &errInvitationNotFound) ||
|
|
errors.As(err, &errInvitationExpired)
|
|
)
|
|
|
|
if isInvalidErr {
|
|
return nil, gqlutils.Invalid(ctx, err)
|
|
}
|
|
|
|
if _, ok := errors.AsType[*iam.ErrInvitationAlreadyAccepted](err); ok {
|
|
return nil, gqlutils.AccountAlreadyActivated(ctx, err)
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot activate account from invitation", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
var ssoLoginURL *string
|
|
samlConfigs, err := r.iam.AccountService.ListSAMLConfigurationsForEmail(ctx, user.EmailAddress)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot list saml configurations", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
for _, samlConfig := range samlConfigs {
|
|
if samlConfig.OrganizationID != user.OrganizationID {
|
|
continue
|
|
}
|
|
|
|
ssoLoginURL = new(r.SSOLoginURL(samlConfig.ID))
|
|
}
|
|
|
|
if ssoLoginURL != nil {
|
|
return &types.ActivateAccountPayload{
|
|
CreatePasswordToken: nil,
|
|
SsoLoginURL: ssoLoginURL,
|
|
Profile: types.NewProfile(user),
|
|
}, nil
|
|
}
|
|
|
|
var createPasswordToken *string
|
|
if identity.HashedPassword == nil {
|
|
token, err := r.iam.AuthService.GetResetPasswordToken(ctx, identity.EmailAddress)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot generate password create token", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
createPasswordToken = &token
|
|
}
|
|
|
|
return &types.ActivateAccountPayload{
|
|
CreatePasswordToken: createPasswordToken,
|
|
SsoLoginURL: nil,
|
|
Profile: types.NewProfile(user),
|
|
}, 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, input.Continue)
|
|
if err != nil {
|
|
var (
|
|
errMembershipNotFound *iam.ErrMembershipNotFound
|
|
errPasswordAuthenticationRequired *iam.ErrPasswordAuthenticationRequired
|
|
errSAMLAuthenticationRequired *iam.ErrSAMLAuthenticationRequired
|
|
)
|
|
|
|
switch {
|
|
case errors.As(err, &errMembershipNotFound):
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
|
|
case errors.As(err, &errPasswordAuthenticationRequired):
|
|
return &types.AssumeOrganizationSessionPayload{
|
|
Result: types.PasswordRequired{
|
|
Reason: types.ReauthenticationReason(errPasswordAuthenticationRequired.Reason),
|
|
},
|
|
}, nil
|
|
|
|
case errors.As(err, &errSAMLAuthenticationRequired):
|
|
return &types.AssumeOrganizationSessionPayload{
|
|
Result: types.SAMLAuthenticationRequired{
|
|
Reason: types.ReauthenticationReason(errSAMLAuthenticationRequired.Reason),
|
|
},
|
|
}, 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, profile, err := r.iam.OrganizationService.CreateOrganization(
|
|
ctx,
|
|
identity.ID,
|
|
&iam.CreateOrganizationRequest{
|
|
Name: input.Name,
|
|
LogoFile: logoFile,
|
|
HorizontalLogoFile: horizontalLogoFile,
|
|
},
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
|
return nil, gqlutils.Conflict(ctx, err)
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot create organization", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return &types.CreateOrganizationPayload{
|
|
Organization: types.NewOrganization(organization),
|
|
Profile: types.NewProfile(profile),
|
|
}, 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"))
|
|
}
|
|
|
|
// CreateUser is the resolver for the createUser field.
|
|
func (r *mutationResolver) CreateUser(ctx context.Context, input types.CreateUserInput) (*types.CreateUserPayload, error) {
|
|
if err := r.authorize(ctx, input.OrganizationID, iam.ActionMembershipProfileCreate); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
profile, err := r.iam.OrganizationService.CreateUser(
|
|
ctx,
|
|
&iam.CreateUserRequest{
|
|
OrganizationID: input.OrganizationID,
|
|
EmailAddress: input.EmailAddress,
|
|
Role: input.Role,
|
|
FullName: input.FullName,
|
|
AdditionalEmailAddresses: input.AdditionalEmailAddresses,
|
|
Kind: input.Kind,
|
|
Position: input.Position,
|
|
ContractStartDate: gqlutils.UnwrapOmittable(input.ContractStartDate),
|
|
ContractEndDate: gqlutils.UnwrapOmittable(input.ContractEndDate),
|
|
},
|
|
)
|
|
if err != nil {
|
|
var errAlreadyExists *iam.ErrUserAlreadyExists
|
|
if errors.As(err, &errAlreadyExists) {
|
|
return nil, gqlutils.Conflict(ctx, err)
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot create user", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return &types.CreateUserPayload{
|
|
ProfileEdge: types.NewProfileEdge(profile, coredata.MembershipProfileOrderFieldCreatedAt),
|
|
}, nil
|
|
}
|
|
|
|
// InviteUser is the resolver for the inviteUser field.
|
|
func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) {
|
|
if err := r.authorize(ctx, input.ProfileID, iam.ActionInvitationCreate); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
invitation, err := r.iam.OrganizationService.InviteUser(
|
|
ctx,
|
|
&iam.CreateInvitationRequest{
|
|
OrganizationID: input.OrganizationID,
|
|
ProfileID: input.ProfileID,
|
|
},
|
|
)
|
|
if err != nil {
|
|
var errOrganizationNotFound *iam.ErrOrganizationNotFound
|
|
var errUserAlreadyExists *iam.ErrUserAlreadyExists
|
|
|
|
if errors.As(err, &errOrganizationNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
if errors.As(err, &errUserAlreadyExists) {
|
|
return nil, gqlutils.Conflict(ctx, err)
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot invite user", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return &types.InviteUserPayload{
|
|
InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt),
|
|
}, nil
|
|
}
|
|
|
|
// DeactivateUser is the resolver for the deactivateUser field.
|
|
func (r *mutationResolver) DeactivateUser(ctx context.Context, input types.DeactivateUserInput) (*types.DeactivateUserPayload, error) {
|
|
if err := r.authorize(ctx, input.ProfileID, iam.ActionMembershipProfileDeactivate); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err := r.iam.OrganizationService.UpdateUserState(
|
|
ctx,
|
|
input.ProfileID,
|
|
coredata.ProfileStateInactive,
|
|
)
|
|
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot deactivate profile", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return &types.DeactivateUserPayload{
|
|
Success: true,
|
|
}, nil
|
|
}
|
|
|
|
// UpdateUser is the resolver for the updateUser field.
|
|
func (r *mutationResolver) UpdateUser(ctx context.Context, input types.UpdateUserInput) (*types.UpdateUserPayload, error) {
|
|
if err := r.authorize(ctx, input.ID, iam.ActionMembershipProfileUpdate); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
profile, err := r.iam.OrganizationService.UpdateUser(
|
|
ctx,
|
|
&iam.UpdateUserRequest{
|
|
ID: input.ID,
|
|
FullName: input.FullName,
|
|
AdditionalEmailAddresses: input.AdditionalEmailAddresses,
|
|
Kind: input.Kind,
|
|
Position: input.Position,
|
|
ContractStartDate: gqlutils.UnwrapOmittable(input.ContractStartDate),
|
|
ContractEndDate: gqlutils.UnwrapOmittable(input.ContractEndDate),
|
|
},
|
|
)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot update profile", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return &types.UpdateUserPayload{
|
|
Profile: types.NewProfile(profile),
|
|
}, 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
|
|
}
|
|
|
|
// RemoveUser is the resolver for the removeUser field.
|
|
func (r *mutationResolver) RemoveUser(ctx context.Context, input types.RemoveUserInput) (*types.RemoveUserPayload, error) {
|
|
if err := r.authorize(ctx, input.ProfileID, iam.ActionMembershipProfileDelete); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err := r.iam.OrganizationService.RemoveUser(ctx, input.OrganizationID, input.ProfileID)
|
|
if err != nil {
|
|
var errManagedBySCIM *iam.ErrUserManagedBySCIM
|
|
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 user from organization", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return &types.RemoveUserPayload{DeletedProfileID: input.ProfileID}, 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
|
|
}
|
|
|
|
// 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 *types.ProfileOrderBy) (*types.ProfileConnection, error) {
|
|
if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileList); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filter := coredata.NewMembershipProfileFilter(nil).WithMembership()
|
|
|
|
if gqlutils.OnlyTotalCountSelected(ctx) {
|
|
return &types.ProfileConnection{
|
|
Resolver: r,
|
|
ParentID: obj.ID,
|
|
Filters: filter,
|
|
}, 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, filter), 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
|
|
}
|
|
|
|
// AuditLogEntries is the resolver for the auditLogEntries field.
|
|
func (r *organizationResolver) AuditLogEntries(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AuditLogEntryOrderBy, filter *types.AuditLogEntryFilter) (*types.AuditLogEntryConnection, error) {
|
|
if err := r.authorize(ctx, obj.ID, iam.ActionAuditLogEntryList); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pageOrderBy := page.OrderBy[coredata.AuditLogEntryOrderField]{
|
|
Field: coredata.AuditLogEntryOrderFieldCreatedAt,
|
|
Direction: page.OrderDirectionDesc,
|
|
}
|
|
if orderBy != nil {
|
|
pageOrderBy = page.OrderBy[coredata.AuditLogEntryOrderField]{
|
|
Field: orderBy.Field,
|
|
Direction: orderBy.Direction,
|
|
}
|
|
}
|
|
|
|
c := cursor.NewCursor(first, after, last, before, pageOrderBy)
|
|
|
|
coredataFilter := coredata.NewAuditLogEntryFilter()
|
|
if filter != nil {
|
|
if filter.Action != nil {
|
|
coredataFilter.WithAction(*filter.Action)
|
|
}
|
|
if filter.ActorID != nil {
|
|
coredataFilter.WithActorID(*filter.ActorID)
|
|
}
|
|
if filter.ResourceType != nil {
|
|
coredataFilter.WithResourceType(*filter.ResourceType)
|
|
}
|
|
if filter.ResourceID != nil {
|
|
coredataFilter.WithResourceID(*filter.ResourceID)
|
|
}
|
|
}
|
|
|
|
p, err := r.iam.OrganizationService.ListAuditLogEntries(ctx, obj.ID, c, coredataFilter)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot list audit log entries", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return types.NewAuditLogEntryConnection(p, r, obj.ID, coredataFilter), 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)
|
|
|
|
profile, err := r.iam.OrganizationService.GetProfileForIdentityAndOrganization(ctx, identity.ID, obj.ID)
|
|
if err != nil {
|
|
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.NewProfile(profile), 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)
|
|
}
|
|
|
|
// Identity is the resolver for the identity field.
|
|
func (r *profileResolver) Identity(ctx context.Context, obj *types.Profile) (*types.Identity, error) {
|
|
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) {
|
|
if err := r.authorize(ctx, obj.Organization.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) {
|
|
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
|
|
}
|
|
|
|
// PendingInvitations is the resolver for the pendingInvitations field.
|
|
func (r *profileResolver) PendingInvitations(ctx context.Context, obj *types.Profile, 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Permission is the resolver for the permission field.
|
|
func (r *profileResolver) Permission(ctx context.Context, obj *types.Profile, action string) (bool, error) {
|
|
return r.Resolver.Permission(ctx, obj, action)
|
|
}
|
|
|
|
// TotalCount is the resolver for the totalCount field.
|
|
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, obj.Filters)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot count profiles", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
return &count, nil
|
|
case *organizationResolver:
|
|
count, err := r.iam.OrganizationService.CountProfiles(ctx, obj.ParentID, obj.Filters)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot count profiles", 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.MembershipProfileEntityType:
|
|
action = iam.ActionMembershipGet
|
|
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
|
profile, err := r.iam.OrganizationService.GetProfile(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return types.NewProfile(profile), 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
|
|
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)
|
|
)
|
|
|
|
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.SSOLoginURL(samlConfig.ID)
|
|
|
|
return &loginURL, nil
|
|
}
|
|
|
|
// OidcProviders is the resolver for the oidcProviders field.
|
|
func (r *queryResolver) OidcProviders(ctx context.Context) ([]*types.OIDCProviderInfo, error) {
|
|
providers := r.iam.OIDCService.EnabledProviders()
|
|
result := make([]*types.OIDCProviderInfo, 0, len(providers))
|
|
|
|
for _, p := range providers {
|
|
result = append(result, &types.OIDCProviderInfo{
|
|
Name: strings.ToLower(p.String()),
|
|
LoginURL: r.baseURL.WithPath("/api/connect/v1/oidc/" + strings.ToLower(p.String()) + "/login").MustString(),
|
|
})
|
|
}
|
|
|
|
return result, 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// AuditLogEntry returns schema.AuditLogEntryResolver implementation.
|
|
func (r *Resolver) AuditLogEntry() schema.AuditLogEntryResolver { return &auditLogEntryResolver{r} }
|
|
|
|
// AuditLogEntryConnection returns schema.AuditLogEntryConnectionResolver implementation.
|
|
func (r *Resolver) AuditLogEntryConnection() schema.AuditLogEntryConnectionResolver {
|
|
return &auditLogEntryConnectionResolver{r}
|
|
}
|
|
|
|
// 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} }
|
|
|
|
// Membership returns schema.MembershipResolver implementation.
|
|
func (r *Resolver) Membership() schema.MembershipResolver { return &membershipResolver{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}
|
|
}
|
|
|
|
// Profile returns schema.ProfileResolver implementation.
|
|
func (r *Resolver) Profile() schema.ProfileResolver { return &profileResolver{r} }
|
|
|
|
// ProfileConnection returns schema.ProfileConnectionResolver implementation.
|
|
func (r *Resolver) ProfileConnection() schema.ProfileConnectionResolver {
|
|
return &profileConnectionResolver{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 auditLogEntryResolver struct{ *Resolver }
|
|
type auditLogEntryConnectionResolver struct{ *Resolver }
|
|
type connectorResolver struct{ *Resolver }
|
|
type identityResolver struct{ *Resolver }
|
|
type invitationResolver struct{ *Resolver }
|
|
type membershipResolver struct{ *Resolver }
|
|
type mutationResolver struct{ *Resolver }
|
|
type organizationResolver struct{ *Resolver }
|
|
type personalAPIKeyResolver struct{ *Resolver }
|
|
type personalAPIKeyConnectionResolver struct{ *Resolver }
|
|
type profileResolver struct{ *Resolver }
|
|
type profileConnectionResolver 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 }
|