Files
ADconsle/app/Services/LdapAttributeHelper.php
T
kzbkowskiandClaude Sonnet 5 1126d96271
CI / PHP 8.2 (push) Failing after 2m46s
CI / PHP 8.3 (push) Failing after 59s
CI / PHP 8.4 (push) Failing after 1m7s
ADconsole v1.0.0
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
2026-09-08 23:14:39 +02:00

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'];
}
}