2082 lines
70 KiB
Go
2082 lines
70 KiB
Go
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, authz.WithSkipAssumptionCheck()); 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)
|
|
}
|
|
|
|
// Identity is the resolver for the identity field.
|
|
func (r *membershipProfileResolver) Identity(ctx context.Context, obj *types.MembershipProfile) (*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.GetIdentity(ctx, obj.Identity.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
|
|
}
|
|
|
|
// Organization is the resolver for the organization field.
|
|
func (r *membershipProfileResolver) Organization(ctx context.Context, obj *types.MembershipProfile) (*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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
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 errMembershipInactive *iam.ErrMembershipInactive
|
|
|
|
if errors.As(err, &errMembershipNotFound) || errors.As(err, &errMembershipInactive) {
|
|
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
|
|
}
|
|
|
|
// 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, input.Continue)
|
|
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, membership, 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),
|
|
MembershipEdge: types.NewMembershipEdge(membership, coredata.MembershipOrderFieldCreatedAt),
|
|
}, 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
|
|
}
|
|
|
|
// UpdateProfile is the resolver for the updateProfile field.
|
|
func (r *mutationResolver) UpdateProfile(ctx context.Context, input types.UpdateProfileInput) (*types.UpdateProfilePayload, error) {
|
|
if err := r.authorize(ctx, input.ID, iam.ActionMembershipProfileUpdate); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
profile, err := r.iam.OrganizationService.UpdateProfile(
|
|
ctx,
|
|
&iam.UpdateProfileRequest{
|
|
ID: input.ID,
|
|
FullName: input.FullName,
|
|
AdditionalEmailAddresses: input.AdditionalEmailAddresses,
|
|
Kind: input.Kind,
|
|
Position: gqlutils.UnwrapOmittable(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.UpdateProfilePayload{
|
|
Profile: types.NewMembershipProfile(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
|
|
}
|
|
|
|
// 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.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.NewMembershipProfile(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
|
|
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 }
|