Trust GraphQL and MCP still exposed presigned URL strings for trust-center logos while console and connect already serve stable File.downloadUrl paths. Phase 1 migrates the seven public logo fields on trust GraphQL and the trust-center file references on MCP to the shared File type; trust GraphQL NDA stays on fileUrl for a follow-up. Trust resolvers load public files through filemanager and map them with types.NewFile. The trust app Relay queries and components now read logo.downloadUrl. MCP specification, resolvers, and helpers are updated in sync, including NDA on MCP where callers already have file access. filemanager is split into focused files and its URL surface is narrowed to GenerateFileURL(file) for stable app URLs and GeneratePresignedURL for S3 redirects. GetPublicFile remains the DB entry point when only a file ID is known. Add trust and MCP e2e coverage for public logo download URLs. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
369 lines
12 KiB
Go
369 lines
12 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.90
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"go.gearno.de/kit/log"
|
|
"go.probo.inc/probo/pkg/coredata"
|
|
"go.probo.inc/probo/pkg/iam"
|
|
"go.probo.inc/probo/pkg/iam/scim/bridge/provider/googleworkspace"
|
|
"go.probo.inc/probo/pkg/iam/scim/bridge/provider/microsoft365"
|
|
"go.probo.inc/probo/pkg/page"
|
|
"go.probo.inc/probo/pkg/server/api/authn"
|
|
"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"
|
|
)
|
|
|
|
// 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"))
|
|
}
|
|
|
|
// Logo is the resolver for the logo field.
|
|
func (r *organizationResolver) Logo(ctx context.Context, obj *types.Organization) (*types.File, error) {
|
|
file, err := r.iam.OrganizationService.LogoFile(ctx, obj.ID)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot load logo file", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
if file == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return types.NewFile(file, r.fileManager), nil
|
|
}
|
|
|
|
// HorizontalLogo is the resolver for the horizontalLogo field.
|
|
func (r *organizationResolver) HorizontalLogo(ctx context.Context, obj *types.Organization) (*types.File, error) {
|
|
file, err := r.iam.OrganizationService.HorizontalLogoFile(ctx, obj.ID)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot load horizontal logo file", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
if file == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return types.NewFile(file, r.fileManager), 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 {
|
|
if _, ok := errors.AsType[*iam.ErrNoSCIMConfigurationFound](err); ok {
|
|
return nil, nil
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot get scim configuration", log.Error(err))
|
|
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return types.NewSCIMConfiguration(config), nil
|
|
}
|
|
|
|
// ScimBridgeTypes is the resolver for the scimBridgeTypes field.
|
|
func (r *organizationResolver) ScimBridgeTypes(ctx context.Context, obj *types.Organization) ([]*types.SCIMBridgeTypeInfo, error) {
|
|
return []*types.SCIMBridgeTypeInfo{
|
|
{
|
|
Type: coredata.SCIMBridgeTypeGoogleWorkspace,
|
|
Oauth2Scopes: googleworkspace.OAuth2Scopes,
|
|
},
|
|
{
|
|
Type: coredata.SCIMBridgeTypeMicrosoft365,
|
|
Oauth2Scopes: microsoft365.OAuth2Scopes,
|
|
},
|
|
}, 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 {
|
|
if _, ok := errors.AsType[*iam.ErrProfileNotFound](err); ok {
|
|
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)
|
|
}
|
|
|
|
// Organization returns schema.OrganizationResolver implementation.
|
|
func (r *Resolver) Organization() schema.OrganizationResolver { return &organizationResolver{r} }
|
|
|
|
type organizationResolver struct{ *Resolver }
|