Files
probo/pkg/iam/organization_service.go
Émile Ré 38bf027dd6 Replug new organization page
Signed-off-by: Émile Ré <nemile.re@gmail.com>
2026-01-17 10:10:26 -08:00

1154 lines
28 KiB
Go

// Copyright (c) 2025 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 iam
import (
"context"
"errors"
"fmt"
"io"
"time"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/packages/emails"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/validator"
)
type (
OrganizationService struct {
*Service
}
InvitationTokenData struct {
InvitationID gid.GID `json:"invitation_id"`
}
UploadedFile struct {
Content io.Reader
Filename string
Size int64
ContentType string
}
CreateOrganizationRequest struct {
Name string
LogoFile *UploadedFile
HorizontalLogoFile *UploadedFile
}
UpdateOrganizationRequest struct {
Name *string
LogoFile **UploadedFile
HorizontalLogoFile **UploadedFile
}
CreateSAMLConfigurationRequest struct {
EmailDomain string
IdPEntityID string
IdPSsoURL string
IdPCertificate string
AttributeEmail *string
AttributeFirstname *string
AttributeLastname *string
AttributeRole *string
AutoSignupEnabled bool
}
UpdateSAMLConfigurationRequest struct {
ID gid.GID
EnforcementPolicy *coredata.SAMLEnforcementPolicy
IdPEntityID *string
IdPSsoURL *string
IdPCertificate *string
AttributeEmail *string
AttributeFirstname *string
AttributeLastname *string
AttributeRole *string
AutoSignupEnabled *bool
}
)
const (
TokenTypeAPIKey = "api_key"
DefaultAttributeEmail = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
DefaultAttributeFirstname = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
DefaultAttributeLastname = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
DefaultAttributeRole = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role"
)
func (req CreateOrganizationRequest) Validate() error {
v := validator.New()
fv := filevalidation.NewValidator(filevalidation.WithCategories(filevalidation.CategoryImage))
if req.LogoFile != nil {
err := fv.Validate(req.LogoFile.Filename, req.LogoFile.ContentType, req.LogoFile.Size)
if err != nil {
return fmt.Errorf("invalid logo file: %w", err)
}
}
if req.HorizontalLogoFile != nil {
err := fv.Validate(req.HorizontalLogoFile.Filename, req.HorizontalLogoFile.ContentType, req.HorizontalLogoFile.Size)
if err != nil {
return fmt.Errorf("invalid horizontal logo file: %w", err)
}
}
v.Check(req.Name, "name", validator.Required(), validator.SafeTextNoNewLine(255))
return v.Error()
}
func (req UpdateOrganizationRequest) Validate() error {
v := validator.New()
fv := filevalidation.NewValidator(filevalidation.WithCategories(filevalidation.CategoryImage))
if req.LogoFile != nil && *req.LogoFile != nil {
err := fv.Validate((**req.LogoFile).Filename, (**req.LogoFile).ContentType, (**req.LogoFile).Size)
if err != nil {
return fmt.Errorf("invalid logo file: %w", err)
}
}
if req.HorizontalLogoFile != nil && *req.HorizontalLogoFile != nil {
err := fv.Validate((**req.HorizontalLogoFile).Filename, (**req.HorizontalLogoFile).ContentType, (**req.HorizontalLogoFile).Size)
if err != nil {
return fmt.Errorf("invalid horizontal logo file: %w", err)
}
}
v.Check(req.Name, "name", validator.Required(), validator.SafeTextNoNewLine(255))
return v.Error()
}
func NewOrganizationService(svc *Service) *OrganizationService {
return &OrganizationService{Service: svc}
}
func (s *OrganizationService) RemoveMember(
ctx context.Context,
organizationID gid.GID,
membershipID gid.GID,
) error {
scope := coredata.NewScopeFromObjectID(organizationID)
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
membership := coredata.Membership{}
if err := membership.LoadByID(ctx, tx, scope, membershipID); err != nil {
if err == coredata.ErrResourceNotFound {
return NewMembershipNotFoundError(membershipID)
}
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.OrganizationID != organizationID {
return NewMembershipNotFoundError(membership.ID)
}
err := membership.Delete(ctx, tx, scope, membershipID)
if err != nil {
return fmt.Errorf("cannot delete membership: %w", err)
}
return nil
},
)
}
func (s *OrganizationService) DeleteInvitation(
ctx context.Context,
organizationID gid.GID,
invitationID gid.GID,
) error {
scope := coredata.NewScopeFromObjectID(organizationID)
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
invitation := coredata.Invitation{}
err := invitation.LoadByID(ctx, tx, scope, invitationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return NewInvitationNotFoundError(invitationID)
}
return fmt.Errorf("cannot load invitation: %w", err)
}
if invitation.Status != coredata.InvitationStatusPending {
return NewInvitationNotPendingError(invitationID)
}
err = invitation.Delete(ctx, tx, scope, invitationID)
if err != nil {
return fmt.Errorf("cannot delete invitation: %w", err)
}
return nil
},
)
}
func (s *OrganizationService) ListInvitations(
ctx context.Context,
organizationID gid.GID,
cursor *page.Cursor[coredata.InvitationOrderField],
filter *coredata.InvitationFilter,
) (*page.Page[*coredata.Invitation, coredata.InvitationOrderField], error) {
var (
invitations coredata.Invitations
scope = coredata.NewScopeFromObjectID(organizationID)
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
err := invitations.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor, filter)
if err != nil {
return fmt.Errorf("cannot load invitations: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(invitations, cursor), nil
}
func (s *OrganizationService) CountInvitations(
ctx context.Context,
organizationID gid.GID,
filter *coredata.InvitationFilter,
) (int, error) {
var (
count int
scope = coredata.NewScopeFromObjectID(organizationID)
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) (err error) {
invitations := coredata.Invitations{}
count, err = invitations.CountByOrganizationID(ctx, conn, scope, organizationID, filter)
if err != nil {
return fmt.Errorf("cannot count invitations: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *OrganizationService) InviteMember(
ctx context.Context,
organizationID gid.GID,
emailAddress mail.Addr,
fullName string,
role coredata.MembershipRole,
) (*coredata.Invitation, error) {
var (
scope = coredata.NewScopeFromObjectID(organizationID)
now = time.Now()
invitation = &coredata.Invitation{
ID: gid.New(organizationID.TenantID(), coredata.InvitationEntityType),
OrganizationID: organizationID,
Email: emailAddress,
FullName: fullName,
Role: role,
Status: coredata.InvitationStatusPending,
ExpiresAt: now.Add(s.invitationTokenValidity),
CreatedAt: now,
}
)
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
organization := coredata.Organization{}
err := organization.LoadByID(ctx, tx, scope, organizationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return NewOrganizationNotFoundError(organizationID)
}
return fmt.Errorf("cannot load organization: %w", err)
}
user := &coredata.User{}
err = user.LoadByEmail(ctx, tx, emailAddress)
if err != nil && err != coredata.ErrResourceNotFound {
return fmt.Errorf("cannot load user: %w", err)
}
userExists := user.ID != gid.Nil
if userExists {
membership := &coredata.Membership{}
err = membership.LoadByUserAndOrg(ctx, tx, scope, user.ID, organizationID)
if err != nil && err != coredata.ErrResourceNotFound {
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.ID != gid.Nil {
return NewMembershipAlreadyExistsError(user.ID, organizationID)
}
}
err = invitation.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert invitation: %w", err)
}
invitationToken, err := statelesstoken.NewToken(
s.tokenSecret,
TokenTypeOrganizationInvitation,
s.invitationTokenValidity,
InvitationTokenData{InvitationID: invitation.ID},
)
if err != nil {
return fmt.Errorf("cannot generate invitation token: %w", err)
}
baseurl, err := baseurl.Parse(s.baseURL)
if err != nil {
return fmt.Errorf("cannot parse base URL: %w", err)
}
invitationURL := baseurl.WithPath("/auth/signup-from-invitation").WithQuery("token", invitationToken).MustString()
subject, textBody, htmlBody, err := emails.RenderInvitation(
s.baseURL,
invitation.FullName,
organization.Name,
invitationURL,
)
if err != nil {
return fmt.Errorf("cannot render invitation email: %w", err)
}
email := coredata.NewEmail(
invitation.FullName,
invitation.Email,
subject,
textBody,
htmlBody,
)
err = email.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert email: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return invitation, nil
}
func (s *OrganizationService) CreateOrganization(
ctx context.Context,
identityID gid.GID,
req *CreateOrganizationRequest,
) (*coredata.Organization, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
var (
tenantID = gid.NewTenantID()
organizationID = gid.New(tenantID, coredata.OrganizationEntityType)
now = time.Now()
organization = &coredata.Organization{
ID: organizationID,
TenantID: tenantID,
Name: req.Name,
CreatedAt: now,
UpdatedAt: now,
}
logoFile *coredata.File
horizontalLogoFile *coredata.File
scope = coredata.NewScope(tenantID)
)
if req.LogoFile != nil {
var (
fileID = gid.New(tenantID, coredata.FileEntityType)
objectKey = uuid.MustNewV7()
filename = req.LogoFile.Filename
contentType = req.LogoFile.ContentType
now = time.Now()
)
logoFile = &coredata.File{
ID: fileID,
BucketName: s.bucket,
MimeType: contentType,
FileName: filename,
FileKey: objectKey.String(),
FileSize: req.LogoFile.Size,
CreatedAt: now,
UpdatedAt: now,
}
fileSize, err := s.fm.PutFile(
ctx,
logoFile,
req.LogoFile.Content,
map[string]string{
"file-id": fileID.String(),
"organization-id": organization.ID.String(),
},
)
if err != nil {
return nil, fmt.Errorf("cannot upload logo file: %w", err)
}
logoFile.FileSize = fileSize
}
if req.HorizontalLogoFile != nil {
var (
fileID = gid.New(tenantID, coredata.FileEntityType)
objectKey = uuid.MustNewV7()
filename = req.LogoFile.Filename
contentType = req.LogoFile.ContentType
now = time.Now()
)
horizontalLogoFile = &coredata.File{
ID: fileID,
BucketName: s.bucket,
MimeType: contentType,
FileName: filename,
FileKey: objectKey.String(),
FileSize: req.LogoFile.Size,
CreatedAt: now,
UpdatedAt: now,
}
fileSize, err := s.fm.PutFile(
ctx,
horizontalLogoFile,
req.HorizontalLogoFile.Content,
map[string]string{
"file-id": fileID.String(),
"organization-id": organization.ID.String(),
},
)
if err != nil {
return nil, fmt.Errorf("cannot upload logo file: %w", err)
}
horizontalLogoFile.FileSize = fileSize
}
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
err := organization.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert organization: %w", err)
}
if logoFile != nil {
err := logoFile.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
}
if horizontalLogoFile != nil {
err := horizontalLogoFile.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
}
membership := &coredata.Membership{
ID: gid.New(tenantID, coredata.MembershipEntityType),
UserID: identityID,
OrganizationID: organizationID,
Role: coredata.MembershipRoleOwner,
CreatedAt: now,
UpdatedAt: now,
}
err = membership.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot create membership: %w", err)
}
return nil
},
)
if err != nil {
return nil, fmt.Errorf("cannot insert organization: %w", err)
}
return organization, nil
}
func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizationID gid.GID, req *UpdateOrganizationRequest) (*coredata.Organization, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
var (
now = time.Now()
logoFile *coredata.File
horizontalLogoFile *coredata.File
tenantID = organizationID.TenantID()
scope = coredata.NewScopeFromObjectID(organizationID)
organization = &coredata.Organization{}
)
// TODO: s3 upload happen before we validate the tenantID
if req.LogoFile != nil && *req.LogoFile != nil {
var (
fileID = gid.New(tenantID, coredata.FileEntityType)
objectKey = uuid.MustNewV7()
filename = (**req.LogoFile).Filename
contentType = (**req.LogoFile).ContentType
)
logoFile = &coredata.File{
ID: fileID,
BucketName: s.bucket,
MimeType: contentType,
FileName: filename,
FileKey: objectKey.String(),
FileSize: (**req.LogoFile).Size,
CreatedAt: now,
UpdatedAt: now,
}
fileSize, err := s.fm.PutFile(
ctx,
logoFile,
(**req.LogoFile).Content,
map[string]string{
"file-id": fileID.String(),
"organization-id": organizationID.String(),
},
)
if err != nil {
return nil, fmt.Errorf("cannot upload logo file: %w", err)
}
logoFile.FileSize = fileSize
}
if req.HorizontalLogoFile != nil && *req.HorizontalLogoFile != nil {
var (
fileID = gid.New(tenantID, coredata.FileEntityType)
objectKey = uuid.MustNewV7()
filename = (**req.HorizontalLogoFile).Filename
contentType = (**req.HorizontalLogoFile).ContentType
now = time.Now()
)
horizontalLogoFile = &coredata.File{
ID: fileID,
BucketName: s.bucket,
MimeType: contentType,
FileName: filename,
FileKey: objectKey.String(),
FileSize: (**req.HorizontalLogoFile).Size,
CreatedAt: now,
UpdatedAt: now,
}
fileSize, err := s.fm.PutFile(
ctx,
horizontalLogoFile,
(**req.HorizontalLogoFile).Content,
map[string]string{
"file-id": fileID.String(),
"organization-id": organizationID.String(),
},
)
if err != nil {
return nil, fmt.Errorf("cannot upload logo file: %w", err)
}
horizontalLogoFile.FileSize = fileSize
}
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
err := organization.LoadByID(ctx, tx, scope, organizationID)
if err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
organization.UpdatedAt = now
if req.Name != nil {
organization.Name = *req.Name
}
if req.LogoFile != nil {
if *req.LogoFile != nil {
err := logoFile.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
organization.LogoFileID = &logoFile.ID
} else {
organization.LogoFileID = nil
}
}
if req.HorizontalLogoFile != nil {
if *req.HorizontalLogoFile != nil {
err := horizontalLogoFile.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
organization.HorizontalLogoFileID = &horizontalLogoFile.ID
} else {
organization.HorizontalLogoFileID = nil
}
}
err = organization.Update(ctx, scope, tx)
if err != nil {
return fmt.Errorf("cannot update organization: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return organization, nil
}
func (s *OrganizationService) DeleteOrganization(ctx context.Context, organizationID gid.GID) error {
scope := coredata.NewScopeFromObjectID(organizationID)
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
organization := &coredata.Organization{}
err := organization.LoadByID(ctx, tx, scope, organizationID)
if err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
err = organization.Delete(ctx, tx, organizationID)
if err != nil {
return fmt.Errorf("cannot delete organization: %w", err)
}
return nil
},
)
}
func (s *OrganizationService) ListMembers(
ctx context.Context,
organizationID gid.GID,
cursor *page.Cursor[coredata.MembershipOrderField],
) (*page.Page[*coredata.Membership, coredata.MembershipOrderField], error) {
var (
scope = coredata.NewScopeFromObjectID(organizationID)
memberships = coredata.Memberships{}
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
err := memberships.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor)
if err != nil {
return fmt.Errorf("cannot load memberships: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(memberships, cursor), nil
}
func (s *OrganizationService) GetOrganizationForMembership(ctx context.Context, membershipID gid.GID) (*coredata.Organization, error) {
var (
scope = coredata.NewScopeFromObjectID(membershipID)
organization = &coredata.Organization{}
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
membership := &coredata.Membership{}
err := membership.LoadByID(ctx, conn, scope, membershipID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return NewMembershipNotFoundError(membershipID)
}
return fmt.Errorf("cannot load membership: %w", err)
}
err = organization.LoadByID(ctx, conn, scope, membership.OrganizationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return NewOrganizationNotFoundError(membership.OrganizationID)
}
return fmt.Errorf("cannot load organization: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return organization, nil
}
func (s *OrganizationService) GetOrganizationForInvitation(ctx context.Context, invitationID gid.GID) (*coredata.Organization, error) {
var (
scope = coredata.NewScopeFromObjectID(invitationID)
organization = &coredata.Organization{}
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
invitation := &coredata.Invitation{}
err := invitation.LoadByID(ctx, conn, scope, invitationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return NewInvitationNotFoundError(invitationID)
}
return fmt.Errorf("cannot load invitation: %w", err)
}
err = organization.LoadByID(ctx, conn, scope, invitation.OrganizationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return NewOrganizationNotFoundError(invitation.OrganizationID)
}
return fmt.Errorf("cannot load organization: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return organization, nil
}
func (s OrganizationService) GenerateLogoURL(
ctx context.Context,
organizationID gid.GID,
expiresIn time.Duration,
) (*string, error) {
var (
errNoLogoFile = errors.New("no logo file found")
scope = coredata.NewScopeFromObjectID(organizationID)
file = &coredata.File{}
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
organization := &coredata.Organization{}
if err := organization.LoadByID(ctx, conn, scope, organizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
if organization.LogoFileID == nil {
return errNoLogoFile
}
if err := file.LoadByID(ctx, conn, scope, *organization.LogoFileID); err != nil {
return fmt.Errorf("cannot load file: %w", err)
}
return nil
},
)
if err == errNoLogoFile {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("cannot generate logo URL: %w", err)
}
presignedURL, err := s.fm.GenerateFileUrl(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
return &presignedURL, nil
}
func (s OrganizationService) GenerateHorizontalLogoURL(
ctx context.Context,
organizationID gid.GID,
expiresIn time.Duration,
) (*string, error) {
var (
errNoLogoFile = errors.New("no logo file found")
scope = coredata.NewScopeFromObjectID(organizationID)
file = &coredata.File{}
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
organization := &coredata.Organization{}
if err := organization.LoadByID(ctx, conn, scope, organizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
if organization.HorizontalLogoFileID == nil {
return errNoLogoFile
}
if err := file.LoadByID(ctx, conn, scope, *organization.HorizontalLogoFileID); err != nil {
return fmt.Errorf("cannot load file: %w", err)
}
return nil
},
)
if err == errNoLogoFile {
return nil, nil
}
if err != nil {
return nil, err
}
presignedURL, err := s.fm.GenerateFileUrl(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
return &presignedURL, nil
}
func (s OrganizationService) DeleteSAMLConfiguration(
ctx context.Context,
organizationID gid.GID,
configID gid.GID,
) error {
scope := coredata.NewScopeFromObjectID(organizationID)
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
var config coredata.SAMLConfiguration
if err := config.LoadByID(ctx, tx, scope, configID); err != nil {
return fmt.Errorf("cannot load saml configuration: %w", err)
}
if config.OrganizationID != organizationID {
return NewSAMLConfigurationNotFoundError(configID)
}
if err := config.Delete(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot delete saml configuration: %w", err)
}
return nil
},
)
}
func (s OrganizationService) ListSAMLConfigurations(
ctx context.Context,
organizationID gid.GID, cursor *page.Cursor[coredata.SAMLConfigurationOrderField],
) (*page.Page[*coredata.SAMLConfiguration, coredata.SAMLConfigurationOrderField], error) {
var (
scope = coredata.NewScopeFromObjectID(organizationID)
samlConfigurations = coredata.SAMLConfigurations{}
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
err := samlConfigurations.LoadByOrganizationID(ctx, conn, scope, organizationID)
if err != nil {
return fmt.Errorf("cannot load saml configurations: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(samlConfigurations, cursor), nil
}
func (s OrganizationService) CountSAMLConfigurations(
ctx context.Context,
organizationID gid.GID,
) (int, error) {
var (
scope = coredata.NewScopeFromObjectID(organizationID)
count int
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) (err error) {
samlConfigurations := coredata.SAMLConfigurations{}
count, err = samlConfigurations.CountByOrganizationID(ctx, conn, scope, organizationID)
if err != nil {
return fmt.Errorf("cannot count saml configurations: %w", err)
}
return nil
},
)
return count, err
}
func (s OrganizationService) CreateSAMLConfiguration(
ctx context.Context,
organizationID gid.GID,
req *CreateSAMLConfigurationRequest,
) (*coredata.SAMLConfiguration, error) {
var (
now = time.Now()
scope = coredata.NewScopeFromObjectID(organizationID)
domainVerificationToken = uuid.MustNewV4().String()
config = &coredata.SAMLConfiguration{
ID: gid.New(scope.GetTenantID(), coredata.SAMLConfigurationEntityType),
OrganizationID: organizationID,
EnforcementPolicy: coredata.SAMLEnforcementPolicyOff,
IdPEntityID: req.IdPEntityID,
IdPSsoURL: req.IdPSsoURL,
IdPCertificate: req.IdPCertificate,
DomainVerificationToken: &domainVerificationToken,
EmailDomain: req.EmailDomain,
AutoSignupEnabled: req.AutoSignupEnabled,
AttributeEmail: DefaultAttributeEmail,
AttributeFirstname: DefaultAttributeFirstname,
AttributeLastname: DefaultAttributeLastname,
AttributeRole: DefaultAttributeRole,
CreatedAt: now,
UpdatedAt: now,
}
)
if req.AttributeEmail != nil {
config.AttributeEmail = *req.AttributeEmail
}
if req.AttributeFirstname != nil {
config.AttributeFirstname = *req.AttributeFirstname
}
if req.AttributeLastname != nil {
config.AttributeLastname = *req.AttributeLastname
}
if req.AttributeRole != nil {
config.AttributeRole = *req.AttributeRole
}
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
organization := &coredata.Organization{}
err := organization.LoadByID(ctx, tx, scope, organizationID)
if err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
err = config.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert saml configuration: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return config, nil
}
func (s OrganizationService) UpdateSAMLConfiguration(
ctx context.Context,
organizationID gid.GID,
configID gid.GID,
req *UpdateSAMLConfigurationRequest,
) (*coredata.SAMLConfiguration, error) {
var (
scope = coredata.NewScopeFromObjectID(organizationID)
config = &coredata.SAMLConfiguration{}
)
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
organization := &coredata.Organization{}
err := organization.LoadByID(ctx, tx, scope, organizationID)
if err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
config = &coredata.SAMLConfiguration{}
err = config.LoadByID(ctx, tx, scope, configID)
if err != nil {
return fmt.Errorf("cannot load saml configuration: %w", err)
}
if req.EnforcementPolicy != nil {
if config.DomainVerifiedAt == nil {
return NewSAMLConfigurationDomainNotVerifiedError(configID)
}
config.EnforcementPolicy = *req.EnforcementPolicy
}
if req.IdPEntityID != nil {
config.IdPEntityID = *req.IdPEntityID
}
if req.IdPSsoURL != nil {
config.IdPSsoURL = *req.IdPSsoURL
}
if req.IdPCertificate != nil {
config.IdPCertificate = *req.IdPCertificate
}
if req.AttributeEmail != nil {
config.AttributeEmail = *req.AttributeEmail
}
if req.AttributeFirstname != nil {
config.AttributeFirstname = *req.AttributeFirstname
}
if req.AttributeLastname != nil {
config.AttributeLastname = *req.AttributeLastname
}
if req.AttributeRole != nil {
config.AttributeRole = *req.AttributeRole
}
if req.AutoSignupEnabled != nil {
config.AutoSignupEnabled = *req.AutoSignupEnabled
}
config.UpdatedAt = time.Now()
err = config.Update(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot update saml configuration: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return config, nil
}
func (s OrganizationService) GetOrganization(ctx context.Context, organizationID gid.GID) (*coredata.Organization, error) {
var (
scope = coredata.NewScopeFromObjectID(organizationID)
organization = &coredata.Organization{}
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
err := organization.LoadByID(ctx, conn, scope, organizationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return NewOrganizationNotFoundError(organizationID)
}
return fmt.Errorf("cannot load organization: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return organization, nil
}