Question:
I have a setup with the following tables: Client, Project, and Role.
Structure:
- Client: Clients have a parent-child hierarchy (tree structure).
- Project: Each project belongs to one client, but projects can be moved between clients.
- User: A user can be assigned to multiple clients, and their roles may differ per client.
Example Structure:
- Root Client (Client 1):
- User A: Can access all child client projects.
- User B: Can access all child client projects.
- Client 2 (Child of Client 1):
- User C: Can access only Client 2 and its child client projects.
- User D: Can access only Client 2 and its child client projects.
- Projects:
- Project 1 (Assigned to Client 2)
- Project 2 (Assigned to Client 2)
- Client 4 (Child of Client 2):
- User G: Can access only Client 4 projects.
- Projects:
- Project 5 (Assigned to Client 4)
- Client 3 (Child of Client 1):
- User E: Can access only Client 3 and its child client projects.
- User F: Can access only Client 3 and its child client projects.
- Projects:
- Project 3 (Assigned to Client 3)
- Project 4 (Assigned to Client 3)
- Client 5 (Child of Client 3):
- User H: Can access only Client 5 projects.
- Projects:
- Project 6 (Assigned to Client 5)
Current Issue:
I was previously managing Roles with a ForeignKey to Client, and a Many-to-Many relationship with Projects, Users, and Groups (e.g., Admin, Viewer, Editor). But with the new hierarchy, this has become more complex.
On the frontend, I authenticate the user and fetch their roles and permissions via a Role API. I also fetch the associated client and their projects via a Client API. However, now that clients have a parent-child hierarchy, the challenge is:
- How do I manage User Roles?
- A user can be assigned to multiple clients with different roles. How do I handle this effectively in the context of the client hierarchy?
Desired Behavior:
- Users should only have access to the projects assigned to the clients they are associated with, depending on their role.
- A root client user should have access to all child client projects.
- A child client user should only have access to its own client and its descendants.
- Roles should be managed in a way that accounts for multiple client assignments and the hierarchical structure.
Root Client (Client 1)
├── User A (Can access all child client projects)
├── User B (Can access all child client projects)
├── Client 2 (Child of Client 1)
│ ├── User C (Can access only Client 2 and its child client projects)
│ ├── User D (Can access only Client 2 and its child client projects)
│ ├── Projects:
│ │ ├── Project 1 (Assigned to Client 2)
│ │ ├── Project 2 (Assigned to Client 2)
│ └── Client 4 (Child of Client 2)
│ ├── User G (Can access only Client 4 projects)
│ └── Projects:
│ └── Project 5 (Assigned to Client 4)
└── Client 3 (Child of Client 1)
├── User E (Can access only Client 3 and its child client projects)
├── User F (Can access only Client 3 and its child client projects)
├── Projects:
│ ├── Project 3 (Assigned to Client 3)
│ ├── Project 4 (Assigned to Client 3)
└── Client 5 (Child of Client 3)
├── User H (Can access only Client 5 projects)
└── Projects:
└── Project 6 (Assigned to Client 5)
My current Role table design
class Role(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, help_text=(
“Name of the Role”), verbose_name=(“Name”), default=“my_role”)
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True, help_text=(“User associated with this role”),
verbose_name=(“User”)
)
group = models.ForeignKey(
Group,
on_delete=models.SET_NULL,
null=True,
help_text=(“Group associated with this role”),
verbose_name=(“Group”)
)
client = models.ForeignKey(Client, on_delete=models.SET_NULL, null=True, blank=True, help_text=(
“Client associated with this role”), verbose_name=(“Client”))
project = models.ManyToManyField(
Project,
help_text=(“Project associated with this role”),
verbose_name=(“Project”),
blank=True
)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, help_text=(
“The person who created”), verbose_name=(“Created by”), related_name=“role_created_by”)
created_at = models.DateTimeField(default=timezone.now, help_text=(
“Creation date”), verbose_name=(“Created at”))
is_display = models.BooleanField(default=True)
is_edited = models.BooleanField(default=False)
is_deleted = models.BooleanField(default=False)
def __str__(self) -> str:
user_name = self.user.username if self.user else "No User"
client_name = self.client.name if self.client else "No Client"
return f"{self.name} - {user_name} - {client_name}"
def user_has_project_access(self, user, project):
"""
Check if the user has access to the project based on their role and client hierarchy.
"""
if self.user == user:
if self.client and self.client.user_has_access(user):
return True
if self.project.filter(id=project.id).exists():
return True
return False
Welcome @TheodoreAsher !
Taking a conceptual step back, fundamentally, you’re trying to answer one question in every view:
Can Person A
perform Operation X
on Entity N
.
This is true regardless of how you structure your data.
For every view, you know who Person A
is from request.user
. You know what Operation X
is because it’s what that view is going to do. And you should know (or be able to determine) what Entity N
is. (Frequently, it’s from information in the URL, like the primary key - or it may need to derrived from other information.)
Once you can identify this information for your views, you can then implement one (or more) of the permission-related methods such as the User.has_perms
or the user_passes_test
functions. (There are more than just these two - you need to evaluate what is going to work best in your specific situation.) You write whatever queries and logic is necessary to answer that question “Yes” or “No”, and handle accordingly.
Couple of additional thoughts:
The easiest way that I have found to implement a Role-Based Access Control (RBAC) system in Django is to treat the Django Group
model as the Role. That gives you the inherent ability to assign permissions to Group
(the “Role”) such that the built-in User.has_perm
method will pass for any permission assigned either directly to User
or to any of the Group
to which they are assigned.
And, using an appropriate naming convention to avoid conflicts, there’s nothing wrong with using the Group
model for both “functionality-based groups” and “roles”. With the has_perms
function being additive, the test would pass for any permission granted to either one.
Also, when creating an arbitrary hierarchical structure, I strongly suggest that you do not implement it simply as a set of “Foreign Keys to parent”. See an earlier comment of mine at
Some modelling advice - #12 by KenWhitesell along with the external links in it. Depending upon the complexity of the models involved, you may also want to separate the “hierarchical data structure” from the “business model structure”. In other words, you don’t maintain the hierarchy as columns within the business model - it’s a separate table that manages the relationships.
In other words, your business model doesn’t contain any information about its relationships with other models in this hierarchy. That information is stored in a separate model that only contains that information.