Files
pocket-id/backend/internal/ldapsync/module.go
ItalyPaleAle 2aa13fe06c feat: add FRANCIS_HOST to connect to a standalone Francis runtime
FRANCIS_HOST decides where the Francis actor runtime lives. When it is
empty or set to "embedded" (the default), Pocket ID starts the runtime inside its own process, backed by its own database.

Any other value is the address, or a comma-separated list of addresses, of a standalone Francis runtime. Pocket ID then connects to it as a remote actor host and starts no embedded runtime.

Note: connecting to a standalone runtime also needs FRANCIS_HOST_PSK or FRANCIS_HOST_JWT, and optionally (but recommended) FRANCIS_CA.
2026-08-31 14:56:45 +02:00

91 lines
3.6 KiB
Go

package ldapsync
import (
"context"
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin"
francishost "github.com/italypaleale/francis/host"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/httpserver"
"github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/storage"
)
// UserSyncer applies the desired LDAP state to the users in the database
// Every method takes the transaction the sync runs in, since users, groups, and memberships are reconciled atomically
type UserSyncer interface {
CreateUserInternal(ctx context.Context, dbConfig *appconfig.AppConfigModel, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB) (model.User, error)
UpdateUserInternal(ctx context.Context, dbConfig *appconfig.AppConfigModel, userID string, input dto.UserCreateDto, updateOwnUser bool, isLdapSync bool, tx *gorm.DB) (model.User, error)
DisableUserInternal(ctx context.Context, tx *gorm.DB, userID string) error
DeleteUserInternal(ctx context.Context, dbConfig *appconfig.AppConfigModel, tx *gorm.DB, userID string, allowLdapDelete bool) error
// UpdateProfilePicture stores a user's profile picture, which happens after the transaction has been committed since it touches the storage layer
UpdateProfilePicture(ctx context.Context, userID string, file io.ReadSeeker) error
}
// GroupSyncer applies the desired LDAP state to the user groups in the database
type GroupSyncer interface {
CreateInternal(ctx context.Context, input dto.UserGroupCreateDto, tx *gorm.DB) (model.UserGroup, error)
UpdateInternal(ctx context.Context, dbConfig *appconfig.AppConfigModel, id string, input dto.UserGroupCreateDto, isLdapSync bool, tx *gorm.DB) (model.UserGroup, error)
UpdateUsersInternal(ctx context.Context, id string, userIDs []string, tx *gorm.DB) (model.UserGroup, error)
}
// ScimSyncScheduler schedules SCIM after the LDAP transaction has committed
type ScimSyncScheduler interface {
ScheduleSync(ctx context.Context)
}
type Dependencies struct {
DB *gorm.DB
Actors francishost.Host
HTTPClient *http.Client
FileStorage storage.FileStorage
Users UserSyncer
Groups GroupSyncer
AppConfig appconfig.AppConfigResolver
ScimSync ScimSyncScheduler
// ScheduleDisabled keeps the recurring sync from being armed
// It's set in the test environment, where syncs are driven explicitly by the end-to-end tests
ScheduleDisabled bool
}
type Module struct {
service *Service
handler *handler
}
func New(deps Dependencies) (*Module, error) {
service := newService(deps)
// Register the actor that drives the recurring sync
// It's a singleton, so the host bootstraps it at startup and the alarm fires once per cluster rather than once per replica
err := deps.Actors.RegisterSingletonActor(SyncActorType, NewSyncActor(service, deps.AppConfig, deps.ScheduleDisabled))
if err != nil {
return nil, fmt.Errorf("error registering the %s actor: %w", SyncActorType, err)
}
return &Module{
service: service,
handler: newHandler(service, deps.AppConfig),
}, nil
}
// RegisterRoutes mounts the manual LDAP synchronization endpoint
// auth guards it, as it's an admin-only operation
func (m *Module) RegisterRoutes(apiGroup *gin.RouterGroup, auth gin.HandlerFunc) {
apiGroup.POST("/application-configuration/sync-ldap", auth, httpserver.Handle(m.handler.syncLdap))
}
// SyncAll runs a full LDAP synchronization with the provided application configuration
func (m *Module) SyncAll(ctx context.Context, dbConfig *appconfig.AppConfigModel) error {
return m.service.SyncAll(ctx, dbConfig)
}