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

136 lines
7.7 KiB
Markdown

# 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](deployment.md#data--backups) 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).