- Added ProjectTask entity with fields for name, description, budget, hour contingent, hourly rate, and total price. - Created ProjectTaskRepository with methods for querying tasks by project and user access. - Implemented ProjectTaskVoter for fine-grained access control based on user roles and project membership. - Developed ProjectTaskSecurityListener to enforce permission checks during task creation. - Introduced custom ProjectTaskProjectFilter for filtering tasks based on project existence. - Integrated ProjectTask management in the frontend with Vue.js components, including CRUD operations and filtering capabilities. - Added API endpoints for ProjectTask with appropriate security measures. - Created migration for project_tasks table in the database. - Updated documentation to reflect new module features and usage.
7.8 KiB
myCRM Architecture Overview
Quick Reference for Claude
Security Architecture
This codebase uses a multi-layered security system combining:
- Symfony Authentication - User login, password hashing via bcrypt
- Role-Based Access Control (RBAC) - Users → Roles → Modules → Permissions
- Custom Voters - Fine-grained entity-level authorization
- API Platform - Per-operation security expressions
- Query Extensions - Automatic filtering of accessible data
See /SECURITY_ARCHITECTURE.md for comprehensive details.
Entity Relationships - Quick Diagram
User
├── Standard Roles: ROLE_ADMIN, ROLE_USER (in 'roles' JSON column)
├── Application Roles: Collection<Role> (ManyToMany)
│ └── Role
│ └── Permissions: Collection<RolePermission> (OneToMany)
│ └── RolePermission
│ └── Module (the module being granted permission for)
└── Can belong to Project teams: Collection<Project> (ManyToMany)
Project
├── owner: User (REQUIRED)
├── teamMembers: Collection<User> (ManyToMany)
├── customer: Contact
├── status: ProjectStatus
└── gitRepositories: Collection<GitRepository> (OneToMany)
ProjectTask
├── project: Project (nullable - can be project-independent)
└── Budget & hourly tracking
GitRepository
├── project: Project (REQUIRED)
└── URL, branch, provider info
Contact
├── contactPersons: Collection<ContactPerson> (OneToMany)
└── Company info: name, address, tax numbers, etc.
ContactPerson
└── contact: Contact (ManyToOne - no separate permissions)
Permission Hierarchy
Level 1: Symfony Standard Roles
- Checked in
config/packages/security.yamlaccess_control - Used for basic authentication gates (API requires
IS_AUTHENTICATED_FULLY)
Level 2: Module-Based Permissions
User.hasModulePermission(moduleCode, action)checks:- User's application roles
- Each role's RolePermission for the module
- The specific action flag (canView, canCreate, canEdit, etc.)
Level 3: Object-Level Voters
ModuleVoter- Module-level permissions (string subject or ModuleAwareInterface)ProjectVoter- Ownership/team access to projectsProjectTaskVoter- Project-based + admin-only for standalone tasksGitRepositoryVoter- Project-based access
Level 4: Event Listeners
ProjectTaskSecurityListener- Additional prePersist check
Core Files to Know
Security Components:
src/Security/Voter/ModuleVoter.php- Routes touser.hasModulePermission()src/Security/Voter/ProjectVoter.php- Checksproject.owner === useror teamsrc/Security/Voter/ProjectTaskVoter.php- Combines project + admin checkssrc/Security/Voter/GitRepositoryVoter.php- Tied to project access
Entities:
src/Entity/User.php- hasModulePermission() methodsrc/Entity/Role.php- permissions collectionsrc/Entity/RolePermission.php- defines per-module flagssrc/Entity/Module.php- registry of modulessrc/Entity/Project.php- hasAccess() checks owner/teamsrc/Entity/Interface/ModuleAwareInterface.php- getModuleName()
API Configuration:
config/packages/api_platform.yaml- Global API defaultsconfig/packages/security.yaml- Firewall, access_control- Entity
#[ApiResource]attributes - Per-operation security expressions
API Filters
Most entities have API filters:
SearchFilter- Partial text searchBooleanFilter- Filter by boolean fieldsDateFilter- Filter by date ranges- Custom
ProjectTaskProjectFilter- hasProject filter for tasks
Authentication Methods
- Form Login -
LoginFormAuthenticator- Email + password form - OAuth -
PocketIdAuthenticator- OIDC provider integration
Both update lastLoginAt in LoginSuccessListener.
Data Serialization
All APIs use serializer groups to control what's returned:
{entity}:read- Fields returned in GET{entity}:write- Fields accepted in POST/PUT
Example: User with #[Groups(['user:read', 'project:read'])] appears in both user and project responses.
Key Patterns
Module-Based Permission Check:
is_granted('VIEW', 'contacts') // Collection operation
↓
ModuleVoter.supports() → true
↓
ModuleVoter.voteOnAttribute('VIEW', 'contacts', $token)
↓
user.hasModulePermission('contacts', 'view')
Object-Level Access Check:
is_granted('DELETE', $project) // Delete operation
↓
ProjectVoter.supports() → true
↓
ProjectVoter.voteOnAttribute('DELETE', $project, $token)
↓
project.getOwner() === $user
Project-Related Filtering:
ProjectAccessExtension applies to Project GetCollection
↓
Adds: WHERE owner = :user OR teamMembers = :user
↓
User sees only their projects
Adding New Features
New Module-Based Entity:
- Implement
ModuleAwareInterface::getModuleName() - Create
Modulerecord in database - Add API security:
new GetCollection(security: "is_granted('VIEW', 'module_code')") - Create RolePermissions for existing roles
New Project-Related Entity:
- Add
project: Projectrelationship - Create custom Voter like
ProjectTaskVoter - Use in API:
new Get(security: "is_granted('VIEW', object)")
New User Role:
- Create
Roleentity - Add
RolePermissionrecords for each module - Assign to users via
user.addUserRole(role)
Common Security Attributes
Used in voters and API operations:
VIEW- Can read/listCREATE- Can create newEDIT- Can updateDELETE- Can removeEXPORT- Can export dataMANAGE- Admin functions
Important Constants
Voter Attributes:
ProjectVoter::VIEW
ProjectVoter::EDIT
ProjectVoter::DELETE
ProjectVoter::CREATE
// Similar for other voters
API Platform Groups:
{entity}:read → Normalization (GET)
{entity}:write → Denormalization (POST/PUT)
Symfony Standard Roles:
ROLE_USER → Authenticated user
ROLE_ADMIN → All permissions (bypasses voters)
Quick Debugging Checklist
-
User can't access API endpoint?
- Check
access_controlin security.yaml - Check API operation security expression
- Check voter implementation
- Check
-
User has role but can't perform action?
- Verify RolePermission exists for that role+module
- Check the specific permission flag (canView, etc.)
- Verify module code matches (case-sensitive!)
-
Non-admin accessing admin resource?
- Check ROLE_ADMIN bypass in voter
- Verify voter supports() returns true
- Look at voteOnAttribute() logic
-
Permission check in code?
// Get current user $user = $this->getUser(); // null if not authenticated // Check module permission if ($user->hasModulePermission('contacts', 'create')) { // Allow } // In Twig {% if app.user.hasModulePermission('contacts', 'view') %}...{% endif %} // In security expressions is_granted('CREATE', 'contacts') is_granted('EDIT', $contact)
Files Changed Recently
Check git status to see security-related changes:
src/Security/Voter/*.php- Voter implementationssrc/Entity/*.php- Entity definitionssrc/EventListener/*.php- Security listenersconfig/packages/security.yaml- Auth configuration
Testing Security
Unit Testing:
- Create user and project fixtures
- Test voter returns ALLOW/DENY/ABSTAIN correctly
- Test edge cases (null owner, empty team, etc.)
Integration Testing:
- Make API calls as different users
- Verify 403 Forbidden for unauthorized access
- Verify 200 OK for authorized access
- Check returned data respects serializer groups
See Also
/SECURITY_ARCHITECTURE.md- Complete security documentation/docs/PERMISSIONS.md- User management and permissions guideconfig/packages/security.yaml- Security configuration- API Platform docs: https://api-platform.com/docs/core/security/