instances added god may help

This commit is contained in:
2026-02-23 12:58:19 +01:00
parent 25370a1a55
commit 99db252f60
290 changed files with 9858 additions and 4914 deletions

View File

@@ -3,13 +3,11 @@
namespace MintyPHP\Service\Auth;
use MintyPHP\Repository\Tenant\TenantMicrosoftAuthRepository;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\Settings\SettingService;
use MintyPHP\Support\Crypto;
class TenantSsoService
{
public const PROVIDER_MICROSOFT = 'microsoft';
private const PROFILE_SYNC_FIELD_OPTIONS = [
'first_name' => 'Sync first name',
'last_name' => 'Sync last name',
@@ -17,9 +15,23 @@ class TenantSsoService
'mobile' => 'Sync mobile',
'avatar' => 'Sync avatar image',
];
private const DEFAULT_PROFILE_SYNC_FIELDS = ['first_name', 'last_name'];
public static function findTenantByLoginSlug(string $slug): ?array
public function __construct(
private readonly AuthTenantGateway $tenantGateway,
private readonly TenantMicrosoftAuthRepository $tenantMicrosoftAuthRepository,
private readonly AuthSettingsGateway $settingsGateway,
private readonly AuthCryptoGateway $cryptoGateway
) {
}
public function microsoftProviderKey(): string
{
return self::PROVIDER_MICROSOFT;
}
public function findTenantByLoginSlug(string $slug): ?array
{
$slug = trim(strtolower($slug));
if ($slug === '') {
@@ -27,18 +39,18 @@ class TenantSsoService
}
if (preg_match('/^[a-f0-9-]{36}$/', $slug)) {
$tenant = TenantRepository::findByUuid($slug);
$tenant = $this->tenantGateway->findByUuid($slug);
if ($tenant && (string) ($tenant['status'] ?? 'active') === 'active') {
return $tenant;
}
}
$matches = [];
foreach (TenantRepository::list() as $tenant) {
foreach ($this->tenantGateway->list() as $tenant) {
if ((string) ($tenant['status'] ?? 'active') !== 'active') {
continue;
}
if (self::slugify((string) ($tenant['description'] ?? '')) === $slug) {
if ($this->slugify((string) ($tenant['description'] ?? '')) === $slug) {
$matches[] = $tenant;
}
}
@@ -50,70 +62,72 @@ class TenantSsoService
return $matches[0];
}
public static function tenantLoginSlug(array $tenant): string
public function tenantLoginSlug(array $tenant): string
{
$slug = self::slugify((string) ($tenant['description'] ?? ''));
$slug = $this->slugify((string) ($tenant['description'] ?? ''));
if ($slug !== '') {
return $slug;
}
return strtolower((string) ($tenant['uuid'] ?? ''));
}
public static function getTenantMicrosoftAuth(int $tenantId): array
public function getTenantMicrosoftAuth(int $tenantId): array
{
$row = TenantMicrosoftAuthRepository::findByTenantId($tenantId) ?? [];
$row = $this->tenantMicrosoftAuthRepository->findByTenantId($tenantId) ?? [];
$syncProfileOnLogin = !empty($row['sync_profile_on_login']);
$syncFields = self::normalizeProfileSyncFields($row['sync_profile_fields'] ?? []);
$syncFields = $this->normalizeProfileSyncFields($row['sync_profile_fields'] ?? []);
if ($syncProfileOnLogin && !$syncFields) {
$syncFields = self::defaultProfileSyncFields();
$syncFields = $this->defaultProfileSyncFields();
}
return [
'enabled' => !empty($row['enabled']),
'enforce_microsoft_login' => !empty($row['enforce_microsoft_login']),
'sync_profile_on_login' => $syncProfileOnLogin,
'sync_profile_fields' => self::profileSyncFieldsCsv($syncFields),
'sync_profile_fields' => $this->profileSyncFieldsCsv($syncFields),
'sync_profile_fields_list' => $syncFields,
'entra_tenant_id' => strtolower(trim((string) ($row['entra_tenant_id'] ?? ''))),
'allowed_domains' => self::normalizeDomains((string) ($row['allowed_domains'] ?? '')),
'allowed_domains' => $this->normalizeDomains((string) ($row['allowed_domains'] ?? '')),
'use_shared_app' => !array_key_exists('use_shared_app', $row) || !empty($row['use_shared_app']),
'client_id_override' => trim((string) ($row['client_id_override'] ?? '')),
'client_secret_override_enc' => trim((string) ($row['client_secret_override_enc'] ?? '')),
];
}
public static function buildMicrosoftUiState(int $tenantId, array $input = []): array
public function buildMicrosoftUiState(int $tenantId, array $input = []): array
{
$existing = self::getTenantMicrosoftAuth($tenantId);
$state = self::normalizeMicrosoftInputState($input, $existing);
$configState = self::evaluateMicrosoftConfigState($state);
$existing = $this->getTenantMicrosoftAuth($tenantId);
$state = $this->normalizeMicrosoftInputState($input, $existing);
$configState = $this->evaluateMicrosoftConfigState($state);
return [
'enabled' => $state['enabled'],
'config_complete' => !$state['enabled'] || $configState['complete'],
'config_error_code' => (string) ($configState['error_code'] ?? ''),
'config_error_label' => self::microsoftConfigErrorLabel((string) ($configState['error_code'] ?? '')),
'config_error_label' => $this->microsoftConfigErrorLabel((string) ($configState['error_code'] ?? '')),
'password_mode' => $state['enabled'] && $state['enforce_microsoft_login']
? 'microsoft_only'
: 'local_and_microsoft',
'credential_source' => $state['use_shared_app'] ? 'shared' : 'override',
'sync_needs_graph' => $state['sync_profile_on_login']
&& self::profileSyncFieldsRequireGraph($state['sync_profile_fields']),
&& $this->profileSyncFieldsRequireGraph($state['sync_profile_fields']),
];
}
public static function isLocalPasswordLoginAllowed(int $tenantId): bool
public function isLocalPasswordLoginAllowed(int $tenantId): bool
{
$auth = self::getTenantMicrosoftAuth($tenantId);
$auth = $this->getTenantMicrosoftAuth($tenantId);
if (!$auth['enabled']) {
return true;
}
return !$auth['enforce_microsoft_login'];
}
public static function resolveTenantLoginMethods(int $tenantId): array
public function resolveTenantLoginMethods(int $tenantId): array
{
$tenant = TenantRepository::find($tenantId);
$tenant = $this->tenantGateway->findById($tenantId);
if (!$tenant || (string) ($tenant['status'] ?? 'active') !== 'active') {
return [
'local' => false,
@@ -122,9 +136,10 @@ class TenantSsoService
];
}
$configResult = self::getEffectiveMicrosoftProviderConfig($tenant);
$configResult = $this->getEffectiveMicrosoftProviderConfig($tenant);
return [
'local' => self::isLocalPasswordLoginAllowed($tenantId),
'local' => $this->isLocalPasswordLoginAllowed($tenantId),
'microsoft' => !empty($configResult['ok']),
'microsoft_reason' => $configResult['ok'] ?? false
? ''
@@ -132,14 +147,14 @@ class TenantSsoService
];
}
public static function getEffectiveMicrosoftProviderConfig(array $tenant): array
public function getEffectiveMicrosoftProviderConfig(array $tenant): array
{
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId <= 0) {
return ['ok' => false, 'error' => 'tenant_not_found'];
}
$auth = self::getTenantMicrosoftAuth($tenantId);
$auth = $this->getTenantMicrosoftAuth($tenantId);
if (!$auth['enabled']) {
return ['ok' => false, 'error' => 'sso_disabled'];
}
@@ -152,9 +167,10 @@ class TenantSsoService
$useSharedApp = $auth['use_shared_app'];
$clientId = '';
$clientSecret = '';
if ($useSharedApp) {
$clientId = (string) (SettingService::getMicrosoftSharedClientId() ?? '');
$clientSecret = (string) (SettingService::getMicrosoftSharedClientSecret() ?? '');
$clientId = $this->settingsGateway->getMicrosoftSharedClientId();
$clientSecret = $this->settingsGateway->getMicrosoftSharedClientSecret();
if (trim($clientId) === '' || trim($clientSecret) === '') {
return ['ok' => false, 'error' => 'shared_credentials_missing'];
}
@@ -168,7 +184,7 @@ class TenantSsoService
}
if ($auth['client_secret_override_enc'] !== '') {
try {
$clientSecret = Crypto::decryptString($auth['client_secret_override_enc']);
$clientSecret = $this->cryptoGateway->decryptString($auth['client_secret_override_enc']);
} catch (\Throwable $exception) {
return ['ok' => false, 'error' => 'secret_invalid'];
}
@@ -180,9 +196,6 @@ class TenantSsoService
$clientId = trim($clientId);
$clientSecret = trim($clientSecret);
if ($clientId === '' || $clientSecret === '') {
return ['ok' => false, 'error' => 'client_credentials_missing'];
}
return [
'ok' => true,
@@ -196,16 +209,16 @@ class TenantSsoService
'sync_profile_on_login' => $auth['sync_profile_on_login'],
'sync_profile_fields' => $auth['sync_profile_fields_list'],
'sync_profile_needs_graph' => $auth['sync_profile_on_login']
&& self::profileSyncFieldsRequireGraph($auth['sync_profile_fields_list']),
'authority' => SettingService::getMicrosoftAuthority(),
&& $this->profileSyncFieldsRequireGraph($auth['sync_profile_fields_list']),
'authority' => $this->settingsGateway->getMicrosoftAuthority(),
'use_shared_app' => $useSharedApp,
],
];
}
public static function saveTenantMicrosoftAuth(int $tenantId, array $input): array
public function saveTenantMicrosoftAuth(int $tenantId, array $input): array
{
$tenant = TenantRepository::find($tenantId);
$tenant = $this->tenantGateway->findById($tenantId);
if (!$tenant) {
return ['ok' => false, 'errors' => [t('Tenant not found')]];
}
@@ -213,13 +226,13 @@ class TenantSsoService
$enabled = !empty($input['microsoft_enabled']);
$enforce = !empty($input['enforce_microsoft_login']);
$entraTenantId = strtolower(trim((string) ($input['entra_tenant_id'] ?? '')));
$allowedDomains = self::normalizeDomains((string) ($input['allowed_domains'] ?? ''));
$allowedDomains = $this->normalizeDomains((string) ($input['allowed_domains'] ?? ''));
$syncProfileOnLogin = !empty($input['sync_profile_on_login']);
$syncProfileFields = self::normalizeProfileSyncFields($input['sync_profile_fields'] ?? []);
$syncProfileFields = $this->normalizeProfileSyncFields($input['sync_profile_fields'] ?? []);
if ($syncProfileOnLogin && !$syncProfileFields) {
$syncProfileFields = self::defaultProfileSyncFields();
$syncProfileFields = $this->defaultProfileSyncFields();
}
$syncProfileFieldsCsv = self::profileSyncFieldsCsv($syncProfileFields);
$syncProfileFieldsCsv = $this->profileSyncFieldsCsv($syncProfileFields);
$useSharedApp = !empty($input['use_shared_app']);
$clientIdOverride = trim((string) ($input['client_id_override'] ?? ''));
$clientSecretOverride = trim((string) ($input['client_secret_override'] ?? ''));
@@ -227,16 +240,16 @@ class TenantSsoService
$errors = [];
$existing = self::getTenantMicrosoftAuth($tenantId);
$existing = $this->getTenantMicrosoftAuth($tenantId);
$clientSecretOverrideEnc = (string) ($existing['client_secret_override_enc'] ?? '');
if ($clearSecret) {
$clientSecretOverrideEnc = '';
} elseif ($clientSecretOverride !== '') {
if (!Crypto::isConfigured()) {
if (!$this->cryptoGateway->isConfigured()) {
$errors[] = t('APP_CRYPTO_KEY is missing or invalid');
} else {
try {
$clientSecretOverrideEnc = Crypto::encryptString($clientSecretOverride);
$clientSecretOverrideEnc = $this->cryptoGateway->encryptString($clientSecretOverride);
} catch (\Throwable $exception) {
$errors[] = t('Client secret could not be encrypted');
}
@@ -253,9 +266,9 @@ class TenantSsoService
'client_id_override' => $clientIdOverride,
'client_secret_override_enc' => $clientSecretOverrideEnc,
];
$configState = self::evaluateMicrosoftConfigState($stateForValidation);
$configState = $this->evaluateMicrosoftConfigState($stateForValidation);
if ($enabled && !($configState['complete'] ?? false)) {
$label = self::microsoftConfigErrorLabel((string) ($configState['error_code'] ?? ''));
$label = $this->microsoftConfigErrorLabel((string) ($configState['error_code'] ?? ''));
if ($label !== '') {
$errors[] = $label;
}
@@ -268,7 +281,7 @@ class TenantSsoService
return ['ok' => false, 'errors' => $errors];
}
$saved = TenantMicrosoftAuthRepository::upsertByTenantId($tenantId, [
$saved = $this->tenantMicrosoftAuthRepository->upsertByTenantId($tenantId, [
'enabled' => $enabled,
'enforce_microsoft_login' => $enabled ? $enforce : false,
'sync_profile_on_login' => $enabled ? $syncProfileOnLogin : false,
@@ -287,7 +300,7 @@ class TenantSsoService
return ['ok' => true];
}
public static function isEmailDomainAllowed(string $email, array $allowedDomains): bool
public function isEmailDomainAllowed(string $email, array $allowedDomains): bool
{
$allowedDomains = array_values(array_filter(array_map(
static fn ($domain): string => strtolower(trim((string) $domain)),
@@ -302,20 +315,21 @@ class TenantSsoService
if (count($parts) !== 2) {
return false;
}
return in_array($parts[1], $allowedDomains, true);
}
public static function profileSyncFieldOptions(): array
public function profileSyncFieldOptions(): array
{
return self::PROFILE_SYNC_FIELD_OPTIONS;
}
public static function defaultProfileSyncFields(): array
public function defaultProfileSyncFields(): array
{
return self::DEFAULT_PROFILE_SYNC_FIELDS;
}
public static function normalizeProfileSyncFields($raw): array
public function normalizeProfileSyncFields($raw): array
{
if (is_string($raw)) {
$raw = preg_split('/[\s,;]+/', $raw) ?: [];
@@ -336,21 +350,21 @@ class TenantSsoService
return array_keys($normalized);
}
public static function profileSyncFieldsCsv(array $fields): string
public function profileSyncFieldsCsv(array $fields): string
{
$fields = self::normalizeProfileSyncFields($fields);
$fields = $this->normalizeProfileSyncFields($fields);
return implode(',', $fields);
}
public static function profileSyncFieldsRequireGraph(array $fields): bool
public function profileSyncFieldsRequireGraph(array $fields): bool
{
$fields = self::normalizeProfileSyncFields($fields);
$fields = $this->normalizeProfileSyncFields($fields);
return in_array('phone', $fields, true)
|| in_array('mobile', $fields, true)
|| in_array('avatar', $fields, true);
}
private static function normalizeDomains(string $raw): string
private function normalizeDomains(string $raw): string
{
$parts = preg_split('/[,\n;\r]+/', $raw) ?: [];
$domains = [];
@@ -369,7 +383,7 @@ class TenantSsoService
return implode(',', $domains);
}
private static function normalizeMicrosoftInputState(array $input, array $existing): array
private function normalizeMicrosoftInputState(array $input, array $existing): array
{
$enabled = array_key_exists('microsoft_enabled', $input)
? !empty($input['microsoft_enabled'])
@@ -381,10 +395,10 @@ class TenantSsoService
? !empty($input['sync_profile_on_login'])
: !empty($existing['sync_profile_on_login']);
$syncProfileFields = array_key_exists('sync_profile_fields', $input)
? self::normalizeProfileSyncFields($input['sync_profile_fields'] ?? [])
: self::normalizeProfileSyncFields($existing['sync_profile_fields_list'] ?? []);
? $this->normalizeProfileSyncFields($input['sync_profile_fields'] ?? [])
: $this->normalizeProfileSyncFields($existing['sync_profile_fields_list'] ?? []);
if ($syncProfileOnLogin && !$syncProfileFields) {
$syncProfileFields = self::defaultProfileSyncFields();
$syncProfileFields = $this->defaultProfileSyncFields();
}
$entraTenantId = array_key_exists('entra_tenant_id', $input)
@@ -417,7 +431,7 @@ class TenantSsoService
];
}
private static function evaluateMicrosoftConfigState(array $state): array
private function evaluateMicrosoftConfigState(array $state): array
{
if (empty($state['enabled'])) {
return ['complete' => true, 'error_code' => ''];
@@ -429,8 +443,8 @@ class TenantSsoService
}
if (!empty($state['use_shared_app'])) {
$sharedClientId = trim((string) (SettingService::getMicrosoftSharedClientId() ?? ''));
$sharedClientSecret = trim((string) (SettingService::getMicrosoftSharedClientSecret() ?? ''));
$sharedClientId = trim($this->settingsGateway->getMicrosoftSharedClientId());
$sharedClientSecret = trim($this->settingsGateway->getMicrosoftSharedClientSecret());
if ($sharedClientId === '' || $sharedClientSecret === '') {
return ['complete' => false, 'error_code' => 'shared_credentials_missing'];
}
@@ -448,7 +462,7 @@ class TenantSsoService
}
if ($secretEnc !== '__pending_secret__') {
try {
$secret = trim((string) Crypto::decryptString($secretEnc));
$secret = trim($this->cryptoGateway->decryptString($secretEnc));
} catch (\Throwable $exception) {
return ['complete' => false, 'error_code' => 'secret_invalid'];
}
@@ -460,7 +474,7 @@ class TenantSsoService
return ['complete' => true, 'error_code' => ''];
}
private static function microsoftConfigErrorLabel(string $errorCode): string
private function microsoftConfigErrorLabel(string $errorCode): string
{
return match ($errorCode) {
'tenant_id_invalid' => t('Entra tenant ID is invalid'),
@@ -473,7 +487,7 @@ class TenantSsoService
};
}
private static function slugify(string $value): string
private function slugify(string $value): string
{
$value = strtolower(trim($value));
if ($value === '') {