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
136 lines
7.7 KiB
Markdown
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).
|