Browse Source

feat: LDAP group-based permission system - group_permissions table, /me returns permissions[], /group-permissions CRUD

master
Budi Setiawan 3 weeks ago
parent
commit
4d58bd3dac
  1. 79
      server-connection/databaselist/app/Http/Controllers/Api/V1/GroupPermissionController.php
  2. 36
      server-connection/databaselist/app/Http/Controllers/Api/V1/MeController.php
  3. 23
      server-connection/databaselist/app/Models/GroupPermission.php
  4. 8
      server-connection/databaselist/app/Services/ApiAuthService.php
  5. 1
      server-connection/databaselist/app/Services/LdapJwtService.php
  6. 58
      server-connection/databaselist/app/Services/PermissionService.php
  7. 25
      server-connection/databaselist/database/migrations/2026_06_25_000001_create_group_permissions_table.php
  8. 96
      server-connection/databaselist/database/seeders/GroupPermissionSeeder.php
  9. 14
      server-connection/databaselist/routes/api.php

79
server-connection/databaselist/app/Http/Controllers/Api/V1/GroupPermissionController.php

@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\Concerns\RespondsWithJson;
use App\Http\Controllers\Controller;
use App\Models\GroupPermission;
use App\Services\PermissionService;
use App\Support\ApiActor;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class GroupPermissionController extends Controller
{
use RespondsWithJson;
public function __construct(
private readonly PermissionService $permissions,
) {
}
public function index(): JsonResponse
{
$rows = GroupPermission::orderBy('group_key')->orderBy('permission')->get();
$grouped = $rows->groupBy('group_key')->map(fn ($items) => $items->pluck('permission')->values());
return $this->ok([
'group_permissions' => $grouped,
'available_permissions' => $this->permissions->allPermissions(),
]);
}
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'group_key' => ['required', 'string', 'max:100'],
'permission' => ['required', 'string', 'max:100', 'in:' . implode(',', $this->permissions->allPermissions())],
]);
$row = GroupPermission::firstOrCreate([
'group_key' => $data['group_key'],
'permission' => $data['permission'],
]);
$this->permissions->flushCacheForGroups([$data['group_key']]);
return $this->ok($row->toArray(), [], $row->wasRecentlyCreated ? 201 : 200);
}
public function destroy(Request $request, string $group, string $permission): JsonResponse
{
$deleted = GroupPermission::where('group_key', $group)
->where('permission', $permission)
->delete();
$this->permissions->flushCacheForGroups([$group]);
return $this->ok(['deleted' => (bool) $deleted]);
}
public function syncGroup(Request $request, string $group): JsonResponse
{
$data = $request->validate([
'permissions' => ['required', 'array'],
'permissions.*' => ['string', 'in:' . implode(',', $this->permissions->allPermissions())],
]);
GroupPermission::where('group_key', $group)->delete();
foreach ($data['permissions'] as $permission) {
GroupPermission::create(['group_key' => $group, 'permission' => $permission]);
}
$this->permissions->flushCacheForGroups([$group]);
return $this->ok(['group_key' => $group, 'permissions' => $data['permissions']]);
}
}

36
server-connection/databaselist/app/Http/Controllers/Api/V1/MeController.php

@ -4,19 +4,53 @@ namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\Concerns\RespondsWithJson; use App\Http\Controllers\Api\Concerns\RespondsWithJson;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\LdapUserMeta;
use App\Services\LdapAuthService;
use App\Support\ApiActor; use App\Support\ApiActor;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class MeController extends Controller class MeController extends Controller
{ {
use RespondsWithJson; use RespondsWithJson;
public function __construct(
private readonly LdapAuthService $ldapAuth,
) {
}
public function show(Request $request): JsonResponse public function show(Request $request): JsonResponse
{ {
/** @var ApiActor $actor */ /** @var ApiActor $actor */
$actor = $request->attributes->get('api_actor'); $actor = $request->attributes->get('api_actor');
return $this->ok($actor->toArray()); $base = $actor->toArray();
if ($actor->type === 'ldap_jwt') {
$profile = $this->ldapAuth->getProfile($actor->identifier);
$meta = LdapUserMeta::where('username', $actor->identifier)->first();
$avatarUrl = null;
if ($meta?->avatar) {
try {
$avatarUrl = Storage::disk('oss')->url($meta->avatar);
} catch (\Throwable) {
$avatarUrl = null;
}
}
$base = array_merge($base, [
'display_name' => $profile['display_name'] ?? $actor->identifier,
'email' => $profile['email'] ?? null,
'domain' => $profile['domain'] ?? null,
'last_logon' => $profile['last_logon'] ?? null,
'phone' => $meta?->phone,
'other_email' => $meta?->other_email,
'avatar' => $avatarUrl,
]);
}
return $this->ok($base);
} }
} }

23
server-connection/databaselist/app/Models/GroupPermission.php

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class GroupPermission extends Model
{
protected $fillable = ['group_key', 'permission'];
public static function permissionsForGroups(array $groups): array
{
if (empty($groups)) {
return [];
}
return self::whereIn('group_key', $groups)
->pluck('permission')
->unique()
->values()
->all();
}
}

8
server-connection/databaselist/app/Services/ApiAuthService.php

@ -15,6 +15,7 @@ class ApiAuthService
private readonly ApiTokenService $apiTokens, private readonly ApiTokenService $apiTokens,
private readonly LdapJwtService $ldapJwt, private readonly LdapJwtService $ldapJwt,
private readonly ResourceServer $resourceServer, private readonly ResourceServer $resourceServer,
private readonly PermissionService $permissions,
) { ) {
} }
@ -59,11 +60,16 @@ class ApiAuthService
return null; return null;
} }
$groups = $payload['groups'] ?? [];
$resolvedPermissions = $this->permissions->permissionsForGroups($groups);
return new ApiActor( return new ApiActor(
'ldap_jwt', 'ldap_jwt',
$payload['username'], $payload['username'],
$payload['scopes'], $payload['scopes'],
$payload['is_admin'] $payload['is_admin'],
$groups,
$resolvedPermissions,
); );
} }

1
server-connection/databaselist/app/Services/LdapJwtService.php

@ -46,6 +46,7 @@ class LdapJwtService
'username' => (string) ($payload['sub'] ?? ''), 'username' => (string) ($payload['sub'] ?? ''),
'is_admin' => (bool) ($payload['is_admin'] ?? false), 'is_admin' => (bool) ($payload['is_admin'] ?? false),
'scopes' => array_values((array) ($payload['scopes'] ?? [])), 'scopes' => array_values((array) ($payload['scopes'] ?? [])),
'groups' => array_values((array) ($payload['groups'] ?? [])),
]; ];
} catch (UnexpectedValueException|\DomainException|\InvalidArgumentException) { } catch (UnexpectedValueException|\DomainException|\InvalidArgumentException) {
return null; return null;

58
server-connection/databaselist/app/Services/PermissionService.php

@ -0,0 +1,58 @@
<?php
namespace App\Services;
use App\Models\GroupPermission;
use Illuminate\Support\Facades\Cache;
class PermissionService
{
private const CACHE_TTL = 300;
public function permissionsForGroups(array $groups): array
{
if (empty($groups)) {
return [];
}
$cacheKey = 'group_perms:' . md5(implode(',', $groups));
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($groups) {
return GroupPermission::permissionsForGroups($groups);
});
}
public function hasPermission(array $groups, string $permission): bool
{
return in_array($permission, $this->permissionsForGroups($groups), true);
}
public function allPermissions(): array
{
return [
'profile.view',
'profile.edit',
'dashboard.view',
'cmdb.view',
'cmdb.manage',
'cmdb.assign_owner',
'monitoring.view',
'ticketing.view',
'ticketing.create',
'ticketing.manage',
'mail.view',
'asset_mgmt.view',
'asset_mgmt.manage',
'geonet_agent.view',
'admin.users',
'admin.groups',
'admin.permissions',
];
}
public function flushCacheForGroups(array $groups): void
{
$cacheKey = 'group_perms:' . md5(implode(',', $groups));
Cache::forget($cacheKey);
}
}

25
server-connection/databaselist/database/migrations/2026_06_25_000001_create_group_permissions_table.php

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('group_permissions', function (Blueprint $table) {
$table->id();
$table->string('group_key', 100)->index();
$table->string('permission', 100)->index();
$table->timestamps();
$table->unique(['group_key', 'permission']);
});
}
public function down(): void
{
Schema::dropIfExists('group_permissions');
}
};

96
server-connection/databaselist/database/seeders/GroupPermissionSeeder.php

@ -0,0 +1,96 @@
<?php
namespace Database\Seeders;
use App\Models\GroupPermission;
use Illuminate\Database\Seeder;
class GroupPermissionSeeder extends Seeder
{
/**
* Default permission mapping per LDAP group.
*
* Permissions catalogue:
* profile.view – lihat profil sendiri
* profile.edit – edit profil sendiri
* cmdb.view – lihat semua asset di CMDB
* cmdb.manage – edit meta asset, set status/tag
* cmdb.assign_owner – assign asset ke user LDAP
* monitoring.view – akses halaman monitoring
* ticketing.view – lihat tiket
* ticketing.create – buat tiket
* ticketing.manage – manage semua tiket (admin)
* mail.view – akses fitur mail
* dashboard.view – akses dashboard
* asset_mgmt.view – menu Asset Management
* asset_mgmt.manage – kelola inventaris (create/edit/delete)
* geonet_agent.view – lihat GeoNetAgent data
* admin.users – manage LDAP users
* admin.groups – manage LDAP groups
* admin.permissions – manage group-permission mapping
*/
private array $defaults = [
'Domain Admins' => [
'profile.view', 'profile.edit',
'dashboard.view',
'cmdb.view', 'cmdb.manage', 'cmdb.assign_owner',
'monitoring.view',
'ticketing.view', 'ticketing.create', 'ticketing.manage',
'mail.view',
'asset_mgmt.view', 'asset_mgmt.manage',
'geonet_agent.view',
'admin.users', 'admin.groups', 'admin.permissions',
],
'IT-Admin' => [
'profile.view', 'profile.edit',
'dashboard.view',
'cmdb.view', 'cmdb.manage', 'cmdb.assign_owner',
'monitoring.view',
'ticketing.view', 'ticketing.create', 'ticketing.manage',
'asset_mgmt.view', 'asset_mgmt.manage',
'geonet_agent.view',
],
'IT-Staff' => [
'profile.view', 'profile.edit',
'dashboard.view',
'cmdb.view', 'cmdb.manage', 'cmdb.assign_owner',
'monitoring.view',
'ticketing.view', 'ticketing.create',
'asset_mgmt.view',
'geonet_agent.view',
],
'Management' => [
'profile.view', 'profile.edit',
'dashboard.view',
'cmdb.view',
'monitoring.view',
'ticketing.view', 'ticketing.create',
'mail.view',
'asset_mgmt.view',
],
'Staff' => [
'profile.view', 'profile.edit',
'dashboard.view',
'ticketing.view', 'ticketing.create',
'mail.view',
],
'HR' => [
'profile.view', 'profile.edit',
'dashboard.view',
'ticketing.view', 'ticketing.create',
'mail.view',
],
];
public function run(): void
{
foreach ($this->defaults as $group => $permissions) {
foreach ($permissions as $permission) {
GroupPermission::firstOrCreate([
'group_key' => $group,
'permission' => $permission,
]);
}
}
}
}

14
server-connection/databaselist/routes/api.php

@ -2,6 +2,7 @@
use App\Http\Controllers\Api\V1\AuthController; use App\Http\Controllers\Api\V1\AuthController;
use App\Http\Controllers\Api\V1\DatabaseController; use App\Http\Controllers\Api\V1\DatabaseController;
use App\Http\Controllers\Api\V1\GroupPermissionController;
use App\Http\Controllers\Api\V1\LdapGroupController; use App\Http\Controllers\Api\V1\LdapGroupController;
use App\Http\Controllers\Api\V1\LdapUserController; use App\Http\Controllers\Api\V1\LdapUserController;
use App\Http\Controllers\Api\V1\MeController; use App\Http\Controllers\Api\V1\MeController;
@ -56,5 +57,18 @@ Route::prefix('v1')->group(function () {
Route::delete('/ldap/groups/{group}/members/{username}', [LdapGroupController::class, 'removeMember']) Route::delete('/ldap/groups/{group}/members/{username}', [LdapGroupController::class, 'removeMember'])
->middleware('api.scope:ldap:write') ->middleware('api.scope:ldap:write')
->name('api.v1.ldap.groups.members.remove'); ->name('api.v1.ldap.groups.members.remove');
Route::get('/group-permissions', [GroupPermissionController::class, 'index'])
->middleware('api.scope:admin')
->name('api.v1.group-permissions.index');
Route::post('/group-permissions', [GroupPermissionController::class, 'store'])
->middleware('api.scope:admin')
->name('api.v1.group-permissions.store');
Route::put('/group-permissions/{group}', [GroupPermissionController::class, 'syncGroup'])
->middleware('api.scope:admin')
->name('api.v1.group-permissions.sync');
Route::delete('/group-permissions/{group}/{permission}', [GroupPermissionController::class, 'destroy'])
->middleware('api.scope:admin')
->name('api.v1.group-permissions.destroy');
}); });
}); });

Loading…
Cancel
Save