From 4e359fe659e5ae14558632ab3d2a9194acfc6933 Mon Sep 17 00:00:00 2001 From: fs Date: Mon, 13 Apr 2026 14:13:21 +0200 Subject: [PATCH] feat(auth): sync display name and job title from Microsoft Graph, fix tenant SSO form handling Fetches displayName/jobTitle from Graph API and syncs them via SSO profile sync. Also fixes LDAP enabled state in tenant form, adds LDAP save on tenant create, and hardens LDAP port validation precedence. Co-Authored-By: Claude Opus 4.6 --- i18n/default_de.json | 2 ++ i18n/default_en.json | 2 ++ lib/Repository/User/UserWriteRepository.php | 6 +++--- lib/Service/Auth/MicrosoftOidcService.php | 6 +++++- lib/Service/Auth/SsoUserLinkService.php | 2 ++ lib/Service/Auth/TenantSsoService.php | 14 +++++++++++--- pages/admin/tenants/_form.phtml | 5 +++-- pages/admin/tenants/create().php | 8 ++++++++ pages/admin/tenants/edit($id).php | 4 +++- 9 files changed, 39 insertions(+), 10 deletions(-) diff --git a/i18n/default_de.json b/i18n/default_de.json index 35753cb..fd00a25 100644 --- a/i18n/default_de.json +++ b/i18n/default_de.json @@ -934,6 +934,8 @@ "Sync fields": "Sync-Felder", "Sync first name": "Vorname synchronisieren", "Sync last name": "Nachname synchronisieren", + "Sync display name": "Anzeigename synchronisieren", + "Sync job title": "Berufsbezeichnung synchronisieren", "Sync phone": "Telefon synchronisieren", "Sync mobile": "Mobil synchronisieren", "Sync avatar image": "Avatarbild synchronisieren", diff --git a/i18n/default_en.json b/i18n/default_en.json index ca03ed0..9ff5db6 100644 --- a/i18n/default_en.json +++ b/i18n/default_en.json @@ -934,6 +934,8 @@ "Sync fields": "Sync fields", "Sync first name": "Sync first name", "Sync last name": "Sync last name", + "Sync display name": "Sync display name", + "Sync job title": "Sync job title", "Sync phone": "Sync phone", "Sync mobile": "Sync mobile", "Sync avatar image": "Sync avatar image", diff --git a/lib/Repository/User/UserWriteRepository.php b/lib/Repository/User/UserWriteRepository.php index 6bc4621..9c48486 100644 --- a/lib/Repository/User/UserWriteRepository.php +++ b/lib/Repository/User/UserWriteRepository.php @@ -225,13 +225,13 @@ class UserWriteRepository implements UserWriteRepositoryInterface return false; } - $row = DB::selectOne('select first_name, last_name, email, phone, mobile from users where id = ? limit 1', (string) $id); + $row = DB::selectOne('select first_name, last_name, email, phone, mobile, job_title, display_name from users where id = ? limit 1', (string) $id); $existing = $row['users'] ?? null; if (!is_array($existing)) { return false; } - $allowed = ['first_name', 'last_name', 'phone', 'mobile']; + $allowed = ['first_name', 'last_name', 'display_name', 'job_title', 'phone', 'mobile']; $updates = []; foreach ($allowed as $field) { if (!array_key_exists($field, $fields)) { @@ -251,7 +251,7 @@ class UserWriteRepository implements UserWriteRepositoryInterface return true; } - if (isset($updates['first_name']) || isset($updates['last_name'])) { + if (!isset($updates['display_name']) && (isset($updates['first_name']) || isset($updates['last_name']))) { $displayData = [ 'first_name' => $updates['first_name'] ?? (string) ($existing['first_name'] ?? ''), 'last_name' => $updates['last_name'] ?? (string) ($existing['last_name'] ?? ''), diff --git a/lib/Service/Auth/MicrosoftOidcService.php b/lib/Service/Auth/MicrosoftOidcService.php index 36b6be2..821ad7c 100644 --- a/lib/Service/Auth/MicrosoftOidcService.php +++ b/lib/Service/Auth/MicrosoftOidcService.php @@ -197,6 +197,8 @@ class MicrosoftOidcService 'preferred_language' => trim((string) ($claims['preferred_language'] ?? '')), 'graph_given_name' => trim((string) ($graphProfile['given_name'] ?? '')), 'graph_family_name' => trim((string) ($graphProfile['family_name'] ?? '')), + 'graph_display_name' => trim((string) ($graphProfile['display_name'] ?? '')), + 'graph_job_title' => trim((string) ($graphProfile['job_title'] ?? '')), 'graph_phone' => trim((string) ($graphProfile['phone'] ?? '')), 'graph_mobile' => trim((string) ($graphProfile['mobile'] ?? '')), 'graph_avatar_mime' => trim((string) ($graphAvatar['mime'] ?? '')), @@ -368,7 +370,7 @@ class MicrosoftOidcService private function fetchMicrosoftGraphProfile(string $accessToken): array { - $url = 'https://graph.microsoft.com/v1.0/me?$select=givenName,surname,mobilePhone,businessPhones'; + $url = 'https://graph.microsoft.com/v1.0/me?$select=givenName,surname,displayName,jobTitle,mobilePhone,businessPhones'; $response = $this->oidcHttpGateway->call('GET', $url, '', [ 'Accept' => 'application/json', 'Authorization' => 'Bearer ' . $accessToken, @@ -393,6 +395,8 @@ class MicrosoftOidcService 'profile' => [ 'given_name' => trim((string) ($data['givenName'] ?? '')), 'family_name' => trim((string) ($data['surname'] ?? '')), + 'display_name' => trim((string) ($data['displayName'] ?? '')), + 'job_title' => trim((string) ($data['jobTitle'] ?? '')), 'mobile' => trim((string) ($data['mobilePhone'] ?? '')), 'phone' => $phone, ], diff --git a/lib/Service/Auth/SsoUserLinkService.php b/lib/Service/Auth/SsoUserLinkService.php index ce16ebd..8365562 100644 --- a/lib/Service/Auth/SsoUserLinkService.php +++ b/lib/Service/Auth/SsoUserLinkService.php @@ -153,6 +153,8 @@ class SsoUserLinkService 'last_name' => trim((string) ($claims['family_name'] ?? '')) !== '' ? trim((string) ($claims['family_name'] ?? '')) : trim((string) ($claims['graph_family_name'] ?? '')), + 'display_name' => trim((string) ($claims['graph_display_name'] ?? '')), + 'job_title' => trim((string) ($claims['graph_job_title'] ?? '')), 'phone' => trim((string) ($claims['graph_phone'] ?? '')), 'mobile' => trim((string) ($claims['graph_mobile'] ?? '')), ]; diff --git a/lib/Service/Auth/TenantSsoService.php b/lib/Service/Auth/TenantSsoService.php index 70f9341..3ab4b16 100644 --- a/lib/Service/Auth/TenantSsoService.php +++ b/lib/Service/Auth/TenantSsoService.php @@ -13,6 +13,8 @@ class TenantSsoService private const PROFILE_SYNC_FIELD_OPTIONS = [ 'first_name' => 'Sync first name', 'last_name' => 'Sync last name', + 'display_name' => 'Sync display name', + 'job_title' => 'Sync job title', 'phone' => 'Sync phone', 'mobile' => 'Sync mobile', 'avatar' => 'Sync avatar image', @@ -459,7 +461,9 @@ class TenantSsoService $fields = $this->normalizeProfileSyncFields($fields); return in_array('phone', $fields, true) || in_array('mobile', $fields, true) - || in_array('avatar', $fields, true); + || in_array('avatar', $fields, true) + || in_array('job_title', $fields, true) + || in_array('display_name', $fields, true); } public function shouldAutoRememberOnMicrosoftLogin(int $tenantId): bool @@ -784,7 +788,7 @@ class TenantSsoService if ($enabled && $baseDn === '') { $errors[] = t('LDAP base DN is required'); } - if ($enabled && $port < 1 || $port > 65535) { + if ($enabled && ($port < 1 || $port > 65535)) { $errors[] = t('LDAP port must be between 1 and 65535'); } if ($enabled && !str_contains($userSearchFilter, '%s')) { @@ -832,7 +836,11 @@ class TenantSsoService } $autoRememberMode = $this->normalizeAutoRememberMode($input['ldap_auto_remember_mode'] ?? null); - $attributeMapJson = json_encode($attributeMap, JSON_THROW_ON_ERROR); + try { + $attributeMapJson = json_encode($attributeMap, JSON_THROW_ON_ERROR); + } catch (\JsonException) { + return ['ok' => false, 'errors' => [t('LDAP attribute map could not be encoded')]]; + } $saved = $this->tenantLdapAuthRepository->upsertByTenantId($tenantId, [ 'enabled' => $enabled, diff --git a/pages/admin/tenants/_form.phtml b/pages/admin/tenants/_form.phtml index ec7df2b..b50c20a 100644 --- a/pages/admin/tenants/_form.phtml +++ b/pages/admin/tenants/_form.phtml @@ -49,7 +49,7 @@ if ($rawAllowUserThemeMode === null || $rawAllowUserThemeMode === '') { } else { $tenantAllowUserThemeMode = '0'; } -$microsoftEnabledRaw = $values['microsoft_enabled'] ?? $values['enabled'] ?? false; +$microsoftEnabledRaw = $values['microsoft_enabled'] ?? false; $microsoftEnabled = in_array(strtolower(trim((string) $microsoftEnabledRaw)), ['1', 'true', 'yes', 'on'], true); $microsoftEnforceRaw = $values['enforce_microsoft_login'] ?? false; $microsoftEnforce = in_array(strtolower(trim((string) $microsoftEnforceRaw)), ['1', 'true', 'yes', 'on'], true); @@ -686,11 +686,12 @@ $openOverrideCard = $detailsOpenAll; isMethod('POST')) { 'tenant_sso_create_failed' ); } + $ldapSaveResult = $tenantSsoService->saveTenantLdapAuth($createdTenantId, $post); + if (!($ldapSaveResult['ok'] ?? false)) { + Flash::error( + (string) (($ldapSaveResult['errors'][0] ?? t('Tenant LDAP settings could not be saved'))), + $closeTarget, + 'tenant_ldap_create_failed' + ); + } } } $action = (string) ($post['action'] ?? 'create'); diff --git a/pages/admin/tenants/edit($id).php b/pages/admin/tenants/edit($id).php index d0acad4..0f97d4d 100644 --- a/pages/admin/tenants/edit($id).php +++ b/pages/admin/tenants/edit($id).php @@ -73,7 +73,9 @@ app(\MintyPHP\Service\Audit\AuditMetadataEnricherInterface::class)->enrich($tena $errorBag = formErrors(); $errors = []; -$form = array_merge($tenant, $ssoConfig, $ldapConfig); +$form = array_merge($tenant, $ssoConfig); +$form['microsoft_enabled'] = $ssoConfig['enabled'] ?? false; +$form['ldap_enabled'] = $ldapConfig['enabled'] ?? false; if ($request->isMethod('POST') && !Session::checkCsrfToken()) { Flash::error(t('Form expired, please try again'), $editTarget, 'csrf_expired');