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
99 lines
2.8 KiB
PHP
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)));
|
|
}
|
|
}
|