Files
ADconsle/app/Ldap/Rules/MemberOfLoginGroup.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

99 lines
2.8 KiB
PHP

<?php
namespace App\Ldap\Rules;
use Illuminate\Database\Eloquent\Model as Eloquent;
use LdapRecord\Laravel\Auth\Rule;
use LdapRecord\Models\Model as LdapRecord;
/**
* Reguła autoryzacji logowania do panelu.
*
* Po poprawnym bindzie (login + hasło) LdapRecord sprawdza jeszcze tę regułę.
* Do panelu wpuszczamy wyłącznie członków grupy z config('ldap.auth.login_group')
* (wartość z AD_LOGIN_GROUP). Grupę można podać jako pełny DN (zalecane) lub samą
* nazwę (CN). Sprawdzane jest członkostwo:
* - bezpośrednie (atrybut memberOf),
* - zagnieżdżone (grupa w grupie) oraz grupa podstawowa - przez relację groups().
*
* Brak skonfigurowanej grupy => nikt się nie zaloguje (fail-closed).
*/
class MemberOfLoginGroup implements Rule
{
public function passes(LdapRecord $user, ?Eloquent $model = null): bool
{
$required = trim((string) config('ldap.auth.login_group'));
if ($required === '') {
return false;
}
$isDn = str_contains($required, '=');
$needle = $isDn ? $this->normalizeDn($required) : $this->normalizeCn($required);
// Szybka ścieżka: bezpośrednie członkostwo z atrybutu memberOf.
foreach ((array) $user->getAttribute('memberof') as $dn) {
if ($this->matches((string) $dn, $needle, $isDn)) {
return true;
}
}
// Pełna ścieżka: członkostwo zagnieżdżone + grupa podstawowa.
foreach ($this->nestedGroupDns($user) as $dn) {
if ($this->matches($dn, $needle, $isDn)) {
return true;
}
}
return false;
}
/**
* DN wszystkich grup użytkownika z uwzględnieniem zagnieżdżenia.
*
* @return list<string>
*/
protected function nestedGroupDns(LdapRecord $user): array
{
if (! method_exists($user, 'groups')) {
return [];
}
return $user->groups()->recursive()->get(['cn'])
->map(fn ($group) => (string) $group->getDn())
->all();
}
private function matches(string $groupDn, string $needle, bool $isDn): bool
{
if ($groupDn === '') {
return false;
}
return $isDn
? $this->normalizeDn($groupDn) === $needle
: $this->normalizeCn($this->firstRdn($groupDn)) === $needle;
}
private function firstRdn(string $dn): string
{
return explode(',', $dn, 2)[0];
}
private function normalizeCn(string $value): string
{
$value = trim($value);
if (stripos($value, 'cn=') === 0) {
$value = substr($value, 3);
}
return mb_strtolower(trim($value));
}
private function normalizeDn(string $dn): string
{
return mb_strtolower(preg_replace('/\s*,\s*/', ',', trim($dn)));
}
}