First public release. - docs: document the audit log, user offboarding, CSV export and the "require a reason for destructive actions" setting across README, docs/ and CLAUDE/AGENTS.md; note retention for panel_audit - docs: fill the publishing placeholders (repo URL, security contact, issue-template links) and add a semantic-versioning policy - move agent/contributor context into CLAUDE/ (AGENTS.md + a README); root CLAUDE.md is now a thin @-import pointer - add a favicon (shield-lock, matching the login screen) to the login page and the app layout - CHANGELOG: promote [Unreleased] to [1.0.0]; keep the earlier internal build as [0.1.0] Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfuvfwexztdS7LDpGuKsuK
149 lines
4.7 KiB
PHP
149 lines
4.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Carbon\Carbon;
|
|
use LdapRecord\Models\ActiveDirectory\User;
|
|
|
|
/**
|
|
* Zestaw pomocniczych metod do odczytu "surowych" atrybutów Active Directory,
|
|
* które nie są prostymi łańcuchami znaków (bitowe flagi, znaczniki czasu
|
|
* w formacie Windows FileTime itd.).
|
|
*/
|
|
class LdapAttributeHelper
|
|
{
|
|
// Wybrane flagi bitowe userAccountControl (RFC/MS-ADTS).
|
|
public const UAC_ACCOUNTDISABLE = 0x0002;
|
|
|
|
public const UAC_DONT_EXPIRE_PASSWORD = 0x10000;
|
|
|
|
public const UAC_LOCKOUT = 0x0010;
|
|
|
|
public const UAC_SERVER_TRUST_ACCOUNT = 0x1000; // konto komputera / kontrolera domeny
|
|
|
|
public static function userAccountControl(User $user): int
|
|
{
|
|
return (int) ($user->getFirstAttribute('userAccountControl') ?? 0);
|
|
}
|
|
|
|
public static function isDisabled(User $user): bool
|
|
{
|
|
return (bool) (self::userAccountControl($user) & self::UAC_ACCOUNTDISABLE);
|
|
}
|
|
|
|
public static function passwordNeverExpires(User $user): bool
|
|
{
|
|
return (bool) (self::userAccountControl($user) & self::UAC_DONT_EXPIRE_PASSWORD);
|
|
}
|
|
|
|
/**
|
|
* Konto jest zablokowane (lockout), jeśli lockoutTime jest różne od zera.
|
|
*/
|
|
public static function isLocked(User $user): bool
|
|
{
|
|
$lockoutTime = $user->getFirstAttribute('lockouttime');
|
|
|
|
return $lockoutTime !== null && (int) $lockoutTime !== 0;
|
|
}
|
|
|
|
/**
|
|
* Konwersja znacznika czasu Windows FileTime (100-nanosekundowe
|
|
* interwały od 1601-01-01) na obiekt Carbon. Wartości 0 lub
|
|
* 9223372036854775807 (nigdy) zwracają null.
|
|
*/
|
|
public static function windowsTimestampToCarbon(mixed $value): ?Carbon
|
|
{
|
|
// LdapRecord dla modeli AD sam rzutuje pola typu pwdLastSet na obiekty
|
|
// daty - wtedy nie ma czego konwertować.
|
|
if ($value instanceof \DateTimeInterface) {
|
|
return Carbon::instance($value);
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
$value = $value[0] ?? null;
|
|
}
|
|
|
|
$value = (string) $value;
|
|
|
|
if ($value === '' || $value === '0' || $value === '9223372036854775807') {
|
|
return null;
|
|
}
|
|
|
|
// Różnica w sekundach między 1601-01-01 a 1970-01-01 (epoka Unix).
|
|
$unixTimestamp = intdiv((int) $value, 10_000_000) - 11_644_473_600;
|
|
|
|
if ($unixTimestamp < 0) {
|
|
return null;
|
|
}
|
|
|
|
return Carbon::createFromTimestamp($unixTimestamp);
|
|
}
|
|
|
|
public static function passwordLastSet(User $user): ?Carbon
|
|
{
|
|
return self::windowsTimestampToCarbon($user->getFirstAttribute('pwdlastset'));
|
|
}
|
|
|
|
/**
|
|
* Data wygaśnięcia KONTA (accountExpires). Wartości 0 i 0x7FFFFFFFFFFFFFFF
|
|
* oznaczają "nigdy". Czytamy surowy atrybut, bo rzutowanie LdapRecord
|
|
* potrafi zamienić "nigdy" na datę z roku 1601 / 9999.
|
|
*/
|
|
public static function accountExpiresAt(User $user): ?Carbon
|
|
{
|
|
$raw = $user->getAttributes()['accountexpires'][0] ?? null;
|
|
|
|
if ($raw === null || $raw === '' || $raw === '0' || $raw === '9223372036854775807') {
|
|
return null;
|
|
}
|
|
|
|
$carbon = self::windowsTimestampToCarbon($raw);
|
|
|
|
return ($carbon && $carbon->year > 1601 && $carbon->year < 9000) ? $carbon : null;
|
|
}
|
|
|
|
public const ACCOUNT_EXPIRES_NEVER = '9223372036854775807';
|
|
|
|
/**
|
|
* Zamienia datę na wartość accountExpires w formacie Windows FileTime
|
|
* (18-19 cyfr - LdapRecord zapisze ją bez modyfikacji).
|
|
*/
|
|
public static function dateToWindowsTimestamp(\DateTimeInterface $date): string
|
|
{
|
|
return (string) (($date->getTimestamp() + 11_644_473_600) * 10_000_000);
|
|
}
|
|
|
|
/**
|
|
* Wylicza planowaną datę wygaśnięcia hasła na podstawie polityki
|
|
* maksymalnego wieku hasła (w dniach), pobranej z domeny.
|
|
*/
|
|
public static function passwordExpiresAt(User $user, ?int $maxPasswordAgeDays): ?Carbon
|
|
{
|
|
if (self::passwordNeverExpires($user) || ! $maxPasswordAgeDays) {
|
|
return null;
|
|
}
|
|
|
|
$lastSet = self::passwordLastSet($user);
|
|
|
|
return $lastSet?->copy()->addDays($maxPasswordAgeDays);
|
|
}
|
|
|
|
/**
|
|
* Zwraca krótki, tekstowy status konta do wyświetlenia w tabeli.
|
|
*
|
|
* @return array{label: string, css: string}
|
|
*/
|
|
public static function statusBadge(User $user): array
|
|
{
|
|
if (self::isLocked($user)) {
|
|
return ['label' => __('common.account_status.locked'), 'css' => 'badge-status-locked'];
|
|
}
|
|
|
|
if (self::isDisabled($user)) {
|
|
return ['label' => __('common.account_status.disabled'), 'css' => 'badge-status-disabled'];
|
|
}
|
|
|
|
return ['label' => __('common.account_status.active'), 'css' => 'badge-status-active'];
|
|
}
|
|
}
|