feat: integrate ApiPlatformBundle and enhance settings access control
- Added ApiPlatformBundle to the project configuration. - Updated SettingsController to use custom access control for viewing and managing settings. - Modified AppFixtures to reflect new module structure and permissions for project management. - Adjusted ProjectStatus and ProjectTask entities to align with new permission checks. - Enhanced User entity to include a method for retrieving module permissions. - Implemented CleanupModulesCommand to deactivate or remove unimplemented modules. - Added CSRF protection configuration for forms. - Introduced property_info configuration for enhanced property handling. - Updated base template to include user module permissions in the frontend. - Created test_permissions.php for testing user permissions and roles.
This commit is contained in:
parent
1d85df5f70
commit
b4974b93ef
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,3 +26,4 @@ npm-debug.log
|
||||
yarn-error.log
|
||||
###< symfony/webpack-encore-bundle ###
|
||||
auth.json
|
||||
config/routes/billing.yaml
|
||||
|
||||
@ -1,35 +1,78 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { usePermissionStore } from '../stores/permissions';
|
||||
import AppMenuItem from './AppMenuItem.vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
|
||||
// Core-Menü (immer vorhanden)
|
||||
// Core-Menü mit Permission-Prüfungen
|
||||
const coreMenu = [
|
||||
{
|
||||
label: 'Home',
|
||||
items: [{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/' }]
|
||||
items: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
icon: 'pi pi-fw pi-home',
|
||||
to: '/',
|
||||
visible: () => permissionStore.canView('dashboard')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'CRM',
|
||||
items: [
|
||||
{ label: 'Kontakte', icon: 'pi pi-fw pi-users', to: '/contacts' },
|
||||
{ label: 'Projekte', icon: 'pi pi-fw pi-briefcase', to: '/projects' },
|
||||
{ label: 'Tätigkeiten', icon: 'pi pi-fw pi-list-check', to: '/project-tasks' }
|
||||
{
|
||||
label: 'Kontakte',
|
||||
icon: 'pi pi-fw pi-users',
|
||||
to: '/contacts',
|
||||
visible: () => permissionStore.canView('contacts')
|
||||
},
|
||||
{
|
||||
label: 'Projekte',
|
||||
icon: 'pi pi-fw pi-briefcase',
|
||||
to: '/projects',
|
||||
visible: () => permissionStore.canView('projects')
|
||||
},
|
||||
{
|
||||
label: 'Tätigkeiten',
|
||||
icon: 'pi pi-fw pi-list-check',
|
||||
to: '/project-tasks',
|
||||
visible: () => permissionStore.canView('project_tasks')
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Administration-Menü (immer vorhanden)
|
||||
// Administration-Menü mit Permission-Prüfungen
|
||||
const adminMenu = {
|
||||
label: 'Administration',
|
||||
visible: () => authStore.isAdmin,
|
||||
items: [
|
||||
{ label: 'Projekt-Status', icon: 'pi pi-fw pi-tag', to: '/project-statuses' },
|
||||
{ label: 'Benutzerverwaltung', icon: 'pi pi-fw pi-user-edit', to: '/users' },
|
||||
{ label: 'Rollenverwaltung', icon: 'pi pi-fw pi-shield', to: '/roles' },
|
||||
{ label: 'Einstellungen', icon: 'pi pi-fw pi-cog', to: '/settings' }
|
||||
{
|
||||
label: 'Projekt-Status',
|
||||
icon: 'pi pi-fw pi-tag',
|
||||
to: '/project-statuses',
|
||||
visible: () => permissionStore.canView('projects')
|
||||
},
|
||||
{
|
||||
label: 'Benutzerverwaltung',
|
||||
icon: 'pi pi-fw pi-user-edit',
|
||||
to: '/users',
|
||||
visible: () => permissionStore.canView('users')
|
||||
},
|
||||
{
|
||||
label: 'Rollenverwaltung',
|
||||
icon: 'pi pi-fw pi-shield',
|
||||
to: '/roles',
|
||||
visible: () => permissionStore.canView('roles')
|
||||
},
|
||||
{
|
||||
label: 'Einstellungen',
|
||||
icon: 'pi pi-fw pi-cog',
|
||||
to: '/settings',
|
||||
visible: () => permissionStore.canView('settings')
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@ -48,16 +91,29 @@ const loadPluginMenus = async () => {
|
||||
// Konvertiere zu PrimeVue Menü-Format
|
||||
const menuGroup = {
|
||||
label: groupLabel,
|
||||
items: items.map(item => ({
|
||||
items: items.map(item => {
|
||||
// Erstelle Permission-Prüfung
|
||||
let visibleFn;
|
||||
|
||||
if (item.permission) {
|
||||
// Explizite Permission aus Plugin
|
||||
visibleFn = () => authStore.hasPermission(item.permission);
|
||||
} else if (item.module) {
|
||||
// Fallback: Leite von Modulname ab (z.B. 'billing')
|
||||
visibleFn = () => permissionStore.canView(item.module);
|
||||
} else if (item.source) {
|
||||
// Fallback: Nutze Plugin-Identifier als Modulname
|
||||
visibleFn = () => permissionStore.canView(item.source);
|
||||
}
|
||||
|
||||
return {
|
||||
label: item.label,
|
||||
icon: item.icon ? `pi pi-fw ${item.icon}` : 'pi pi-fw pi-circle',
|
||||
to: item.to,
|
||||
...(item.items && { items: item.items }),
|
||||
// Wenn Permission definiert, prüfen
|
||||
...(item.permission && {
|
||||
visible: () => authStore.hasPermission(item.permission)
|
||||
...(visibleFn && { visible: visibleFn })
|
||||
};
|
||||
})
|
||||
}))
|
||||
};
|
||||
|
||||
model.value.push(menuGroup);
|
||||
@ -74,8 +130,13 @@ const loadPluginMenus = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Beim Mount laden
|
||||
onMounted(() => {
|
||||
// Beim Mount laden - warten bis Permissions geladen sind
|
||||
onMounted(async () => {
|
||||
// Warten bis Permissions geladen sind
|
||||
while (!permissionStore.loaded) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
loadPluginMenus();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { useLayout } from './composables/layout';
|
||||
import { onBeforeMount, ref, watch } from 'vue';
|
||||
import { onBeforeMount, ref, watch, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
@ -29,6 +29,24 @@ const props = defineProps({
|
||||
const isActiveMenu = ref(false);
|
||||
const itemKey = ref(null);
|
||||
|
||||
// Prüfe ob Item sichtbar sein soll
|
||||
const isVisible = computed(() => {
|
||||
const visible = props.item.visible;
|
||||
|
||||
// Wenn visible eine Funktion ist, aufrufen
|
||||
if (typeof visible === 'function') {
|
||||
return visible();
|
||||
}
|
||||
|
||||
// Wenn visible undefined ist, Item anzeigen
|
||||
if (visible === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sonst boolean-Wert verwenden
|
||||
return visible;
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
itemKey.value = props.parentItemKey ? props.parentItemKey + '-' + props.index : String(props.index);
|
||||
|
||||
@ -69,19 +87,19 @@ function checkActiveRoute(item) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li :class="{ 'layout-root-menuitem': root, 'active-menuitem': isActiveMenu }">
|
||||
<div v-if="root && item.visible !== false" class="layout-menuitem-root-text">{{ item.label }}</div>
|
||||
<a v-if="(!item.to || item.items) && item.visible !== false" :href="item.url" @click="itemClick($event, item, index)" :class="item.class" :target="item.target" tabindex="0">
|
||||
<li v-if="isVisible" :class="{ 'layout-root-menuitem': root, 'active-menuitem': isActiveMenu }">
|
||||
<div v-if="root" class="layout-menuitem-root-text">{{ item.label }}</div>
|
||||
<a v-if="!item.to || item.items" :href="item.url" @click="itemClick($event, item, index)" :class="item.class" :target="item.target" tabindex="0">
|
||||
<i :class="item.icon" class="layout-menuitem-icon"></i>
|
||||
<span class="layout-menuitem-text">{{ item.label }}</span>
|
||||
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" v-if="item.items"></i>
|
||||
</a>
|
||||
<router-link v-if="item.to && !item.items && item.visible !== false" @click="itemClick($event, item, index)" :class="[item.class, { 'active-route': checkActiveRoute(item) }]" tabindex="0" :to="item.to">
|
||||
<router-link v-if="item.to && !item.items" @click="itemClick($event, item, index)" :class="[item.class, { 'active-route': checkActiveRoute(item) }]" tabindex="0" :to="item.to">
|
||||
<i :class="item.icon" class="layout-menuitem-icon"></i>
|
||||
<span class="layout-menuitem-text">{{ item.label }}</span>
|
||||
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" v-if="item.items"></i>
|
||||
</router-link>
|
||||
<Transition v-if="item.items && item.visible !== false" name="layout-submenu">
|
||||
<Transition v-if="item.items" name="layout-submenu">
|
||||
<ul v-show="root ? true : isActiveMenu" class="layout-submenu">
|
||||
<app-menu-item v-for="(child, i) in item.items" :key="child" :index="i" :item="child" :parentItemKey="itemKey" :root="false"></app-menu-item>
|
||||
</ul>
|
||||
|
||||
@ -1051,7 +1051,7 @@ onMounted(async () => {
|
||||
|
||||
async function loadCustomers() {
|
||||
try {
|
||||
const response = await fetch('/api/contacts?pagination=false')
|
||||
const response = await fetch('/api/contacts?itemsPerPage=5000')
|
||||
if (!response.ok) throw new Error('Fehler beim Laden der Kunden')
|
||||
|
||||
const data = await response.json()
|
||||
@ -1174,7 +1174,7 @@ async function loadGitRepositories(projectId) {
|
||||
|
||||
async function loadProjectTasks(projectId) {
|
||||
try {
|
||||
const response = await fetch(`/api/project_tasks?project=${projectId}`)
|
||||
const response = await fetch(`/api/project_tasks?project=/api/projects/${projectId}`)
|
||||
if (!response.ok) throw new Error('Fehler beim Laden der Tätigkeiten')
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
@ -7,8 +7,7 @@
|
||||
"php": ">=8.2",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"api-platform/doctrine-orm": "*",
|
||||
"api-platform/symfony": "*",
|
||||
"api-platform/core": "^4.1",
|
||||
"doctrine/dbal": "^3",
|
||||
"doctrine/doctrine-bundle": "^2.18",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.6",
|
||||
@ -19,37 +18,37 @@
|
||||
"nelmio/cors-bundle": "^2.6",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.3",
|
||||
"symfony/asset": "7.1.*",
|
||||
"symfony/asset-mapper": "7.1.*",
|
||||
"symfony/cache": "7.1.*",
|
||||
"symfony/console": "7.1.*",
|
||||
"symfony/doctrine-messenger": "7.1.*",
|
||||
"symfony/dotenv": "7.1.*",
|
||||
"symfony/expression-language": "7.1.*",
|
||||
"symfony/asset": "7.3.*",
|
||||
"symfony/asset-mapper": "7.3.*",
|
||||
"symfony/cache": "7.3.*",
|
||||
"symfony/console": "7.3.*",
|
||||
"symfony/doctrine-messenger": "7.3.*",
|
||||
"symfony/dotenv": "7.3.*",
|
||||
"symfony/expression-language": "7.3.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/form": "7.1.*",
|
||||
"symfony/framework-bundle": "7.1.*",
|
||||
"symfony/http-client": "7.1.*",
|
||||
"symfony/intl": "7.1.*",
|
||||
"symfony/mailer": "7.1.*",
|
||||
"symfony/mime": "7.1.*",
|
||||
"symfony/form": "7.3.*",
|
||||
"symfony/framework-bundle": "7.3.*",
|
||||
"symfony/http-client": "7.3.*",
|
||||
"symfony/intl": "7.3.*",
|
||||
"symfony/mailer": "7.3.*",
|
||||
"symfony/mime": "7.3.*",
|
||||
"symfony/monolog-bundle": "^3.0",
|
||||
"symfony/notifier": "7.1.*",
|
||||
"symfony/process": "7.1.*",
|
||||
"symfony/property-access": "7.1.*",
|
||||
"symfony/property-info": "7.1.*",
|
||||
"symfony/runtime": "7.1.*",
|
||||
"symfony/security-bundle": "7.1.*",
|
||||
"symfony/serializer": "7.1.*",
|
||||
"symfony/notifier": "7.3.*",
|
||||
"symfony/process": "7.3.*",
|
||||
"symfony/property-access": "7.3.*",
|
||||
"symfony/property-info": "7.3.*",
|
||||
"symfony/runtime": "7.3.*",
|
||||
"symfony/security-bundle": "7.3.*",
|
||||
"symfony/serializer": "7.3.*",
|
||||
"symfony/stimulus-bundle": "^2.31",
|
||||
"symfony/string": "7.1.*",
|
||||
"symfony/translation": "7.1.*",
|
||||
"symfony/twig-bundle": "7.1.*",
|
||||
"symfony/string": "7.3.*",
|
||||
"symfony/translation": "7.3.*",
|
||||
"symfony/twig-bundle": "7.3.*",
|
||||
"symfony/ux-turbo": "^2.31",
|
||||
"symfony/validator": "7.1.*",
|
||||
"symfony/web-link": "7.1.*",
|
||||
"symfony/validator": "7.3.*",
|
||||
"symfony/web-link": "7.3.*",
|
||||
"symfony/webpack-encore-bundle": "^2.3",
|
||||
"symfony/yaml": "7.1.*",
|
||||
"symfony/yaml": "7.3.*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^2.12|^3.0"
|
||||
},
|
||||
@ -99,19 +98,19 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.1.*",
|
||||
"require": "7.3.*",
|
||||
"docker": true
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "*",
|
||||
"phpunit/phpunit": "^12.4",
|
||||
"symfony/browser-kit": "7.1.*",
|
||||
"symfony/css-selector": "7.1.*",
|
||||
"symfony/debug-bundle": "7.1.*",
|
||||
"symfony/browser-kit": "7.3.*",
|
||||
"symfony/css-selector": "7.3.*",
|
||||
"symfony/debug-bundle": "7.3.*",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/stopwatch": "7.1.*",
|
||||
"symfony/web-profiler-bundle": "7.1.*"
|
||||
"symfony/stopwatch": "7.3.*",
|
||||
"symfony/web-profiler-bundle": "7.3.*"
|
||||
},
|
||||
"repositories": {
|
||||
"mycrm-test-module": {
|
||||
|
||||
2613
composer.lock
generated
2613
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -14,9 +14,9 @@ return [
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
MyCRM\BillingModule\BillingModuleBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
11
config/packages/csrf.yaml
Normal file
11
config/packages/csrf.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
form:
|
||||
csrf_protection:
|
||||
token_id: submit
|
||||
|
||||
csrf_protection:
|
||||
stateless_token_ids:
|
||||
- submit
|
||||
- authenticate
|
||||
- logout
|
||||
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
framework:
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
94
src/Command/CleanupModulesCommand.php
Normal file
94
src/Command/CleanupModulesCommand.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Repository\ModuleRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:modules:cleanup',
|
||||
description: 'Deaktiviert oder entfernt nicht implementierte Module aus der Datenbank',
|
||||
)]
|
||||
class CleanupModulesCommand extends Command
|
||||
{
|
||||
private const VALID_MODULES = [
|
||||
'dashboard',
|
||||
'contacts',
|
||||
'projects',
|
||||
'project_tasks',
|
||||
'documents',
|
||||
'users',
|
||||
'roles',
|
||||
'settings',
|
||||
'billing', // Plugin-Modul
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ModuleRepository $moduleRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('Bereinigung von nicht implementierten Modulen');
|
||||
|
||||
// Finde alle Module, die nicht in der Liste der validen Module sind
|
||||
$allModules = $this->moduleRepository->findAll();
|
||||
$deactivatedModules = [];
|
||||
$deletedModules = [];
|
||||
|
||||
foreach ($allModules as $module) {
|
||||
if (!in_array($module->getCode(), self::VALID_MODULES, true)) {
|
||||
// Prüfe, ob das Modul Permissions hat
|
||||
if ($module->getPermissions()->count() > 0) {
|
||||
// Deaktiviere nur, da es noch Referenzen hat
|
||||
$module->setIsActive(false);
|
||||
$deactivatedModules[] = sprintf(
|
||||
'%s (%s) - deaktiviert (hat %d Berechtigungen)',
|
||||
$module->getName(),
|
||||
$module->getCode(),
|
||||
$module->getPermissions()->count()
|
||||
);
|
||||
} else {
|
||||
// Kann sicher gelöscht werden
|
||||
$deletedModules[] = sprintf(
|
||||
'%s (%s)',
|
||||
$module->getName(),
|
||||
$module->getCode()
|
||||
);
|
||||
$this->entityManager->remove($module);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
if (count($deactivatedModules) > 0) {
|
||||
$io->section('Deaktivierte Module:');
|
||||
$io->listing($deactivatedModules);
|
||||
}
|
||||
|
||||
if (count($deletedModules) > 0) {
|
||||
$io->section('Gelöschte Module:');
|
||||
$io->listing($deletedModules);
|
||||
}
|
||||
|
||||
if (count($deactivatedModules) === 0 && count($deletedModules) === 0) {
|
||||
$io->success('Keine nicht implementierten Module gefunden!');
|
||||
} else {
|
||||
$io->success('Module wurden bereinigt!');
|
||||
$io->note('Sie sollten nun "php bin/console doctrine:fixtures:load" ausführen, um die Datenbank neu zu initialisieren (optional).');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,6 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route('/api/settings', name: 'api_settings_')]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
class SettingsController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
@ -21,6 +20,8 @@ class SettingsController extends AbstractController
|
||||
#[Route('', name: 'get', methods: ['GET'])]
|
||||
public function getSettings(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('VIEW', 'settings');
|
||||
|
||||
return $this->json([
|
||||
'settings' => $this->settingsService->getAllSettings()
|
||||
]);
|
||||
@ -29,6 +30,8 @@ class SettingsController extends AbstractController
|
||||
#[Route('', name: 'update', methods: ['PUT', 'PATCH'])]
|
||||
public function updateSettings(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('MANAGE', 'settings');
|
||||
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
if (!isset($data['settings'])) {
|
||||
|
||||
@ -36,32 +36,39 @@ class AppFixtures extends Fixture
|
||||
'sortOrder' => 20
|
||||
],
|
||||
[
|
||||
'name' => 'Unternehmen',
|
||||
'code' => 'companies',
|
||||
'description' => 'Firmendatenbank',
|
||||
'icon' => 'pi-building',
|
||||
'name' => 'Projekte',
|
||||
'code' => 'projects',
|
||||
'description' => 'Projektverwaltung',
|
||||
'icon' => 'pi-briefcase',
|
||||
'sortOrder' => 30
|
||||
],
|
||||
[
|
||||
'name' => 'Deals',
|
||||
'code' => 'deals',
|
||||
'description' => 'Sales-Pipeline',
|
||||
'icon' => 'pi-dollar',
|
||||
'name' => 'Aufgaben',
|
||||
'code' => 'project_tasks',
|
||||
'description' => 'Aufgaben- und Task-Management',
|
||||
'icon' => 'pi-check-square',
|
||||
'sortOrder' => 40
|
||||
],
|
||||
[
|
||||
'name' => 'Aktivitäten',
|
||||
'code' => 'activities',
|
||||
'description' => 'Interaktions-Historie',
|
||||
'icon' => 'pi-calendar',
|
||||
'name' => 'Dokumente',
|
||||
'code' => 'documents',
|
||||
'description' => 'Dokumentenverwaltung',
|
||||
'icon' => 'pi-file',
|
||||
'sortOrder' => 50
|
||||
],
|
||||
[
|
||||
'name' => 'Berichte',
|
||||
'code' => 'reports',
|
||||
'description' => 'Analytics und Reports',
|
||||
'icon' => 'pi-chart-bar',
|
||||
'sortOrder' => 60
|
||||
'name' => 'Benutzerverwaltung',
|
||||
'code' => 'users',
|
||||
'description' => 'Verwaltung von Benutzern und Zugriffsrechten',
|
||||
'icon' => 'pi-user',
|
||||
'sortOrder' => 90
|
||||
],
|
||||
[
|
||||
'name' => 'Rollenverwaltung',
|
||||
'code' => 'roles',
|
||||
'description' => 'Verwaltung von Rollen und Berechtigungen',
|
||||
'icon' => 'pi-shield',
|
||||
'sortOrder' => 91
|
||||
],
|
||||
[
|
||||
'name' => 'Einstellungen',
|
||||
@ -108,26 +115,27 @@ class AppFixtures extends Fixture
|
||||
$manager->persist($permission);
|
||||
}
|
||||
|
||||
// Erstelle Vertriebsmitarbeiter-Rolle
|
||||
$salesRole = new Role();
|
||||
$salesRole->setName('Vertriebsmitarbeiter');
|
||||
$salesRole->setDescription('Zugriff auf Kontakte, Deals und Aktivitäten');
|
||||
$salesRole->setIsSystem(false);
|
||||
$manager->persist($salesRole);
|
||||
// Erstelle Projektmanager-Rolle
|
||||
$projectManagerRole = new Role();
|
||||
$projectManagerRole->setName('Projektmanager');
|
||||
$projectManagerRole->setDescription('Zugriff auf Kontakte, Projekte und Aufgaben');
|
||||
$projectManagerRole->setIsSystem(false);
|
||||
$manager->persist($projectManagerRole);
|
||||
|
||||
// Berechtigungen für Vertrieb
|
||||
$salesModules = ['dashboard', 'contacts', 'companies', 'deals', 'activities'];
|
||||
foreach ($salesModules as $moduleCode) {
|
||||
// Berechtigungen für Projektmanager
|
||||
$projectManagerModules = ['dashboard', 'contacts', 'projects', 'project_tasks', 'documents', 'settings'];
|
||||
foreach ($projectManagerModules as $moduleCode) {
|
||||
if (isset($moduleEntities[$moduleCode])) {
|
||||
$permission = new RolePermission();
|
||||
$permission->setRole($salesRole);
|
||||
$permission->setRole($projectManagerRole);
|
||||
$permission->setModule($moduleEntities[$moduleCode]);
|
||||
$permission->setCanView(true);
|
||||
$permission->setCanCreate($moduleCode !== 'dashboard');
|
||||
$permission->setCanEdit($moduleCode !== 'dashboard');
|
||||
$permission->setCanCreate($moduleCode !== 'dashboard' && $moduleCode !== 'settings');
|
||||
$permission->setCanEdit($moduleCode !== 'dashboard' && $moduleCode !== 'settings');
|
||||
$permission->setCanDelete(false);
|
||||
$permission->setCanExport($moduleCode !== 'dashboard');
|
||||
$permission->setCanManage(false);
|
||||
$permission->setCanExport($moduleCode !== 'dashboard' && $moduleCode !== 'settings');
|
||||
// Settings: VIEW und MANAGE erlaubt für Projektmanager
|
||||
$permission->setCanManage($moduleCode === 'settings');
|
||||
|
||||
$manager->persist($permission);
|
||||
}
|
||||
@ -141,7 +149,7 @@ class AppFixtures extends Fixture
|
||||
$manager->persist($viewerRole);
|
||||
|
||||
// Nur Leserechte für bestimmte Module
|
||||
$viewerModules = ['dashboard', 'contacts', 'companies', 'deals', 'activities', 'reports'];
|
||||
$viewerModules = ['dashboard', 'contacts', 'projects', 'project_tasks', 'documents'];
|
||||
foreach ($viewerModules as $moduleCode) {
|
||||
if (isset($moduleEntities[$moduleCode])) {
|
||||
$permission = new RolePermission();
|
||||
@ -172,19 +180,19 @@ class AppFixtures extends Fixture
|
||||
|
||||
$manager->persist($admin);
|
||||
|
||||
// Erstelle Test-Vertriebsmitarbeiter
|
||||
$sales = new User();
|
||||
$sales->setEmail('sales@mycrm.local');
|
||||
$sales->setFirstName('Max');
|
||||
$sales->setLastName('Mustermann');
|
||||
$sales->setIsActive(true);
|
||||
$sales->setRoles(['ROLE_USER']);
|
||||
$sales->addUserRole($salesRole);
|
||||
// Erstelle Test-Projektmanager
|
||||
$projectManager = new User();
|
||||
$projectManager->setEmail('pm@mycrm.local');
|
||||
$projectManager->setFirstName('Max');
|
||||
$projectManager->setLastName('Mustermann');
|
||||
$projectManager->setIsActive(true);
|
||||
$projectManager->setRoles(['ROLE_USER']);
|
||||
$projectManager->addUserRole($projectManagerRole);
|
||||
|
||||
$hashedPassword = $this->passwordHasher->hashPassword($sales, 'sales123');
|
||||
$sales->setPassword($hashedPassword);
|
||||
$hashedPassword = $this->passwordHasher->hashPassword($projectManager, 'pm123');
|
||||
$projectManager->setPassword($hashedPassword);
|
||||
|
||||
$manager->persist($sales);
|
||||
$manager->persist($projectManager);
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
security: "is_granted('VIEW', 'project_statuses')",
|
||||
security: "is_granted('VIEW', 'projects')",
|
||||
stateless: false
|
||||
),
|
||||
new Get(
|
||||
@ -30,7 +30,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
stateless: false
|
||||
),
|
||||
new Post(
|
||||
security: "is_granted('CREATE', 'project_statuses')",
|
||||
security: "is_granted('CREATE', 'projects')",
|
||||
stateless: false
|
||||
),
|
||||
new Put(
|
||||
@ -191,9 +191,10 @@ class ProjectStatus implements ModuleAwareInterface
|
||||
/**
|
||||
* Returns the module code this entity belongs to.
|
||||
* Required by ModuleVoter for permission checks.
|
||||
* ProjectStatus inherits permissions from the projects module.
|
||||
*/
|
||||
public function getModuleName(): string
|
||||
{
|
||||
return 'project_statuses';
|
||||
return 'projects';
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
denormalizationContext: ['groups' => ['project_task:write']],
|
||||
order: ['createdAt' => 'DESC']
|
||||
)]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial', 'project.name' => 'partial'])]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial', 'project.name' => 'partial', 'project' => 'exact'])]
|
||||
#[ApiFilter(DateFilter::class, properties: ['createdAt', 'updatedAt'])]
|
||||
#[ApiFilter(ProjectTaskProjectFilter::class)]
|
||||
class ProjectTask implements ModuleAwareInterface
|
||||
|
||||
@ -265,7 +265,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
foreach ($this->userRoles as $role) {
|
||||
foreach ($role->getPermissions() as $permission) {
|
||||
if ($permission->getModule()->getCode() === $moduleCode) {
|
||||
return match($action) {
|
||||
$hasPermission = match($action) {
|
||||
'view' => $permission->canView(),
|
||||
'create' => $permission->canCreate(),
|
||||
'edit' => $permission->canEdit(),
|
||||
@ -274,12 +274,58 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
'manage' => $permission->canManage(),
|
||||
default => false,
|
||||
};
|
||||
|
||||
// If ANY role grants the permission, return true
|
||||
if ($hasPermission) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all module permissions as a map for frontend
|
||||
* Format: ['module' => ['action1', 'action2', ...]]
|
||||
*/
|
||||
public function getModulePermissionsMap(): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
foreach ($this->userRoles as $role) {
|
||||
foreach ($role->getPermissions() as $permission) {
|
||||
$moduleCode = $permission->getModule()->getCode();
|
||||
|
||||
if (!isset($map[$moduleCode])) {
|
||||
$map[$moduleCode] = [];
|
||||
}
|
||||
|
||||
// Add each action that is allowed
|
||||
if ($permission->canView() && !in_array('view', $map[$moduleCode])) {
|
||||
$map[$moduleCode][] = 'view';
|
||||
}
|
||||
if ($permission->canCreate() && !in_array('create', $map[$moduleCode])) {
|
||||
$map[$moduleCode][] = 'create';
|
||||
}
|
||||
if ($permission->canEdit() && !in_array('edit', $map[$moduleCode])) {
|
||||
$map[$moduleCode][] = 'edit';
|
||||
}
|
||||
if ($permission->canDelete() && !in_array('delete', $map[$moduleCode])) {
|
||||
$map[$moduleCode][] = 'delete';
|
||||
}
|
||||
if ($permission->canExport() && !in_array('export', $map[$moduleCode])) {
|
||||
$map[$moduleCode][] = 'export';
|
||||
}
|
||||
if ($permission->canManage() && !in_array('manage', $map[$moduleCode])) {
|
||||
$map[$moduleCode][] = 'manage';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
|
||||
37
symfony.lock
37
symfony.lock
@ -1,11 +1,11 @@
|
||||
{
|
||||
"api-platform/symfony": {
|
||||
"api-platform/core": {
|
||||
"version": "4.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "4.0",
|
||||
"ref": "e9952e9f393c2d048f10a78f272cd35e807d972b"
|
||||
"ref": "cb9e6b8ceb9b62f32d41fc8ad72a25d5bd674c6d"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/api_platform.yaml",
|
||||
@ -155,6 +155,18 @@
|
||||
".env.dev"
|
||||
]
|
||||
},
|
||||
"symfony/form": {
|
||||
"version": "7.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.2",
|
||||
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/csrf.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/framework-bundle": {
|
||||
"version": "7.1",
|
||||
"recipe": {
|
||||
@ -231,6 +243,18 @@
|
||||
"config/packages/notifier.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/property-info": {
|
||||
"version": "7.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.3",
|
||||
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/property_info.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "7.1",
|
||||
"recipe": {
|
||||
@ -297,15 +321,6 @@
|
||||
"templates/base.html.twig"
|
||||
]
|
||||
},
|
||||
"symfony/uid": {
|
||||
"version": "7.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||
}
|
||||
},
|
||||
"symfony/ux-turbo": {
|
||||
"version": "2.31",
|
||||
"recipe": {
|
||||
|
||||
@ -21,7 +21,8 @@
|
||||
firstName: app.user.firstName,
|
||||
lastName: app.user.lastName,
|
||||
fullName: app.user.fullName,
|
||||
roles: app.user.roles
|
||||
roles: app.user.roles,
|
||||
modulePermissions: app.user.modulePermissionsMap
|
||||
}|json_encode|e('html_attr') : 'null' }}"
|
||||
></div>
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
78
test_permissions.php
Normal file
78
test_permissions.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
(new Dotenv())->bootEnv(__DIR__.'/.env');
|
||||
|
||||
$kernel = new App\Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
|
||||
$kernel->boot();
|
||||
$container = $kernel->getContainer();
|
||||
|
||||
$entityManager = $container->get('doctrine.orm.entity_manager');
|
||||
|
||||
// Finde einen Nicht-Admin Benutzer
|
||||
$userRepo = $entityManager->getRepository(App\Entity\User::class);
|
||||
$user = $userRepo->findOneBy(['email' => 'o.schwarten@osdata.net']);
|
||||
|
||||
if (!$user) {
|
||||
echo "Benutzer nicht gefunden!\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Benutzer: " . $user->getEmail() . "\n";
|
||||
echo "Rollen: " . implode(', ', $user->getRoles()) . "\n\n";
|
||||
|
||||
echo "User Roles (Entities): Count = " . $user->getUserRoles()->count() . "\n";
|
||||
|
||||
// Force load the collection
|
||||
$userRoles = $user->getUserRoles();
|
||||
if ($userRoles instanceof \Doctrine\ORM\PersistentCollection) {
|
||||
$userRoles->initialize();
|
||||
}
|
||||
|
||||
foreach ($userRoles as $role) {
|
||||
echo " - " . $role->getName() . " (ID: " . $role->getId() . ")\n";
|
||||
|
||||
$permissions = $role->getPermissions();
|
||||
if ($permissions instanceof \Doctrine\ORM\PersistentCollection) {
|
||||
$permissions->initialize();
|
||||
}
|
||||
|
||||
echo " Permissions count: " . $permissions->count() . "\n";
|
||||
foreach ($permissions as $permission) {
|
||||
$module = $permission->getModule();
|
||||
echo " Module: " . $module->getCode() . " - View: " . ($permission->canView() ? 'YES' : 'NO') . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
echo "Testing hasModulePermission('billing', 'view'): ";
|
||||
echo $user->hasModulePermission('billing', 'view') ? "TRUE" : "FALSE";
|
||||
echo "\n";
|
||||
|
||||
echo "Testing hasModulePermission('invoices', 'view'): ";
|
||||
echo $user->hasModulePermission('invoices', 'view') ? "TRUE" : "FALSE";
|
||||
echo "\n";
|
||||
|
||||
// Test Voter
|
||||
$authChecker = $container->get('security.authorization_checker');
|
||||
$tokenStorage = $container->get('security.token_storage');
|
||||
|
||||
// Create a token for the user
|
||||
$token = new Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken(
|
||||
$user,
|
||||
'main',
|
||||
$user->getRoles()
|
||||
);
|
||||
$tokenStorage->setToken($token);
|
||||
|
||||
echo "\nTesting Voter with is_granted('VIEW', 'billing'): ";
|
||||
try {
|
||||
$result = $authChecker->isGranted('VIEW', 'billing');
|
||||
echo $result ? "TRUE" : "FALSE";
|
||||
} catch (Exception $e) {
|
||||
echo "ERROR: " . $e->getMessage();
|
||||
}
|
||||
echo "\n";
|
||||
Loading…
x
Reference in New Issue
Block a user