Files
ADconsle/docs/architecture.md
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

7.7 KiB

Architecture

Principles

  1. The directory is the database. No table mirrors AD. Controllers call LdapRecord which talks to a DC per request. Nothing to sync, nothing to reconcile, no stale copy.
  2. The SQL database is infrastructure only — sessions, cache, jobs/job_batches/failed_jobs, the panel's own configuration (panel_settings for general settings, panel_attributes for the AD attribute / column definitions) and the change log (panel_audit). None of these mirror directory state. The default users / password_reset_tokens tables from Laravel's starter migration are unused.
  3. Thin services, fat controllers-per-module. One controller per screen; a handful of small services for the cross-cutting bits.

Request lifecycle

Browser
  │  cookie session (file)  ──► ad_admin profile
  ▼
routes/web.php  (English URL segments and route names)
  │  middleware: auth:ldap  +  App\Http\Middleware\SetLocale
  ▼
App\Http\Controllers\Admin\*Controller
  │  LdapRecord query / model save
  ▼
Active Directory (LDAPS)          Cache::store('file')  ── OU tree / dashboard / org chart

Authentication

  • Guard ldap (config/auth.php) — LdapRecord "plain bind", no Eloquent user provider, no users table.
  • LoginController@login binds to AD with the submitted credentials, then runs App\Ldap\Rules\MemberOfLoginGroup: the account must be a member of AD_LOGIN_GROUP (accepts full DN or CN; walks nested groups; includes the primary group).
  • On success the sanitised profile from App\Ldap\User::toPanelProfile() is written to the session key ad_admin; every panel route group is behind auth:ldap.
  • Logout clears the session. Sessions are file-driver by default.

Key classes

Class Responsibility
App\Ldap\User AD user model. attributeMap() (config/ldap.php → field names), panelAttributes() (the attribute list every user query selects), toPanelProfile().
App\Ldap\Rules\MemberOfLoginGroup Login authorisation.
App\Services\PanelSettingsService The one entry point for panel config. General settings ← panel_settings (JSON row panel, merged with defaults()); attribute / column definitions ← panel_attributes (one row each, ordered by position, merged with defaultAttributes()), normalised with the locked rows enforced.
App\Models\PanelSetting / App\Models\PanelAttribute Eloquent models for the two config tables. Don't read them from controllers — go through PanelSettingsService.
App\Services\AttributeCipher Laravel-Crypt-compatible encrypt/decrypt for attributes typed encrypted; key from the panel (or a pasted APP_KEY).
App\Services\LdapAttributeHelper Decodes userAccountControl bit flags, Windows FileTime ↔ Carbon, accountExpires / pwdLastSet "never" sentinels, derived status badge, passwordExpiresAt().
App\Services\OuTreeService Builds the sidebar OU tree (with object counts), cached on the file store; flush() on demand.
App\Services\AuditLogger Writes one panel_audit row per panel action (actor from the ldap guard, action slug, target, optional reason + changes/context JSON). Best-effort: a write failure is report()ed, never propagated, so auditing can't break an AD operation.
App\Http\Middleware\SetLocale Locale from session → cookie → APP_LOCALE.

Modules

Route prefix Controller Notes
/dashboard DashboardController Streams every user with chunk(), aggregates in PHP, caches the result 5 min (dashboard:summary on the file store) with a manual refresh.
/users UserController List + OU tree + search; CRUD; password reset / OU move / expiry — single and bulk; filter= query for the dashboard tiles; CSV export of the matching list (GET /users/export, streamed, UTF-8 BOM, one column per configured attribute); offboarding (offboardOne / bulkOffboard → runOffboard: attribute changes → group removal → OU move, in that order because the move changes the DN).
/computers ComputerController List + OU tree; details include LAPS (legacy ms-Mcs-AdmPwd and Windows LAPS msLAPS-Password) and BitLocker (msFVE-RecoveryInformation); trust-account reset; enable/disable; delete.
/groups GroupController List with type/scope filter; create; edit description & mail; membership add/remove; delete.
/organisation OrgChartController manager → directReports; rendered client-side with d3-org-chart (CDN); cached 10 min.
/gpo GpoController Read-only: groupPolicyContainer objects, gPLink on OUs, simplified RSoP.
/audit AuditController Read-only view of panel_audit with filters (actor / action / object / date range). No write route.
/settings ConfigController General settings + attribute definitions → PanelSettingsService.

Audit log

Every write the panel performs against AD calls AuditLogger::log($action, $opts), which appends a row to panel_audit:

  • actor / actor_name — the signed-in admin (sAMAccountName + cn from the ldap guard; falls back to the ad_admin session profile, then system).
  • action — a stable slug: user.disable, user.reset_password, user.offboard, user.bulk_move, computer.laps_view, group.member_add, config.settings, … (viewing a LAPS / BitLocker secret is audited too).
  • target_type / target / target_dn — the object acted on.
  • reason — free text. Optional by default; the Audit log settings block has a "require a reason for destructive actions" switch (panel_settings → audit.require_reason) that makes it mandatory for disable / delete / move / offboard. A row is written either way.
  • changes / context — JSON: a before/after diff, and extras such as the number of accounts in a bulk action or the list of offboarding steps run.
  • ip, created_at.

The table is read only by AuditController (filter + paginate) and never by the rest of the app. A failed insert is report()ed and swallowed — auditing can never abort an AD operation. Nothing prunes the table; see deployment.md for retention.

Caching

App-level caches use Cache::store('file') explicitly (independent of CACHE_STORE):

Key TTL Refresh
ou_tree:{scope} 10 min ?refresh_ou=1 in a module, or OuTreeService::flush()
dashboard:summary (+ …_ts) 5 min "Recalculate now" button (POST /dashboard/refresh)
org_chart:v3 (+ …_ts) 10 min "Refresh" on the org chart

Some user mutations also bust org_chart:v3 and dashboard:summary.

Front-end

  • Bootstrap 5 + Bootstrap Icons from a CDN.
  • public/css/style.css and public/js/main.js are hand-written and served directly — there is no Vite build to run for the app to work. (resources/js, resources/css and vite.config.js exist for anyone who wants to add a bundled asset pipeline.)
  • Light/dark theme via data-bs-theme on <html>, remembered in localStorage.

Internationalisation

  • lang/pl/*.php and lang/en/*.php, structured by namespace (nav, users, computers, groups, gpo, dashboard, config, common, auth, pages, …). Access with __('namespace.key').
  • config/locales.php is the single list of available languages.
  • Switchers on the login page, the navbar and the footer; GET /language/{locale} (LocaleController@switch) persists the choice.

What is intentionally not here

  • No local mirror / cache of AD objects.
  • No GPO content editing (SYSVOL over SMB).
  • No background jobs or scheduler.
  • No REST API (server-rendered Blade only; some endpoints return partial HTML or JSON for the live-search / typeahead helpers).