it_limits_tree_to_default_user_and_computer_ous only mocked OrganizationalUnit::query(); attachCounts() then called the real User/Computer query via countObjectsInOu(), which threw "Can't contact LDAP server" on any checkout whose .env points LDAP_HOSTS at an unreachable DC (i.e. every CI run since v1.0.0). Stub countObjectsInOu() on a partial mock, like the sibling test already does — the test only asserts tree shape, not counts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKT9p3bfP8RZesExdYCS52
ADconsole
A self-hosted web panel for day-to-day Active Directory administration — users, computers, groups and read-only Group Policy — built on Laravel 11 and LdapRecord.
ADconsole gives helpdesk and sysadmin staff a clean UI for the tasks that
otherwise mean RSAT, dsa.msc or PowerShell: reset a password, unlock an
account, move an object between OUs, add someone to a group, check who has a
stale password. It talks directly to a domain controller over LDAP — there
is no database copy of your directory to drift out of sync.
- No AD data at rest. Every read and write goes straight to a DC through LdapRecord. The SQL database only holds the app's own sessions, cache and panel settings.
- Domain login, group-gated. Admins sign in with their own AD account; access is limited to members of one AD group you nominate.
- Bilingual UI — Polish and English, switchable per session.
- Deploys in minutes — a
docker compose upstack, or drop it on any PHP 8.4+ host.
⚠️ ADconsole performs privileged directory operations (password resets, account enable/disable, deletions). Read
SECURITY.mdand the hardening checklist before exposing it.
Features
| Module | What it does |
|---|---|
Dashboard (/dashboard) |
Domain health tiles (disabled / locked / stale-password / inactive / expired accounts, object counts), password-age histogram, "expiring soon / inactive / newly created" mini-reports. Tiles link straight to the matching filtered list. Cached, recalculated on demand. |
Users (/users) |
OU tree + full-text search, create, view/edit (configurable attribute set), password reset (single and bulk), move OU (single and bulk), password-expiry policy, enable/disable, group membership, photo, CSV export of the current (filtered) list, and a one-click offboarding flow (disable + randomise password + strip group memberships + move OU + set expiry, in a safe order). |
Computers (/computers) |
OU tree, OS filter, LAPS (legacy ms-Mcs-AdmPwd + Windows LAPS) and BitLocker recovery keys, reset trust account, enable/disable the object, delete. |
Groups (/groups) |
Type/scope filter, create, edit description & mail, manage members, delete. |
Org chart (/organisation) |
manager → directReports hierarchy rendered with d3-org-chart. |
GPO (/gpo) |
Read-only list of Group Policy objects and their OU links, plus a simplified RSoP lookup. Editing policy content (SYSVOL) is out of scope. |
Audit log (/audit) |
Read-only record of every change the panel makes to AD — actor, action, target, optional reason and a before/after diff — filterable by admin, action, object and date. Stored in the panel_audit table (application data, never AD). |
Configuration (/settings) |
Password policy & generator, attribute-encryption key, an optional "require a reason for destructive actions" switch, and the definition of which AD attributes show up as list columns / profile fields. Stored in the panel_settings / panel_attributes tables. |
Full walkthrough: docs/architecture.md.
Requirements
- PHP 8.4+ with
ldap,pdo_mysql(or your DB driver),mbstring,curl,zip,openssl - Composer 2
- A SQL database (MariaDB/MySQL recommended; SQLite/PostgreSQL work too) — for sessions, cache and panel settings only
- Network reachability to a domain controller, ideally over LDAPS (636) or STARTTLS — password writes fail on an unencrypted connection
- An AD service account that can read your directory and modify the objects
you want to manage (see
docs/active-directory.md) - For the container image: Docker + Docker Compose v2
Quick start — Docker
git clone https://gitea.kzbikowski.pl/kzbkowski/ADconsle.git adconsole && cd adconsole
cp .env.example .env
# edit .env: APP_URL/APP_DOMAIN, DB_* passwords, and the whole LDAP_* / AD_* block
# Production — pulls the released image, external Traefik proxy. Run from the
# repo root and point Compose at the repo-root .env:
docker compose --env-file .env -f docker/compose.yaml up -d
# Or without a proxy — publish a local port instead:
cp docker/compose.override.yaml.template docker/compose.override.yaml
docker compose --env-file .env -f docker/compose.yaml -f docker/compose.override.yaml up -d
# -> http://localhost:8080 (set APP_URL to match)
# Live-reload dev stack (bind-mounts the working tree):
docker compose --env-file .env -f docker/compose.dev.yaml up --build
docker/compose.yaml runs the self-contained image the Gitea release workflow
publishes (…/adconsole:1.1.0); up -d pulls it, --build rebuilds from your
tree. Only .env and docker/data/ (DB + storage) are mounted. The entrypoint
generates APP_KEY, runs migrations and warms the caches on first boot. Details:
docs/deployment.md.
Quick start — bare metal
git clone https://gitea.kzbikowski.pl/kzbkowski/ADconsle.git adconsole && cd adconsole
composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate
# edit .env (LDAP_* / AD_* / DB_*)
php artisan migrate
php artisan config:cache && php artisan route:cache && php artisan view:cache
# point your web server's document root at ./public
# (dev only:) php artisan serve
Step-by-step, including Apache/nginx vhosts: docs/installation.md.
Configuration at a glance
| Where | What | How |
|---|---|---|
.env |
Connection & deployment settings: DC hosts, service account, base DN, default OUs, login group, DB, app URL. | Edit the file, then php artisan config:cache. Every key is documented in .env.example and docs/configuration.md. |
Database (/settings) |
In-app settings: password policy/generator + encryption key (panel_settings), and the AD attribute / column definitions (panel_attributes). |
Edited in the UI; persisted via App\Services\PanelSettingsService. |
Who can log in is AD_LOGIN_GROUP. What the service account may change is set by
AD delegation, not by ADconsole — see docs/active-directory.md.
How it works (short version)
config/ldap.phpdefines one connection fromLDAP_*.App\Ldap\Userextends the LdapRecord AD user model.- Login = LdapRecord plain-bind with the typed credentials; then
App\Ldap\Rules\MemberOfLoginGroupchecks membership ofAD_LOGIN_GROUP(nested + primary group aware). Success storesad_adminin the session; all/…panel routes sit behindauth:ldap. - Controllers query/modify AD live. Heavy reads (OU trees, dashboard, org chart)
are cached on the
filestore for a few minutes with a manual "recalculate" button. LdapAttributeHelperdecodes the awkward bits (userAccountControl bit flags, Windows FileTime timestamps,accountExpires"never" sentinels).- Sensitive attributes (e.g. cloud extension attributes) can be stored encrypted
with a key set in the panel —
App\Services\AttributeCipher.
More: docs/architecture.md.
Tests
php artisan test # ~40 tests, SQLite :memory:
vendor/bin/pint --test # code style
A PHP 8.5 PDO::MYSQL_ATTR_SSL_CA deprecation makes PHPUnit tag every test
DEPR — that is noise, not a failure.
Project layout
app/
Http/Controllers/Admin/ one controller per module
Ldap/ AD user model + login rule
Services/ PanelSettings, AttributeCipher, LdapAttributeHelper, OuTree
Models/ PanelSetting (JSON config row) + PanelAttribute (one row per attribute)
config/ldap.php connection, login group, default OUs, attribute map
resources/views/admin/ Blade UI (+ components/ + partials/)
public/css/style.css hand-written styles (no build step)
public/js/main.js hand-written JS
routes/web.php English URL segments and route names
lang/{pl,en}/ structured translation keys
docs/ installation / AD / deployment / architecture / …
docker/compose.yaml production stack (self-contained image + MariaDB)
docker/compose.dev.yaml live-reload dev stack (bind-mounts the working tree)
docker/php/ PHP 8.4 + Apache Dockerfile (base/dev/prod) + entrypoint
Documentation
Everything lives in docs/:
installation ·
configuration ·
Active Directory setup ·
architecture ·
deployment ·
development ·
troubleshooting ·
upgrading ·
publishing.
Project meta: changelog · contributing · security policy.
Roadmap / non-goals
Planned: server-side password-policy validation before write; optional TOTP for off-LAN login; export of the audit log.
Out of scope: editing GPO content (SYSVOL/SMB); being a general LDAP browser; managing non-AD directories.
License & legal
Released under the MIT License — provided "as is", without warranty.
Copyright (c) 2026 Kacper Żbikowski.
ADconsole is built on the Laravel framework (MIT) and the
laravel/laravel application skeleton (MIT);
the skeleton's MIT terms are satisfied by this repository's own
LICENSE. Third-party PHP and JavaScript dependencies are pulled in
at build time by Composer / a CDN and are not vendored in this repository;
each keeps its own licence (all permissive — MIT / BSD / Apache-2.0). Run
composer licenses after composer install for the authoritative list.
Active Directory, Windows, Windows Server and Group Policy are trademarks of Microsoft Corporation. ADconsole is an independent project and is not affiliated with, endorsed or sponsored by Microsoft.
When you deploy ADconsole your organisation is the data controller for the
personal data it reads from AD (names, logins, e-mail, phone, photo, …). You are
responsible for the lawful basis, retention and data-subject rights; restrict
panel access to authorised admins and always use LDAPS/STARTTLS. See the
in-app About → Legal page and SECURITY.md.
Contributing
Issues and PRs welcome — see CONTRIBUTING.md and the
CODE_OF_CONDUCT.md. Security reports:
SECURITY.md.