How to create Workspaces and manage them for all the users in Django?

Hi there,

I have a complete Login and Registration system in my Django app, that lets me register users, log in users, logout users, change passwords, reset passwords, invite users. I have all the basic functionality.

I want now to have Workspaces for users, as per following points:

  • There will be a workspace admin of each workspace
  • Workspace admins can only add/remove (register/delete) users to his own workspaces.
  • Users can react to only those workspaces to which they have been added.
  • A superuser (main admin) can manage all workspaces, all workspaces-admins and all users.

How do I accomplish this? You can say this is similar to a Slack thing. I just need a guidelines/roadmap and I will implement it myself.

I am thinking to create a Workspace Model and have workspaces attribute in User Model as ManyToManyField, like below:

class User(AbstractBaseUser, PermissionsMixin):
    workspaces = models.ManyToManyField(Workspace)

Is this approach correct? If not please guide me to the proper way. BTW, using this approach, I can add/assign workspaces to any user, but how will I be able to manage the users logging in their own workspaces and reacting with only their workspaces to which they have been assigned. And also workspaces admins controlling their workspaces’ users etc?

Has anyone ever build this kind of thing in Django? Is their source code available? Or is there any Django official documentation on this? Or is there any free/paid course on building this kind of system in Django?

I shall be very grateful for the proper guideline or roadmap.

I would build this using the standard Django permission system with Group objects.

I’d create a Group for each Workspace and assign users to the Group objects as appropriate.

(Effectively, this is very similar to how we manage “departments” within our system. There’s a “supervisor” for each department with specific rights to “staff” in that department. The “staff” can modify data for their department only. The views are structured such that lists only display data to a “staff” where that data is associated to a department that the staff is a member of. In our case, a staff member could be a member of multiple departments.)

It’s less an issue of how do design such a structure, and more an issue of writing the various permissions tests to ensure they work as intended. But this can all be done using Django’s built-in facilities.

So, if I use Django’s built-in functionality. Do I need to create some kind of dashboard page, where a user, after signing in, reaches after various checks like:

  • if a user is super user, he/she will view the dashboard that allows him to manage all workspaces (groups as you mentioned), workspace users (group members), workspace admins (how do I get this functionality if I follow Django’s built-in method?)
  • if a user is workspace admin (again how do I implement this?), he could be shown the dashboard that only allows him/her to manage those workspaces on which he/she is admin and users of those workspaces (members of those groups)
  • if a user is normal user, he can only be in or react with the workspace (group) he/she is assigned to (member of)

Can I have all above functionalit(ies)y using Django built-in method? Also please guide me on the questions I asked above related to implementing workspace admins.

Unfortunately, “dashboard” is a distressingly vague term. If you want to get more specific about details, then we need to use more precise terminology.

A Django page displays content.

Views generate that content.

Views can determine what content is to be displayed on a page based on a variety of data. (That content may include links to other pages.)

The content to be included can be controlled using the methods defined on the User model such as has_perm and has_perms. (See django.contrib.auth | Django documentation | Django)

Access to the view can be controlled using an implementation of the user_passes_test method in the general case, or the permission_required method is more limited situations.

You can also make this as general as needed by implementing the has_permission method on your views to allow for a very granular row-level security model for the data.

To address your primary question - Yes. I have yet to encounter any security requirement that can’t be addressed by the features built in to Django - including an implementation of row-level security on a data model that depends upon data within the model to determine whether or not a user has access to that model at that specific point in time.

What you want to do is to think about these requirements in the right terms.

What views do you have?

What data is displayed in those views?

What users are accessing those views?

How do you determine what data is accessible by those users?

1 Like

@KenWhitesell well, I got the workflow of using Groups for this purpose. But I still have my question unanswered. Let me ask again in other words. How do I make a user manage only few specific groups? Also I thought about the questions you shared in your reply. So let me answer them here.

Right now, I just have simple Invitation, Registration and Login views, no other data is being passed back and forth

Simple user details

Only admins can access Invitation Views, and anyone can access other views

By using user.is_admin attribute

OK these were answers to the questions you told me to think about. Now let me explain my question. Suppose I have created 10 groups viz. group_a, group_b, group_c and so on, each have different unique name.

And I have 5 users viz. user_a, user_b and so on.

How would I accomplish that user_c get to manage (change group member details, add members to group and remove members from group) only group_b and not other groups?

First, my apologies - I should have been more clear. Those questions I posed at the end of my previous response are items you need to be thinking about when you’re building this out - they weren’t intended to elicit answers from you here.

You write code that tests or limits that at the appropriate points.

For example, say you have a “details page” for a user, and you want to provide a widget allowing you to add that user to a group.
Your code to generate the list of groups able to be added would be filtered to only be the groups that the current user can add to the selected user.

Or if you’re looking at it from the other direction, say you have a page that lets you select a group and add or remove users.
Again, your code would filter that group selection list to those groups that the current user can manage.

This might sound like a lot of work to do this, but in reality it turns out to be not that bad. The key is to not code these individually in the views, but to construct generic methods that can be used across views.

We did it by implementing a custom Manager class with the basic functionality to perform these tests, and then had our individual models (about 50 of them being managed this way) use that custom manager. The forms creating the select widgets use those manager functions to get the valid choices for that user at that point in time.

Which group? Did you consider ‘details page’ a group?

Can you please demonstrate this with a short and simple example of code/pseudo-code?

OK, I understood this. But it is only possible to filter those groups, if we know that which groups are being managed by the user or in other words say if we implicitly make the user to manage some specific groups only then we can filter-out those specific groups. So, filtering the groups is the second step, first step is to make user manage some specific group rather than managing all groups (like admin/superuser) and my question was exactly this, that how can we make user to manage only some specific groups?

I don’t know what you’re trying to ask here. I’m talking about a Detail View for a User - a page on which you can add / remove that User to groups.

See ChoiceField

So unfortunately, there’s a terminology issue here - and the options start to vary based upon specific requirements.

You don’t need to think of the Django Group model as “groups”, think of them as “roles”.

For discussion purposes - and this is just one way to approach this (there are others) -

You may have a “group” named “group_a”. This could translate into Django Group(s) “group_a_staff” and “group_a_admin”.

group_a_staff might have a Permission named “view_group_a”, where group_a_admin might have a Permission named “manage_group_a”.

Now assume CBVs using the PermissionRequiredMixin:

The view that generates the edit page for “group_a” then has a get_permission_required method that checks the url to identify that the user is trying to edit “group_a”, and returns ['manage_group_a']

If that user does not have that permission, the has_permission test fails and the user can be redirected elsewhere.

1 Like

Thank you for detailed explanation. I understood the terminology now. But by calling get_permission_required() method, how will the method recognize that the user trying to edit group_a should have manage_group_a permissions? Do I need to add such permissions? If yes, then where should I add and how should I add and how should I get those permissions?

By code that you write.

Could be a couple different ways.

You could either construct the permission name (e.g. return ['manage_'+group_name]) or, you could add an attribute to your Workspaces model (e.g. having something like management_permission = CharField(...) in the model. Or you could come up with something else entirely.

Yes. See Programmatically creating permissions. I would probably add the code to do this in whatever view is being used to create those Workspaces.

In this link, it said to give user the permission to publish blog post add the following permission:

permission = Permission.objects.create(
    codename='can_publish',
    name='Can Publish Posts',
    content_type=content_type,
)

Looking at this, if I want to create a permission for Workspace admin to remove the other users in the same workspace, should I do something like below?

permission = Permission.objects.create(
    codename='can_remove',
    name='Can Remove User',
    content_type=content_type,
)

Close - since you’re managing this on a per-workspace instance, and need to create these permissions for each Workspace, it should more be something like:

permission = Permission.objects.create(
    codename='can_remove_from_workspace_%s' % workspace_name,
    name='Can Remove User From Workspace %s' % workspace_name,
    content_type=content_type,
)

(Values selected for discussion purposes only. Should be changed as you see fit.)

OK, but I am still confused. How will the django or more specifically the Workspace will know that the permission that has name "can_remove_from_workspace_a", is used to remove the users from Workspace? I mean how does it know that this specific functionality (removing users from workspace) should be allowed? Because looking at the permission variable, it just has a name, a codename and content_type, nothing else specific that could tell the Workspace Model what is this permission of.

“It” doesn’t.

You do.

That’s what ties back to the earlier references to the get_permission_required and has_permission methods referenced in message #8 above.

You use these permissions in your methods to determine whether or not a user is allowed to perform a certain action. (Which may include retrieving lists, or accessing views, etc.)

Someone going to a view allowing for a user to be removed from “workspace_a” needs to have the permission can_remove_from_workspace_a.

OK, so let’s say I have the following Workspace Model:

class Workspace(models.Model):
    name = models.CharField(max_length=254)
    created_at = models.DateTimeField(auto_now_add=True)

    def make_admin(self, user):
        user.is_workspace_admin = True
        user.save()

    def remove_admin(self, user):
        user.is_workspace_admin = False
        user.save()

and I have the following permission for the Workspace model:

content_type = ContentType.objects.get_for_model(Workspace)
permission = Permission.objects.create(
    codename='can_remove_from_workspace_%s' % workspace_name,
    name='Can Remove User From Workspace %s' % workspace_name,
    content_type=content_type,
)

and in the views.py, I have:

class WorkspaceView(PermissionRequiredMixin, View):
    permission_required = 'can_remove_from_workspace_%s' % workspace_name

So I have few questions related to above supposition:

  • Do I need to create separate View for each permission, e.g. AddWorkspaceUser(PermissionRequiredMixin, View) for adding user in the same Workspace instance, RemoveWorkspaceUser(PermissionRequiredMixin, View) for removing user from Workspace instance and so on for each Workspace use case?
  • How do I add the permission to user? For example, a user is going to view WorkspaceView, he/she can only see the webpage if he/she has the required permissions and I know that can be checked using has_permission method. But how do I add the permission to user so he/she gets able to view the page in WorkspaceView?
  • Secondly, as you can see from the code above, I have is_workspace_admin attribute in my User Model. So how can I get this functionality that if user has is_workspace_admin=True, then automatically adds the Workspace admin permissions in the User instance, and if it has is_workspace_admin=False, then automatically remove the related permissions, the same/similar functionality that we have with is_admin attribute.
  • Where do I add the code in which I am adding permissions? Do I need to create a separate file permissions.py or can I add it in models.py?

That is not going to work because you don’t know what the workspace_name is going to be at the time the instance of the class is created. That is why you must implement this in the get_permissions_required method.

Wouldn’t you have separate views for those functions anyway?

Even if not, no you don’t. If you’ve got separate functionality within a view to perform those disparate functions in the same view, you can still check permissions within that “subfunction”.

You don’t.

The Permission is assigned to a Django Group, e.g. workspace_a_admin.

You then add that user to that group. That user then inherits all the permissions granted to that group.

Your model design for this is wrong. There is no reason to have a is_workspace_admin attribute on the user. The user’s status regarding their ability to administer a group is defined by their Group membership.

In the view where you’re creating the Workspace. (And likewise, if you allow for a Workspace to be deleted, you’ll want to remove these permissions as well.)

You’ll probably want to create the necessary Group objects in this same location as well as assigning the permissions to the Group.

Thanks for explaining each question.

OK, sure. But how would the Django’s Group know that what functionality the permission that is being added in Django Group, should allow?

Fine, but I also want the user that has is_admin=True be able to add the Workspace Admin Permissions to any other user. How would I achieve this if I remove is_workspace_admin attribute?

“It” doesn’t. Nor does it care. The Group doesn’t perform any validation, the view does.

If you need a Group to identify who can create “Workspace Admins”, then create another group with the related permissions, and protect those views accordingly.

This all is a traditional “Role-Based Security” structure.
You define the Roles that people have.
Those Roles are represented within Django as Group objects.
Those Roles enumerate the Permissions that the members of that Role possess.

It doesn’t matter what the functionality being protected is. The Permissions define what Roles are allowed to perform that function. Users made members of those Roles then are allowed to perform those functions.

Sorry apologies, I meant is_supeuser=True, not is_admin.

So, considering above correction, I guess I would not need to create a separate group, as Django already have permissions for superuser. So, I now need a functionality as the Django default User Model has, like when we make any user have is_superuser=True, then all the Superuser Permissions automatically get attached to that user and if we make it is_supeuser=False, then it takes away the Superuser Permissions. So I want a similar functionality that when a "User A" that has is_superuser=True, makes the is_workspace_admin=True of "User B", then "User B" should automatically gets all the Workspace Admin Permissions. In other words, I want something like this:

workspace_user = User.objects.get(email="some-email-address")

if request.user.is_superuser:
    wordspace_user.permissions.add([Workspace Admin Permissions])

How would I achieve this functionality?

There are a handful of options here, depending upon the precise implications of what you’re trying to achieve.

For example, if you want “User B” to always have all rights to all views controlled by any *_admin group, you have at least two choices for this option.

You can create a Group named all_admin, and assign the necessary permissions to this group in addition to assigning them to their specific groups when those other groups are created.

Or, you could override the has_permission method to check for membership in that group directly. Check to see if the user is a member of all_admin first.

For example, the standard has_permission method looks like this: (copied from Django 3.2)

def has_permission(self):
    """
    Override this method to customize the way permissions are checked.
    """
    perms = self.get_permission_required()
    return self.request.user.has_perms(perms)

Your overriding method could look something like:

def has_permission(self):
    if self.request.user.groups.filter(name='all_admin').exists():
        return True
    return super().has_permission()

(Kinda winging this, but I think I’m close.)

1 Like