- Introduced new endpoints for managing identity providers, including retrieval of redirect URIs and testing/syncing providers. - Enhanced organization settings to include identity provider configurations, allowing for self-enrollment and domain restrictions. - Implemented caching for access policies and added validation for identity provider secrets. - Added integration tests to ensure proper functionality of identity provider management and policy enforcement.
688 lines
20 KiB
Go
688 lines
20 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/config"
|
|
"github.com/ultisuite/ulti-backend/internal/authentik"
|
|
)
|
|
|
|
const orgSettingsSingletonID = 1
|
|
|
|
func defaultOrgPolicy() map[string]any {
|
|
return map[string]any{
|
|
"authentik": map[string]any{
|
|
"enabled": true,
|
|
"api_url": "",
|
|
"slug": "ulti-suite",
|
|
"client_id": "",
|
|
"enforce_sso": true,
|
|
"allow_password_fallback": false,
|
|
"default_groups": "ulti-users",
|
|
},
|
|
"identity_providers": map[string]any{
|
|
"allow_self_enrollment": true,
|
|
"default_login_source": "",
|
|
"providers": []any{},
|
|
},
|
|
"two_factor": map[string]any{
|
|
"required_for_all": false,
|
|
"required_for_admins": true,
|
|
"allowed_methods": []any{"totp", "webauthn"},
|
|
"grace_period_days": 7,
|
|
"remember_device_days": 30,
|
|
},
|
|
"storage_quotas": map[string]any{
|
|
"default_mail_gib": 5,
|
|
"default_drive_gib": 5,
|
|
"default_photos_gib": 5,
|
|
"warn_threshold_pct": 90,
|
|
},
|
|
"usage_quotas": map[string]any{
|
|
"llm_requests_per_day": 100,
|
|
"llm_tokens_per_month": 500000,
|
|
"search_requests_per_day": 50,
|
|
"max_api_tokens_per_user": 10,
|
|
"max_webhooks_per_user": 20,
|
|
},
|
|
"file_policies": map[string]any{
|
|
"max_upload_mib": 512,
|
|
"allowed_extensions": "",
|
|
"block_executable": true,
|
|
"external_sharing": "authenticated",
|
|
"default_link_expiry_days": 30,
|
|
"virus_scan_enabled": false,
|
|
"virustotal_api_key": "",
|
|
"retention_trash_days": 30,
|
|
},
|
|
"llm": map[string]any{
|
|
"default_provider_id": "",
|
|
"providers": []any{},
|
|
"enforce_org_providers": false,
|
|
"allow_user_override": true,
|
|
},
|
|
"search": map[string]any{
|
|
"suite_engine": "postgres",
|
|
"meilisearch_url": "",
|
|
"meilisearch_api_key": "",
|
|
"typesense_url": "",
|
|
"typesense_api_key": "",
|
|
"web_search": map[string]any{
|
|
"default_provider_id": "brave-default",
|
|
"providers": []any{},
|
|
},
|
|
"enforce_org_search": false,
|
|
},
|
|
"administrators": []any{},
|
|
"nextcloud": map[string]any{
|
|
"enabled": false,
|
|
"base_url": "",
|
|
"admin_user": "",
|
|
"admin_password": "",
|
|
"drive_enabled": true,
|
|
"calendar_enabled": true,
|
|
"contacts_enabled": true,
|
|
"talk_enabled": false,
|
|
},
|
|
"mailing": map[string]any{
|
|
"enabled": false,
|
|
"smtp_host": "",
|
|
"smtp_port": 587,
|
|
"smtp_user": "",
|
|
"smtp_password": "",
|
|
"from_email": "noreply@example.com",
|
|
"from_name": "Ulti Suite",
|
|
"tls_mode": "starttls",
|
|
},
|
|
"onlyoffice": map[string]any{
|
|
"enabled": false,
|
|
"document_server_url": "",
|
|
"jwt_secret": "",
|
|
"jwt_header": "Authorization",
|
|
},
|
|
"plugins": []any{
|
|
map[string]any{"id": "mail-automation", "name": "Automatisations mail", "description": "Règles, webhooks et tri IA sur la réception.", "enabled": true, "version": "1.0.0"},
|
|
map[string]any{"id": "contact-discovery", "name": "Découverte contacts", "description": "Enrichissement IA et signatures détectées.", "enabled": true, "version": "1.0.0"},
|
|
map[string]any{"id": "public-share", "name": "Partage public Drive", "description": "Liens publics et partages externes.", "enabled": true, "version": "1.0.0"},
|
|
map[string]any{"id": "office-editor", "name": "Édition OnlyOffice", "description": "Édition collaborative de documents.", "enabled": false, "version": "1.0.0"},
|
|
},
|
|
"integrations": []any{
|
|
map[string]any{"id": "authentik", "name": "Authentik", "description": "SSO, groupes et provisionnement des comptes.", "enabled": true, "configured": false},
|
|
map[string]any{"id": "nextcloud", "name": "Nextcloud", "description": "Drive, agenda, contacts et Talk.", "enabled": false, "configured": false},
|
|
map[string]any{"id": "onlyoffice", "name": "OnlyOffice", "description": "Édition de documents dans le navigateur.", "enabled": false, "configured": false},
|
|
map[string]any{"id": "smtp", "name": "Mailing unifié", "description": "SMTP pour notifications suite (partages, mentions).", "enabled": false, "configured": false},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (s *Service) loadOrgPolicyRaw(ctx context.Context) (map[string]any, time.Time, string, error) {
|
|
var raw []byte
|
|
var updatedAt time.Time
|
|
var updatedBy string
|
|
err := s.db.QueryRow(ctx, `
|
|
SELECT settings, updated_at, updated_by FROM org_settings WHERE id = $1
|
|
`, orgSettingsSingletonID).Scan(&raw, &updatedAt, &updatedBy)
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return map[string]any{}, time.Time{}, "", nil
|
|
}
|
|
return nil, time.Time{}, "", err
|
|
}
|
|
stored := map[string]any{}
|
|
if len(raw) > 0 {
|
|
if err := json.Unmarshal(raw, &stored); err != nil {
|
|
return nil, time.Time{}, "", err
|
|
}
|
|
}
|
|
return stored, updatedAt, updatedBy, nil
|
|
}
|
|
|
|
func mergeMaps(base, patch map[string]any) map[string]any {
|
|
if base == nil {
|
|
base = map[string]any{}
|
|
}
|
|
out := make(map[string]any, len(base)+len(patch))
|
|
for k, v := range base {
|
|
out[k] = v
|
|
}
|
|
for k, patchVal := range patch {
|
|
if patchVal == nil {
|
|
continue
|
|
}
|
|
baseVal, ok := out[k]
|
|
if !ok {
|
|
out[k] = patchVal
|
|
continue
|
|
}
|
|
baseMap, baseOK := baseVal.(map[string]any)
|
|
patchMap, patchOK := patchVal.(map[string]any)
|
|
if baseOK && patchOK {
|
|
out[k] = mergeMaps(baseMap, patchMap)
|
|
continue
|
|
}
|
|
out[k] = patchVal
|
|
}
|
|
return out
|
|
}
|
|
|
|
func mergeOrgSecrets(existing, patch map[string]any) map[string]any {
|
|
merged := mergeMaps(existing, patch)
|
|
secretPaths := []struct {
|
|
section string
|
|
key string
|
|
}{
|
|
{"nextcloud", "admin_password"},
|
|
{"mailing", "smtp_password"},
|
|
{"onlyoffice", "jwt_secret"},
|
|
{"search", "meilisearch_api_key"},
|
|
{"search", "typesense_api_key"},
|
|
{"file_policies", "virustotal_api_key"},
|
|
}
|
|
for _, p := range secretPaths {
|
|
patchSection, _ := patch[p.section].(map[string]any)
|
|
if patchSection == nil {
|
|
continue
|
|
}
|
|
val, _ := patchSection[p.key].(string)
|
|
if strings.TrimSpace(val) == "" {
|
|
if existingSection, ok := existing[p.section].(map[string]any); ok {
|
|
if existingVal, ok := existingSection[p.key].(string); ok && existingVal != "" {
|
|
mergedSection, _ := merged[p.section].(map[string]any)
|
|
if mergedSection == nil {
|
|
mergedSection = map[string]any{}
|
|
}
|
|
mergedSection[p.key] = existingVal
|
|
merged[p.section] = mergedSection
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if patchLLM, ok := patch["llm"].(map[string]any); ok {
|
|
mergeLLMProviderSecrets(existing, patchLLM, merged)
|
|
}
|
|
if patchSearch, ok := patch["search"].(map[string]any); ok {
|
|
if patchWS, ok := patchSearch["web_search"].(map[string]any); ok {
|
|
mergeSearchProviderSecrets(existing, patchWS, merged)
|
|
}
|
|
}
|
|
if patchIDP, ok := patch["identity_providers"].(map[string]any); ok {
|
|
mergeIdentityProviderSecrets(existing, patchIDP, merged)
|
|
}
|
|
return merged
|
|
}
|
|
|
|
func mergeLLMProviderSecrets(existing, patchLLM, merged map[string]any) {
|
|
patchProviders, _ := patchLLM["providers"].([]any)
|
|
if len(patchProviders) == 0 {
|
|
return
|
|
}
|
|
existingLLM, _ := existing["llm"].(map[string]any)
|
|
existingProviders, _ := existingLLM["providers"].([]any)
|
|
mergedLLM, _ := merged["llm"].(map[string]any)
|
|
if mergedLLM == nil {
|
|
return
|
|
}
|
|
mergedProviders := make([]any, len(patchProviders))
|
|
for i, item := range patchProviders {
|
|
pm, ok := item.(map[string]any)
|
|
if !ok {
|
|
mergedProviders[i] = item
|
|
continue
|
|
}
|
|
id, _ := pm["id"].(string)
|
|
apiKey, _ := pm["api_key"].(string)
|
|
if strings.TrimSpace(apiKey) == "" && id != "" {
|
|
for _, ep := range existingProviders {
|
|
em, ok := ep.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if eid, _ := em["id"].(string); eid == id {
|
|
if ek, ok := em["api_key"].(string); ok && ek != "" {
|
|
pm["api_key"] = ek
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mergedProviders[i] = pm
|
|
}
|
|
mergedLLM["providers"] = mergedProviders
|
|
merged["llm"] = mergedLLM
|
|
}
|
|
|
|
func mergeSearchProviderSecrets(existing map[string]any, patchWS map[string]any, merged map[string]any) {
|
|
patchProviders, _ := patchWS["providers"].([]any)
|
|
if len(patchProviders) == 0 {
|
|
return
|
|
}
|
|
existingSearch, _ := existing["search"].(map[string]any)
|
|
existingWS, _ := existingSearch["web_search"].(map[string]any)
|
|
existingProviders, _ := existingWS["providers"].([]any)
|
|
mergedSearch, _ := merged["search"].(map[string]any)
|
|
if mergedSearch == nil {
|
|
return
|
|
}
|
|
mergedWS, _ := mergedSearch["web_search"].(map[string]any)
|
|
if mergedWS == nil {
|
|
mergedWS = map[string]any{}
|
|
}
|
|
mergedProviders := make([]any, len(patchProviders))
|
|
for i, item := range patchProviders {
|
|
pm, ok := item.(map[string]any)
|
|
if !ok {
|
|
mergedProviders[i] = item
|
|
continue
|
|
}
|
|
id, _ := pm["id"].(string)
|
|
apiKey, _ := pm["api_key"].(string)
|
|
if strings.TrimSpace(apiKey) == "" && id != "" {
|
|
for _, ep := range existingProviders {
|
|
em, ok := ep.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if eid, _ := em["id"].(string); eid == id {
|
|
if ek, ok := em["api_key"].(string); ok && ek != "" {
|
|
pm["api_key"] = ek
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mergedProviders[i] = pm
|
|
}
|
|
mergedWS["providers"] = mergedProviders
|
|
mergedSearch["web_search"] = mergedWS
|
|
merged["search"] = mergedSearch
|
|
}
|
|
|
|
func mergeIdentityProviderSecrets(existing, patchIDP, merged map[string]any) {
|
|
patchProviders, _ := patchIDP["providers"].([]any)
|
|
if len(patchProviders) == 0 {
|
|
return
|
|
}
|
|
existingIDP, _ := existing["identity_providers"].(map[string]any)
|
|
existingProviders, _ := existingIDP["providers"].([]any)
|
|
mergedIDP, _ := merged["identity_providers"].(map[string]any)
|
|
if mergedIDP == nil {
|
|
return
|
|
}
|
|
mergedProviders := make([]any, len(patchProviders))
|
|
for i, item := range patchProviders {
|
|
pm, ok := item.(map[string]any)
|
|
if !ok {
|
|
mergedProviders[i] = item
|
|
continue
|
|
}
|
|
id, _ := pm["id"].(string)
|
|
for _, secretPath := range []struct{ section, key string }{
|
|
{"oauth", "client_secret"},
|
|
{"ldap", "bind_password"},
|
|
{"saml", "signing_cert"},
|
|
} {
|
|
mergeProviderSecretField(pm, existingProviders, id, secretPath.section, secretPath.key)
|
|
}
|
|
mergedProviders[i] = pm
|
|
}
|
|
mergedIDP["providers"] = mergedProviders
|
|
merged["identity_providers"] = mergedIDP
|
|
}
|
|
|
|
func mergeProviderSecretField(pm map[string]any, existingProviders []any, id, section, key string) {
|
|
nested, _ := pm[section].(map[string]any)
|
|
if nested == nil {
|
|
return
|
|
}
|
|
val, _ := nested[key].(string)
|
|
if strings.TrimSpace(val) != "" || id == "" {
|
|
return
|
|
}
|
|
for _, ep := range existingProviders {
|
|
em, ok := ep.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if eid, _ := em["id"].(string); eid != id {
|
|
continue
|
|
}
|
|
esec, _ := em[section].(map[string]any)
|
|
if esec == nil {
|
|
continue
|
|
}
|
|
if ek, ok := esec[key].(string); ok && ek != "" {
|
|
nested[key] = ek
|
|
}
|
|
}
|
|
}
|
|
|
|
func maskOrgPolicy(policy map[string]any) map[string]any {
|
|
cloned := deepCloneMap(policy)
|
|
maskStringField(cloned, "nextcloud", "admin_password")
|
|
maskStringField(cloned, "mailing", "smtp_password")
|
|
maskStringField(cloned, "onlyoffice", "jwt_secret")
|
|
maskStringField(cloned, "search", "meilisearch_api_key")
|
|
maskStringField(cloned, "search", "typesense_api_key")
|
|
maskStringField(cloned, "file_policies", "virustotal_api_key")
|
|
if llm, ok := cloned["llm"].(map[string]any); ok {
|
|
if providers, ok := llm["providers"].([]any); ok {
|
|
for i, p := range providers {
|
|
pm, ok := p.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if v, _ := pm["api_key"].(string); strings.TrimSpace(v) != "" {
|
|
pm["api_key"] = ""
|
|
}
|
|
providers[i] = pm
|
|
}
|
|
llm["providers"] = providers
|
|
}
|
|
}
|
|
if search, ok := cloned["search"].(map[string]any); ok {
|
|
if ws, ok := search["web_search"].(map[string]any); ok {
|
|
if providers, ok := ws["providers"].([]any); ok {
|
|
for i, p := range providers {
|
|
pm, ok := p.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if v, _ := pm["api_key"].(string); strings.TrimSpace(v) != "" {
|
|
pm["api_key"] = ""
|
|
}
|
|
providers[i] = pm
|
|
}
|
|
ws["providers"] = providers
|
|
}
|
|
}
|
|
}
|
|
maskIdentityProviderSecrets(cloned)
|
|
return cloned
|
|
}
|
|
|
|
func maskIdentityProviderSecrets(policy map[string]any) {
|
|
idp, ok := policy["identity_providers"].(map[string]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
providers, ok := idp["providers"].([]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
for i, item := range providers {
|
|
pm, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, section := range []string{"oauth", "ldap", "saml"} {
|
|
nested, ok := pm[section].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, key := range []string{"client_secret", "bind_password", "signing_cert"} {
|
|
if v, _ := nested[key].(string); strings.TrimSpace(v) != "" {
|
|
nested[key] = ""
|
|
}
|
|
}
|
|
pm[section] = nested
|
|
}
|
|
providers[i] = pm
|
|
}
|
|
idp["providers"] = providers
|
|
}
|
|
|
|
func maskStringField(m map[string]any, section, key string) {
|
|
sec, ok := m[section].(map[string]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
if v, _ := sec[key].(string); strings.TrimSpace(v) != "" {
|
|
sec[key] = ""
|
|
}
|
|
}
|
|
|
|
func deepCloneMap(in map[string]any) map[string]any {
|
|
raw, _ := json.Marshal(in)
|
|
out := map[string]any{}
|
|
_ = json.Unmarshal(raw, &out)
|
|
return out
|
|
}
|
|
|
|
func secretConfigured(policy map[string]any, section, key string) bool {
|
|
sec, ok := policy[section].(map[string]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
v, _ := sec[key].(string)
|
|
return strings.TrimSpace(v) != ""
|
|
}
|
|
|
|
func buildOrgSecretsStatus(policy map[string]any, cfg *config.Config) map[string]any {
|
|
secrets := map[string]any{
|
|
"nextcloud_admin_password": map[string]any{
|
|
"configured": secretConfigured(policy, "nextcloud", "admin_password") || strings.TrimSpace(cfg.NCAdminPass) != "",
|
|
},
|
|
"mailing_smtp_password": map[string]any{
|
|
"configured": secretConfigured(policy, "mailing", "smtp_password"),
|
|
},
|
|
"onlyoffice_jwt_secret": map[string]any{
|
|
"configured": secretConfigured(policy, "onlyoffice", "jwt_secret") || strings.TrimSpace(cfg.OnlyOfficeJWTSecret) != "",
|
|
},
|
|
"meilisearch_api_key": map[string]any{
|
|
"configured": secretConfigured(policy, "search", "meilisearch_api_key") || strings.TrimSpace(cfg.MeilisearchKey) != "",
|
|
},
|
|
"typesense_api_key": map[string]any{
|
|
"configured": secretConfigured(policy, "search", "typesense_api_key") || strings.TrimSpace(cfg.TypesenseKey) != "",
|
|
},
|
|
"virustotal_api_key": map[string]any{
|
|
"configured": secretConfigured(policy, "file_policies", "virustotal_api_key") || strings.TrimSpace(cfg.VirusTotalAPIKey) != "",
|
|
},
|
|
}
|
|
if idpSecrets := buildIdentityProviderSecretsStatus(policy); len(idpSecrets) > 0 {
|
|
secrets["identity_providers"] = idpSecrets
|
|
}
|
|
return secrets
|
|
}
|
|
|
|
func buildIdentityProviderSecretsStatus(policy map[string]any) map[string]any {
|
|
idp, ok := policy["identity_providers"].(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
providers, ok := idp["providers"].([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
out := make(map[string]any)
|
|
for _, item := range providers {
|
|
pm, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
id, _ := pm["id"].(string)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
entry := map[string]any{}
|
|
if oauth, ok := pm["oauth"].(map[string]any); ok {
|
|
if v, _ := oauth["client_secret"].(string); strings.TrimSpace(v) != "" {
|
|
entry["oauth_client_secret"] = map[string]any{"configured": true}
|
|
}
|
|
}
|
|
if ldap, ok := pm["ldap"].(map[string]any); ok {
|
|
if v, _ := ldap["bind_password"].(string); strings.TrimSpace(v) != "" {
|
|
entry["ldap_bind_password"] = map[string]any{"configured": true}
|
|
}
|
|
}
|
|
if saml, ok := pm["saml"].(map[string]any); ok {
|
|
if v, _ := saml["signing_cert"].(string); strings.TrimSpace(v) != "" {
|
|
entry["saml_signing_cert"] = map[string]any{"configured": true}
|
|
}
|
|
}
|
|
if len(entry) > 0 {
|
|
out[id] = entry
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func buildOrgEffective(cfg *config.Config) map[string]any {
|
|
authentikEnabled := strings.TrimSpace(cfg.AuthentikAPIToken) != "" || strings.TrimSpace(cfg.OIDCIssuer) != ""
|
|
publicBase := authentik.AuthentikPublicBaseURL(cfg.AuthentikAPIURL, cfg.AuthentikPublicHTTPS)
|
|
return map[string]any{
|
|
"authentik": map[string]any{
|
|
"enabled": authentikEnabled,
|
|
"api_url": cfg.AuthentikAPIURL,
|
|
"client_id": cfg.OIDCClientID,
|
|
"issuer": cfg.OIDCIssuer,
|
|
},
|
|
"identity_providers": map[string]any{
|
|
"authentik_public_url": publicBase,
|
|
"oauth_redirect_template": publicBase + "/source/oauth/callback/{slug}/",
|
|
},
|
|
"nextcloud": map[string]any{
|
|
"enabled": cfg.NextcloudEnabled,
|
|
"base_url": firstNonEmpty(cfg.NextcloudPublicURL, cfg.NextcloudURL),
|
|
"admin_user": cfg.NCAdminUser,
|
|
},
|
|
"onlyoffice": map[string]any{
|
|
"enabled": cfg.OnlyOfficeEnabled,
|
|
"document_server_url": firstNonEmpty(cfg.OnlyOfficePublicURL, cfg.OnlyOfficeURL),
|
|
},
|
|
"search": map[string]any{
|
|
"suite_engine": cfg.SearchEngine,
|
|
"meilisearch_url": cfg.MeilisearchURL,
|
|
"typesense_url": cfg.TypesenseURL,
|
|
},
|
|
"immich": map[string]any{
|
|
"enabled": cfg.ImmichEnabled,
|
|
"api_url": cfg.ImmichAPIURL,
|
|
},
|
|
"jitsi": map[string]any{
|
|
"enabled": cfg.JitsiEnabled,
|
|
"public_url": cfg.JitsiPublicURL,
|
|
},
|
|
}
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, v := range values {
|
|
if strings.TrimSpace(v) != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *Service) GetOrgSettings(ctx context.Context) (map[string]any, error) {
|
|
stored, updatedAt, updatedBy, err := s.loadOrgPolicyRaw(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
policy := mergeMaps(defaultOrgPolicy(), stored)
|
|
masked := maskOrgPolicy(policy)
|
|
return map[string]any{
|
|
"policy": masked,
|
|
"effective": buildOrgEffective(s.cfg),
|
|
"secrets": buildOrgSecretsStatus(policy, s.cfg),
|
|
"env_vars": buildOrgEnvVars(),
|
|
"deploy_locked": buildOrgDeployLocked(s.cfg),
|
|
"updated_at": updatedAt.UTC().Format(time.RFC3339),
|
|
"updated_by": updatedBy,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) PutOrgSettings(ctx context.Context, actorSub string, patch map[string]any) (map[string]any, error) {
|
|
storedBefore, _, _, err := s.loadOrgPolicyRaw(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
merged := mergeOrgSecrets(storedBefore, patch)
|
|
raw, err := json.Marshal(merged)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = s.db.Exec(ctx, `
|
|
INSERT INTO org_settings (id, settings, updated_at, updated_by)
|
|
VALUES ($1, $2, NOW(), $3)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
settings = EXCLUDED.settings,
|
|
updated_at = NOW(),
|
|
updated_by = EXCLUDED.updated_by
|
|
`, orgSettingsSingletonID, raw, actorSub)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.logAudit(ctx, actorSub, "update_org_settings", map[string]any{
|
|
"sections": mapKeys(patch),
|
|
})
|
|
|
|
if _, ok := patch["identity_providers"]; ok {
|
|
removed := authentik.RemovedIdentityProviders(storedBefore, merged)
|
|
syncer := authentik.NewSourceSyncer(s.db, s.cfg)
|
|
idpSection, _ := merged["identity_providers"].(map[string]any)
|
|
if err := syncer.SyncSectionWithRemoved(ctx, idpSection, removed); err != nil {
|
|
s.logger.Warn("identity provider sync failed", "error", err)
|
|
}
|
|
}
|
|
|
|
return s.GetOrgSettings(ctx)
|
|
}
|
|
|
|
func mapKeys(m map[string]any) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
const gibBytes = int64(1024 * 1024 * 1024)
|
|
|
|
func policyGibValue(v any, fallback int64) int64 {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
if n > 0 {
|
|
return int64(n * float64(gibBytes))
|
|
}
|
|
case int:
|
|
if n > 0 {
|
|
return int64(n) * gibBytes
|
|
}
|
|
case int64:
|
|
if n > 0 {
|
|
return n * gibBytes
|
|
}
|
|
}
|
|
return fallback * gibBytes
|
|
}
|
|
|
|
func (s *Service) applyOrgDefaultQuotas(ctx context.Context, userID string) error {
|
|
stored, _, _, err := s.loadOrgPolicyRaw(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
policy := mergeMaps(defaultOrgPolicy(), stored)
|
|
storage, _ := policy["storage_quotas"].(map[string]any)
|
|
const defaultGib = int64(5)
|
|
mail := policyGibValue(storage["default_mail_gib"], defaultGib)
|
|
drive := policyGibValue(storage["default_drive_gib"], defaultGib)
|
|
photos := policyGibValue(storage["default_photos_gib"], defaultGib)
|
|
_, err = s.db.Exec(ctx, `
|
|
INSERT INTO settings (user_id, preferences)
|
|
VALUES (
|
|
$1,
|
|
jsonb_build_object(
|
|
'mail_max_storage_bytes', $2::text,
|
|
'drive_max_storage_bytes', $3::text,
|
|
'photos_max_storage_bytes', $4::text
|
|
)
|
|
)
|
|
ON CONFLICT (user_id) DO NOTHING
|
|
`, userID, mail, drive, photos)
|
|
return err
|
|
}
|