Jurager Teams

repository·main·Indexed 42 days ago

https://github.com/jurager/teams

A Laravel package for managing multi-tenant teams, roles, and permissions. It supports dynamic roles, role groups, wildcard permissions, and granular record-level control via entity abilities. Includes built-in middleware for route protection, user invitation systems, and the HasMembers and HasTeams traits for managing team memberships and access levels.

Tokens
21K
Snippets
58
Records
99
Agent score
48%

What's inside Jurager Teams

  1. Overview of Jurager/Teams

    main

    Jurager/Teams is a Laravel package designed for managing teams and user permissions. It supports multi-tenant dynamic roles, role groups, and team-specific permissions.

    Key capabilities include:

    • Role Assignment: Assign roles to team members with specific permissions.
    • Wildcard Permissions: Use patterns like posts.* to cover all related actions.
    • Group Management: Organize members into groups within teams to grant or restrict access. Group permissions override individual user permissions within a team.
    • Global Groups: Grant users access across all teams via a global group (useful for support staff).
    • Record-Level Control: Control access to specific model instances.
    • Route Protection: Use built-in role, permission, and ability middleware to protect Laravel routes.
    • User Invitations: Invite users to teams via email using signed links.
    • Extensibility: The package fires events on team and membership changes.
  2. Note on the invitations table behavior

    main

    The invitations table is always created by the migration process, even if the invitations.enabled configuration flag is set to false.

    The invitations.enabled config flag only controls application-level logic, specifically:

    • Route registration
    • The behavior of inviteUser()
    • The behavior of inviteAccept()
  3. How to identify a Team Owner using the Owner model

    main

    The Owner is a lightweight object, not a database-backed Eloquent model. It is returned by the teamRole() method when the queried user is the owner of the team. To distinguish an owner from a standard role in your code, use an instanceof check.

    if ($role instanceof 
        \Jurager\Teams\Models\Owner
    ) {
        // The user is the team owner
    }
    if ($role instanceof \Jurager\Teams\Models\Owner) {
        // The user is the team owner
    }
  4. How to perform authorization checks

    main

    When implementing authorization logic (e.g., in Laravel Policies), prefer checking for specific permissions rather than checking for roles.

    Checking permissions makes your code more resilient to changes in role names or structures. Checking roles couples your logic to specific role names, which is harder to maintain.

    • Preferred: $user->hasTeamPermission($team, 'permission.code')
    • Avoid: $user->hasTeamRole($team, 'role_name')
    // Preferred — policy method
    $user->hasTeamPermission($team, 'server:update');
    
    // Avoid where possible — role check
    $user->hasTeamRole($team, 'admin');
  5. Understand the Core Models in Jurager/Teams

    main

    Jurager/Teams uses several core models to manage team structures, permissions, and memberships.

    • Team: The central record; by default, it eager-loads roles.permissions and groups.permissions.
    • Role: Represents a role with a code, name, and description. Note that deleting a Role automatically detaches its permissions and abilities.
    • Permission: A permission code scoped to a specific team.
    • Group: A user group within a team. If team_id is null, it acts as a global group.
    • Ability: An entity-level rule used to allow or forbid actions.
    • Membership: The pivot model for the team_user relationship. It eager-loads the role relation.
    • Invitation: A record for pending invites that resolves the user() relation via email.
    • Owner: A synthetic pseudo-model (not an Eloquent model) returned by teamRole() for team owners. It contains a wildcard permission ['*'].
  6. Core concepts of Jurager/Teams

    main

    Jurager/Teams uses several key abstractions to manage access control within a Laravel application:

    • Team: An isolated workspace containing members, roles, and groups.
    • Role: A baseline set of permissions assigned to a user within a specific team (stored in team_user.role_id).
    • Permission: A dot-notation string (e.g., articles.edit) that can be attached to either a Role or a Group.
    • Group: An optional permission layer used to manage subsets of users within a team or across the entire application (global groups).
    • Ability: An allow/forbid rule applied to a specific model instance. These are evaluated on top of existing role and group permissions.
  7. Use Global Groups for cross-team permissions

    main

    A global group is a group where team_id = null. These groups are useful for cross-team roles (such as platform support staff) because they apply across all teams without requiring the user to be a member of every individual team.

    Key Behaviors:

    • Global group permissions are checked by hasGlobalGroupPermissions() during ability evaluation.
    • They are evaluated only inside hasTeamAbility().
    • They are not returned by $user->teamPermissions() and do not affect hasTeamPermission() checks.
    // Creating a global group (no team scoping)
    $group = Group::create(['code' => 'support-global', 'team_id' => null]);
    $group->attachUser($user);
  8. How permission resolution layers work

    main

    Permissions in Jurager/Teams are resolved through a hierarchy of four layers. Understanding the difference between hasTeamPermission() and hasTeamAbility() is critical for correct implementation:

    Resolution Order

    1. Role permissions: Applied to all team members assigned that role.
    2. Group permissions: Overrides role access for members belonging to a specific group.
    3. Global group permissions: Cross-team permissions for members of global groups.
    4. Entity abilities: Instance-level allow/forbid rules applied to specific model records.

    Method Selection

    • Use hasTeamPermission() when you only need to check Layers 1 and 2 (Role and Group permissions).
    • Use hasTeamAbility() when you need to evaluate all four layers, including instance-level entity abilities.
  9. Understand the Jurager/Teams database schema

    main

    The package manages team access, memberships, and permissions using a set of dedicated database tables. The schema supports team ownership, role-based access control (RBAC), group memberships, and entity-level abilities.

    Core Tables

    TablePurpose
    teamsTeam records with user_id (owner reference)
    team_userMembership pivot with role_id
    rolesTeam roles (code, name, description)
    permissionsPermission codes scoped to a team
    groupsUser groups (team_id = null for global groups)
    group_userGroup membership pivot
    abilitiesEntity-level ability definitions
    entity_permissionPolymorphic junction: roles/groups ↔ permissions
    entity_abilityPolymorphic junction: roles/groups/users ↔ abilities (with forbidden flag)
    invitationsPending team invitations (when invitations.enabled = true)
  10. Understand Ability Access Levels and tie-breaking

    main

    Permissions are calculated using numeric access levels. The system tracks allowed and forbidden counters. Access is granted if allowed >= forbidden.

    Access Level Reference

    LevelValueDescription
    DEFAULT0No explicit permission or restriction
    FORBIDDEN1Base denial (no permission at team level)
    ROLE_ALLOWED2Permission granted via the user's role
    ROLE_FORBIDDEN3Restriction applied via the user's role
    GROUP_ALLOWED4Permission granted via the user's group
    GROUP_FORBIDDEN5Restriction applied via the user's group
    USER_ALLOWED5Permission granted specifically for this user on this entity
    USER_FORBIDDEN6Restriction applied specifically to this user on this entity
    GLOBAL_ALLOWED6Permission from a global group (team-independent)

    Important Tie-Breaking Rules

    • Ties favor access: Because the logic is allowed >= forbidden, a tie results in access being granted.
    • Value Overlaps:
      • GROUP_FORBIDDEN and USER_ALLOWED both equal 5. A tie here (e.g., USER_ALLOWED (5) >= GROUP_FORBIDDEN (5)) results in access being granted.
    • USER_FORBIDDEN and GLOBAL_ALLOWED both equal 6. A tie here results in access being granted.
  11. How Groups work as an override layer

    main

    Groups act as an override layer on top of standard team role permissions. They allow you to grant or deny specific abilities to subsets of users within a team.

    • If a user has a team permission, but their group forbids an action, the action is denied.
    • If a user lacks a team permission, but their group allows an action, the action is allowed.

    Important Constraints:

    • Group rules only affect hasTeamAbility() checks. They do not affect plain hasTeamPermission() checks.
    • Group permissions participate in ability level comparison (refer to the Abilities documentation for details).