Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`['a', '']`, and the empty string was passed on as a database, user, role, ability or event name.
The six `(array)` casts are now `arrayOption()`, which trims and drops empties.

- **`docs/tools/commands.md` documented a `db-console:<command>` alias for every command, and
`docs/tools/api.md` named the API middleware `db-console.api-guard`.** No command declares an
alias, and the middleware is `laranail-db-console.api-guard`.

### Changed

- The base `DBConsoleCommand` applies `laranail/package-tools`' `Commands\Concerns\ReadsOptions`;
Expand All @@ -29,10 +33,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
method cannot narrow a protected one. Removing it is behaviour-preserving: both return `''` for
a non-string value.

- **The gate abilities and the install command are vendor-scoped.** The 21 abilities are
`laranail-db-console.<permission>` (was `db-console.<permission>`), and the install command is
`laranail::db-console.install` (was `db-console:install`). Both lived in flat, host-owned
registries, where a sibling package or the application claiming the same key silently replaces
it. `ConsolePermission::ability()` returns the scoped name, so the audit trail records it as the
target of a denied action, and `install` seeds permission rows under it. Rows stored before the
rename keep their names and still resolve. Requires `laranail/package-tools ^0.1.3`.

### Added

- **`ConsolePermission::fromAbility()`** reads a gate ability or stored permission name in either
form, and **`deprecatedAbility()`** names the pre-0.1 ability. Both RBAC drivers read stored names
through `fromAbility()`.
- **`tests/Feature/NamingConventionTest.php`** reads the live gate, Artisan and middleware
registries through package-tools' `AssertsRegisteredNames`, and checks every deprecated alias
answers as its replacement and announces itself once.

- **`assertNoNullOnlyOptionGuards()` is enforced over `src/`.**

### Deprecated

- **Gate abilities `db-console.<permission>` (21).** Each is still defined and asks the gate for its
`laranail-db-console.*` ability, so a host's definition or before/after callback for the scoped
name governs the old one too. The first check of each raises one `E_USER_DEPRECATED` notice.
- **Command `db-console:install`.** Still registered, hidden; it prints one line naming
`laranail::db-console.install`, then runs it and returns its exit code.

Both are removed no earlier than the next minor after 0.1.

## [0.1.0] - 2026-07-11

Initial public release.
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ Requires PHP `^8.4.1 || ^8.5` and Laravel `^13.0`. Headless by design: all logic

```bash
composer require laranail/db-console
php artisan db-console:install
php artisan laranail::db-console.install
```

`db-console:install` still works as a deprecated alias: it prints one line naming the replacement and runs it.

The installer publishes the config, runs the catalog migrations, seeds the shipped console roles, assigns the bootstrap Owner (set `DB_CONSOLE_OWNER_USER_ID`), and runs `doctor` to health-check your servers.

Point it at a **minimal** admin account, never root:
Expand All @@ -36,7 +38,7 @@ GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, EXECUTE, CREAT

### Getting started

1. Set `DB_CONSOLE_OWNER_USER_ID` before running `php artisan db-console:install` (see Install), so the installer can assign the bootstrap Owner.
1. Set `DB_CONSOLE_OWNER_USER_ID` before running `php artisan laranail::db-console.install` (see Install), so the installer can assign the bootstrap Owner.
2. Add a dedicated admin connection named `db_console_admin` to `config/database.php`, using the minimal admin account above. The `primary` server in `config/db-console.php` uses it by default; `DB_CONSOLE_ENGINE` and `DB_CONSOLE_CONNECTION` override the engine and connection name.
3. Check the server before provisioning anything. TLS is mandatory by default, and `doctor` fails on a root-like account:

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"illuminate/validation": "^13.0",
"laranail/console": "^0.1",
"laranail/enumerator": "^0.1",
"laranail/package-tools": "^0.1"
"laranail/package-tools": "^0.1.3"
},
"require-dev": {
"aws/aws-sdk-php": "^3.0",
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Configuration

Every `laranail.db-console.*` configuration key. Publish the file with `php artisan db-console:install` or `vendor:publish`.
Every `laranail.db-console.*` configuration key. Publish the file with `php artisan laranail::db-console.install` or `vendor:publish`.

## Catalog

Expand Down
4 changes: 3 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ Requirements, install, the minimal admin account, and catalog setup for `laranai

```bash
composer require laranail/db-console
php artisan db-console:install
php artisan laranail::db-console.install
```

The pre-0.1 name `db-console:install` still works as a deprecated alias. It prints one line naming `laranail::db-console.install`, then runs it, and is removed no earlier than the next minor after 0.1.

The install flow publishes the config and language files, runs the catalog migrations, seeds the shipped console roles, assigns the bootstrap Owner, and runs `doctor`.

## The minimal admin account
Expand Down
2 changes: 1 addition & 1 deletion docs/tools/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ An optional, off-by-default HTTP surface over the same services.

## Overview

The REST API is disabled by default (`api.enabled`). When enabled, every route sits behind the `db-console.api-guard` middleware: it enforces the API is enabled, the request is over HTTPS (outside local), the caller is authenticated via the configured guard (`sanctum` or `passport`), and the IP is allow-listed. **Authorization itself happens in the services** — the same Gate as the CLI and UI — so an out-of-scope caller gets a 403 identically. Destructive endpoints require a matching `confirm` field. Exceptions render as secret-free JSON with a meaningful HTTP status. API tokens carry abilities that can never exceed the issuing operator's own permissions.
The REST API is disabled by default (`api.enabled`). When enabled, every route sits behind the `laranail-db-console.api-guard` middleware: it enforces the API is enabled, the request is over HTTPS (outside local), the caller is authenticated via the configured guard (`sanctum` or `passport`), and the IP is allow-listed. **Authorization itself happens in the services** — the same Gate as the CLI and UI — so an out-of-scope caller gets a 403 identically. Destructive endpoints require a matching `confirm` field. Exceptions render as secret-free JSON with a meaningful HTTP status. API tokens carry abilities that can never exceed the issuing operator's own permissions.

---

Expand Down
8 changes: 6 additions & 2 deletions docs/tools/commands.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# Commands

The full Artisan surface, each under a namespaced name and a short alias.
The full Artisan surface, each under the namespaced `laranail::db-console.<command>` name.

## Overview

Every command ships as `laranail::db-console.<command>` with a `db-console:<command>` alias. Groups: `db:create|list|drop`, `user:create|list|password|drop|edit`, `grant|revoke|attach|detach`, `wizard`, `reconcile`, `server:add|list|use`, `audit:view|verify`, `secrets:rotate|driver`, `encryption:status`, `role:list|create|assign|revoke`, `access:show|check`, `token:issue`, `webhook:list|add|remove`, plus `doctor` and `db-console:install`. Destructive commands require typed confirmation (or `--force` in CI); `--generate` prints a password once. All accept `--no-interaction` for scripting.
Every command ships as `laranail::db-console.<command>`. Groups: `db:create|list|drop`, `user:create|list|password|drop|edit`, `grant|revoke|attach|detach`, `wizard`, `reconcile`, `server:add|list|use`, `audit:view|verify`, `secrets:rotate|driver`, `encryption:status`, `role:list|create|assign|revoke`, `access:show|check`, `token:issue`, `webhook:list|add|remove`, plus `doctor` and `install`. Destructive commands require typed confirmation (or `--force` in CI); `--generate` prints a password once. All accept `--no-interaction` for scripting.

## Deprecated alias

The install command was `db-console:install` until 0.1. That name is still registered, hidden, as a deprecated alias: it prints one line naming `laranail::db-console.install`, then runs it and returns its exit code. It is removed no earlier than the next minor after 0.1. No other command carries a bare alias.

---

Expand Down
22 changes: 22 additions & 0 deletions docs/tools/rbac.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ Every service method authorizes through the same Gate. Access is **deny-by-defau

`builtin` stores roles, permissions, and assignments in the catalog. `spatie` delegates role→permission composition to `spatie/laravel-permission` while DBConsole still owns the scope triple. Both drivers return identical verdicts for the same assignment.

## Gate abilities

Every console permission is a gate ability named `laranail-db-console.<permission>`, for example `laranail-db-console.database.view`. Check them from a host policy, a Blade `@can` or a route's `can:` middleware, with the scope as the second argument:

```php
Gate::allows('laranail-db-console.database.drop', 'server:prod-mysql');
```

```blade
@can('laranail-db-console.database.view')
<a href="{{ route('laranail-db-console-webui.dashboard') }}">Databases</a>
@endcan
```

`ConsolePermission::DatabaseView->ability()` returns the same string, and is the spelling to prefer in PHP.

### Deprecated `db-console.*` abilities

The 21 abilities were named `db-console.<permission>` until 0.1. Each is still defined, as a deprecated alias that asks the gate for its scoped ability, so a host's own `Gate::define()` or before/after callback for `laranail-db-console.*` governs the old name too. The first check of each old name raises one `E_USER_DEPRECATED` notice naming its replacement. They are removed no earlier than the next minor after 0.1; rename them in host policies, `@can` directives and middleware.

Permission rows stored before the rename keep their `db-console.*` names. Both drivers read either form, so a custom role saved before 0.1 keeps its permissions, and `install` seeds the scoped names alongside them.

## Shipped roles

Owner, Admin, Operator, ReadOnly, Auditor are seeded on install; Owner composes to every permission. Assign with `role:assign --user --role --scope`; inspect with `access:show` and dry-run with `access:check`.
Expand Down
8 changes: 5 additions & 3 deletions src/Access/Drivers/BuiltinRbacDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,12 @@ private function roleExists(string $role): bool
return Role::query()->where('name', $role)->exists();
}

/**
* A stored permission name in either form: `laranail-db-console.x`, or the pre-0.1
* `db-console.x` a role saved before the rename still carries.
*/
private function permissionFromAbility(string $ability): ?ConsolePermission
{
$value = str_starts_with($ability, 'db-console.') ? substr($ability, strlen('db-console.')) : $ability;

return ConsolePermission::tryFrom($value);
return ConsolePermission::fromAbility($ability);
}
}
8 changes: 5 additions & 3 deletions src/Access/Drivers/SpatieRbacDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,12 @@ private function scopeRef(Scope $scope): ?string
};
}

/**
* A stored permission name in either form: `laranail-db-console.x`, or the pre-0.1
* `db-console.x` a role saved before the rename still carries.
*/
private function permissionFromAbility(string $ability): ?ConsolePermission
{
$value = str_starts_with($ability, 'db-console.') ? substr($ability, strlen('db-console.')) : $ability;

return ConsolePermission::tryFrom($value);
return ConsolePermission::fromAbility($ability);
}
}
17 changes: 16 additions & 1 deletion src/Authorization/DBConsolePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@
use Simtabi\Laranail\DBConsole\Access\Contracts\AccessManager;

/**
* Registers one gate ability per ConsolePermission (db-console.<permission>),
* Registers one gate ability per ConsolePermission (laranail-db-console.<permission>),
* each delegating to the AccessManager for a scope-aware verdict. Wiring the
* gate here means the API, CLI, and web UI all enforce identically through
* Gate::allows/authorize — the single enforcement surface (section 17).
*
* The scope is passed as the gate's second argument (a string like
* 'server:prod-mysql'); the AccessManager resolves coverage.
*
* The bare db-console.<permission> abilities used until 0.1 stay defined as
* deprecated aliases: each announces itself once and asks the gate for the
* scoped ability, so a host's own definition or before/after callback for the
* scoped name governs the bare one too. Removed no earlier than the next minor
* after 0.1.
*/
final readonly class DBConsolePolicy
{
Expand All @@ -33,6 +39,15 @@ public function register(Gate $gate): void
$scope,
),
);

$gate->define(
$permission->deprecatedAbility(),
static function (?object $user, ?string $scope = null) use ($gate, $permission): bool {
DeprecatedAbilities::announce($permission->deprecatedAbility(), $permission->ability());

return $gate->forUser($user)->check($permission->ability(), $scope === null ? [] : [$scope]);
},
);
}
}
}
42 changes: 42 additions & 0 deletions src/Authorization/DeprecatedAbilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Simtabi\Laranail\DBConsole\Authorization;

/**
* Announces a deprecated gate ability once per process, naming its vendor-scoped replacement.
*
* The bare `db-console.*` abilities are checked from host policies, Blade `@can` directives and
* middleware on every request; a notice per check would flood the log, so each name is announced
* the first time it is checked and never again.
*/
final class DeprecatedAbilities
{
/** @var array<string, true> */
private static array $announced = [];

public static function announce(string $ability, string $replacement): void
{
if (isset(self::$announced[$ability])) {
return;
}

self::$announced[$ability] = true;

trigger_error(sprintf(
'laranail/db-console: the gate ability [%s] is deprecated and will be removed no earlier than the next minor after 0.1. Use [%s] instead.',
$ability,
$replacement,
), E_USER_DEPRECATED);
}

/**
* Forget which abilities were announced. For test suites; a process announces each name once
* by design.
*/
public static function forgetWarnings(): void
{
self::$announced = [];
}
}
38 changes: 38 additions & 0 deletions src/Console/Commands/DeprecatedInstallCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Simtabi\Laranail\DBConsole\Console\Commands;

use Illuminate\Console\Command;

/**
* The bare `db-console:install` name the install command carried until 0.1.
*
* The install command itself is laranail/package-tools' DefinedInstallCommand, which is final and
* takes no aliases, so the old name is kept as this hidden forwarder: it prints one line naming the
* replacement, then runs `laranail::db-console.install` and returns its exit code.
*
* @deprecated Use `laranail::db-console.install`. Removed no earlier than the next minor after 0.1.
*/
final class DeprecatedInstallCommand extends Command
{
public const string REPLACEMENT = 'laranail::db-console.install';

protected $signature = 'db-console:install';

protected $description = 'Deprecated alias of laranail::db-console.install';

protected $hidden = true;

public function handle(): int
{
$this->warn(sprintf(
'Deprecated: [%s] is a deprecated alias and will be removed no earlier than the next minor after 0.1. Use [%s] instead.',
$this->getName(),
self::REPLACEMENT,
));

return $this->call(self::REPLACEMENT);
}
}
43 changes: 40 additions & 3 deletions src/Enums/ConsolePermission.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
/**
* CONSOLE permissions: what an operator may do with the tool. Entirely
* distinct from the MANAGED privileges DBConsole grants to database users.
* Gate abilities are the prefixed form from ability().
* Gate abilities are the prefixed form from ability(): `laranail-db-console.<permission>`.
*/
enum ConsolePermission: string implements Enumerator, Translatable
{
Expand Down Expand Up @@ -82,10 +82,47 @@ enum ConsolePermission: string implements Enumerator, Translatable
case SettingsManage = 'settings.manage';

/**
* The gate ability string for this permission.
* The prefix every gate ability carries.
*/
public const string ABILITY_PREFIX = 'laranail-db-console.';

/**
* The bare prefix the gate abilities carried until 0.1. Abilities under it are still defined,
* as deprecated aliases that delegate to the scoped ones, and are removed no earlier than the
* next minor after 0.1. Permission names stored under it still resolve (see fromAbility()).
*/
public const string DEPRECATED_ABILITY_PREFIX = 'db-console.';

/**
* The permission a gate ability or stored permission name stands for, in either form
* (`laranail-db-console.x`, or the pre-0.1 `db-console.x`), or the bare permission value.
* Null for anything else, including another package's ability.
*/
public static function fromAbility(string $ability): ?self
{
foreach ([self::ABILITY_PREFIX, self::DEPRECATED_ABILITY_PREFIX] as $prefix) {
if (str_starts_with($ability, $prefix)) {
return self::tryFrom(substr($ability, strlen($prefix)));
}
}

return self::tryFrom($ability);
}

/**
* The gate ability string for this permission: `laranail-db-console.<permission>`.
*/
public function ability(): string
{
return 'db-console.' . $this->value;
return self::ABILITY_PREFIX . $this->value;
}

/**
* The bare ability this permission was checked by until 0.1: `db-console.<permission>`.
* Still defined on the gate as a deprecated alias of ability().
*/
public function deprecatedAbility(): string
{
return self::DEPRECATED_ABILITY_PREFIX . $this->value;
}
}
5 changes: 3 additions & 2 deletions src/Models/Permission.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
namespace Simtabi\Laranail\DBConsole\Models;

/**
* A console ability string (db-console.database.create, ...), seeded from the
* fixed ConsolePermission set (builtin driver only).
* A console ability string (laranail-db-console.database.create, ...), seeded from the
* fixed ConsolePermission set (builtin driver only). Rows seeded before 0.1 carry the
* bare db-console.* form and still resolve through ConsolePermission::fromAbility().
*
* @property string $name
*/
Expand Down
Loading
Loading