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 <noreply@anthropic.com>
This commit is contained in:
@@ -934,6 +934,8 @@
|
|||||||
"Sync fields": "Sync-Felder",
|
"Sync fields": "Sync-Felder",
|
||||||
"Sync first name": "Vorname synchronisieren",
|
"Sync first name": "Vorname synchronisieren",
|
||||||
"Sync last name": "Nachname synchronisieren",
|
"Sync last name": "Nachname synchronisieren",
|
||||||
|
"Sync display name": "Anzeigename synchronisieren",
|
||||||
|
"Sync job title": "Berufsbezeichnung synchronisieren",
|
||||||
"Sync phone": "Telefon synchronisieren",
|
"Sync phone": "Telefon synchronisieren",
|
||||||
"Sync mobile": "Mobil synchronisieren",
|
"Sync mobile": "Mobil synchronisieren",
|
||||||
"Sync avatar image": "Avatarbild synchronisieren",
|
"Sync avatar image": "Avatarbild synchronisieren",
|
||||||
|
|||||||
@@ -934,6 +934,8 @@
|
|||||||
"Sync fields": "Sync fields",
|
"Sync fields": "Sync fields",
|
||||||
"Sync first name": "Sync first name",
|
"Sync first name": "Sync first name",
|
||||||
"Sync last name": "Sync last name",
|
"Sync last name": "Sync last name",
|
||||||
|
"Sync display name": "Sync display name",
|
||||||
|
"Sync job title": "Sync job title",
|
||||||
"Sync phone": "Sync phone",
|
"Sync phone": "Sync phone",
|
||||||
"Sync mobile": "Sync mobile",
|
"Sync mobile": "Sync mobile",
|
||||||
"Sync avatar image": "Sync avatar image",
|
"Sync avatar image": "Sync avatar image",
|
||||||
|
|||||||
@@ -225,13 +225,13 @@ class UserWriteRepository implements UserWriteRepositoryInterface
|
|||||||
return false;
|
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;
|
$existing = $row['users'] ?? null;
|
||||||
if (!is_array($existing)) {
|
if (!is_array($existing)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowed = ['first_name', 'last_name', 'phone', 'mobile'];
|
$allowed = ['first_name', 'last_name', 'display_name', 'job_title', 'phone', 'mobile'];
|
||||||
$updates = [];
|
$updates = [];
|
||||||
foreach ($allowed as $field) {
|
foreach ($allowed as $field) {
|
||||||
if (!array_key_exists($field, $fields)) {
|
if (!array_key_exists($field, $fields)) {
|
||||||
@@ -251,7 +251,7 @@ class UserWriteRepository implements UserWriteRepositoryInterface
|
|||||||
return true;
|
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 = [
|
$displayData = [
|
||||||
'first_name' => $updates['first_name'] ?? (string) ($existing['first_name'] ?? ''),
|
'first_name' => $updates['first_name'] ?? (string) ($existing['first_name'] ?? ''),
|
||||||
'last_name' => $updates['last_name'] ?? (string) ($existing['last_name'] ?? ''),
|
'last_name' => $updates['last_name'] ?? (string) ($existing['last_name'] ?? ''),
|
||||||
|
|||||||
@@ -197,6 +197,8 @@ class MicrosoftOidcService
|
|||||||
'preferred_language' => trim((string) ($claims['preferred_language'] ?? '')),
|
'preferred_language' => trim((string) ($claims['preferred_language'] ?? '')),
|
||||||
'graph_given_name' => trim((string) ($graphProfile['given_name'] ?? '')),
|
'graph_given_name' => trim((string) ($graphProfile['given_name'] ?? '')),
|
||||||
'graph_family_name' => trim((string) ($graphProfile['family_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_phone' => trim((string) ($graphProfile['phone'] ?? '')),
|
||||||
'graph_mobile' => trim((string) ($graphProfile['mobile'] ?? '')),
|
'graph_mobile' => trim((string) ($graphProfile['mobile'] ?? '')),
|
||||||
'graph_avatar_mime' => trim((string) ($graphAvatar['mime'] ?? '')),
|
'graph_avatar_mime' => trim((string) ($graphAvatar['mime'] ?? '')),
|
||||||
@@ -368,7 +370,7 @@ class MicrosoftOidcService
|
|||||||
|
|
||||||
private function fetchMicrosoftGraphProfile(string $accessToken): array
|
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, '', [
|
$response = $this->oidcHttpGateway->call('GET', $url, '', [
|
||||||
'Accept' => 'application/json',
|
'Accept' => 'application/json',
|
||||||
'Authorization' => 'Bearer ' . $accessToken,
|
'Authorization' => 'Bearer ' . $accessToken,
|
||||||
@@ -393,6 +395,8 @@ class MicrosoftOidcService
|
|||||||
'profile' => [
|
'profile' => [
|
||||||
'given_name' => trim((string) ($data['givenName'] ?? '')),
|
'given_name' => trim((string) ($data['givenName'] ?? '')),
|
||||||
'family_name' => trim((string) ($data['surname'] ?? '')),
|
'family_name' => trim((string) ($data['surname'] ?? '')),
|
||||||
|
'display_name' => trim((string) ($data['displayName'] ?? '')),
|
||||||
|
'job_title' => trim((string) ($data['jobTitle'] ?? '')),
|
||||||
'mobile' => trim((string) ($data['mobilePhone'] ?? '')),
|
'mobile' => trim((string) ($data['mobilePhone'] ?? '')),
|
||||||
'phone' => $phone,
|
'phone' => $phone,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ class SsoUserLinkService
|
|||||||
'last_name' => trim((string) ($claims['family_name'] ?? '')) !== ''
|
'last_name' => trim((string) ($claims['family_name'] ?? '')) !== ''
|
||||||
? trim((string) ($claims['family_name'] ?? ''))
|
? trim((string) ($claims['family_name'] ?? ''))
|
||||||
: trim((string) ($claims['graph_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'] ?? '')),
|
'phone' => trim((string) ($claims['graph_phone'] ?? '')),
|
||||||
'mobile' => trim((string) ($claims['graph_mobile'] ?? '')),
|
'mobile' => trim((string) ($claims['graph_mobile'] ?? '')),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class TenantSsoService
|
|||||||
private const PROFILE_SYNC_FIELD_OPTIONS = [
|
private const PROFILE_SYNC_FIELD_OPTIONS = [
|
||||||
'first_name' => 'Sync first name',
|
'first_name' => 'Sync first name',
|
||||||
'last_name' => 'Sync last name',
|
'last_name' => 'Sync last name',
|
||||||
|
'display_name' => 'Sync display name',
|
||||||
|
'job_title' => 'Sync job title',
|
||||||
'phone' => 'Sync phone',
|
'phone' => 'Sync phone',
|
||||||
'mobile' => 'Sync mobile',
|
'mobile' => 'Sync mobile',
|
||||||
'avatar' => 'Sync avatar image',
|
'avatar' => 'Sync avatar image',
|
||||||
@@ -459,7 +461,9 @@ class TenantSsoService
|
|||||||
$fields = $this->normalizeProfileSyncFields($fields);
|
$fields = $this->normalizeProfileSyncFields($fields);
|
||||||
return in_array('phone', $fields, true)
|
return in_array('phone', $fields, true)
|
||||||
|| in_array('mobile', $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
|
public function shouldAutoRememberOnMicrosoftLogin(int $tenantId): bool
|
||||||
@@ -784,7 +788,7 @@ class TenantSsoService
|
|||||||
if ($enabled && $baseDn === '') {
|
if ($enabled && $baseDn === '') {
|
||||||
$errors[] = t('LDAP base DN is required');
|
$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');
|
$errors[] = t('LDAP port must be between 1 and 65535');
|
||||||
}
|
}
|
||||||
if ($enabled && !str_contains($userSearchFilter, '%s')) {
|
if ($enabled && !str_contains($userSearchFilter, '%s')) {
|
||||||
@@ -832,7 +836,11 @@ class TenantSsoService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$autoRememberMode = $this->normalizeAutoRememberMode($input['ldap_auto_remember_mode'] ?? null);
|
$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, [
|
$saved = $this->tenantLdapAuthRepository->upsertByTenantId($tenantId, [
|
||||||
'enabled' => $enabled,
|
'enabled' => $enabled,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ if ($rawAllowUserThemeMode === null || $rawAllowUserThemeMode === '') {
|
|||||||
} else {
|
} else {
|
||||||
$tenantAllowUserThemeMode = '0';
|
$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);
|
$microsoftEnabled = in_array(strtolower(trim((string) $microsoftEnabledRaw)), ['1', 'true', 'yes', 'on'], true);
|
||||||
$microsoftEnforceRaw = $values['enforce_microsoft_login'] ?? false;
|
$microsoftEnforceRaw = $values['enforce_microsoft_login'] ?? false;
|
||||||
$microsoftEnforce = in_array(strtolower(trim((string) $microsoftEnforceRaw)), ['1', 'true', 'yes', 'on'], true);
|
$microsoftEnforce = in_array(strtolower(trim((string) $microsoftEnforceRaw)), ['1', 'true', 'yes', 'on'], true);
|
||||||
@@ -686,11 +686,12 @@ $openOverrideCard = $detailsOpenAll;
|
|||||||
<?php
|
<?php
|
||||||
// ── LDAP configuration section ────────────────────────────────
|
// ── LDAP configuration section ────────────────────────────────
|
||||||
$ldapUiState = is_array($ldapUiState ?? null) ? $ldapUiState : [];
|
$ldapUiState = is_array($ldapUiState ?? null) ? $ldapUiState : [];
|
||||||
$ldapEnabled = !empty($values['enabled'] ?? false) ? false : (!empty($values['ldap_enabled'] ?? $ldapConfig['enabled'] ?? false));
|
|
||||||
if (isset($values['ldap_enabled'])) {
|
if (isset($values['ldap_enabled'])) {
|
||||||
$ldapEnabled = in_array(strtolower(trim((string) $values['ldap_enabled'])), ['1', 'true', 'yes', 'on'], true);
|
$ldapEnabled = in_array(strtolower(trim((string) $values['ldap_enabled'])), ['1', 'true', 'yes', 'on'], true);
|
||||||
} elseif (isset($ldapConfig['enabled'])) {
|
} elseif (isset($ldapConfig['enabled'])) {
|
||||||
$ldapEnabled = !empty($ldapConfig['enabled']);
|
$ldapEnabled = !empty($ldapConfig['enabled']);
|
||||||
|
} else {
|
||||||
|
$ldapEnabled = false;
|
||||||
}
|
}
|
||||||
$ldapEnforce = !empty($values['enforce_ldap_login'] ?? $ldapConfig['enforce_ldap_login'] ?? false);
|
$ldapEnforce = !empty($values['enforce_ldap_login'] ?? $ldapConfig['enforce_ldap_login'] ?? false);
|
||||||
$ldapHost = trim((string) ($values['ldap_host'] ?? $values['host'] ?? $ldapConfig['host'] ?? ''));
|
$ldapHost = trim((string) ($values['ldap_host'] ?? $values['host'] ?? $ldapConfig['host'] ?? ''));
|
||||||
|
|||||||
@@ -112,6 +112,14 @@ if ($request->isMethod('POST')) {
|
|||||||
'tenant_sso_create_failed'
|
'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');
|
$action = (string) ($post['action'] ?? 'create');
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ app(\MintyPHP\Service\Audit\AuditMetadataEnricherInterface::class)->enrich($tena
|
|||||||
|
|
||||||
$errorBag = formErrors();
|
$errorBag = formErrors();
|
||||||
$errors = [];
|
$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()) {
|
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
|
||||||
Flash::error(t('Form expired, please try again'), $editTarget, 'csrf_expired');
|
Flash::error(t('Form expired, please try again'), $editTarget, 'csrf_expired');
|
||||||
|
|||||||
Reference in New Issue
Block a user