Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement multi-tenancy access controls: restrict users to specific areas of interest #554

Closed
askbow opened this issue Sep 14, 2016 · 56 comments
Assignees
Labels
status: accepted This issue has been accepted for implementation
Milestone

Comments

@askbow
Copy link

askbow commented Sep 14, 2016

I've read #446 and thought that it would be nice to have a more general and finer access control with an ability to restrict users (user groups) to view / edit / create objects only in specific Tenants, Tenant Groups, or Sites. And such access control should be enforced in UI as well as in API access.

That way one could give some limited access for the tenants' engineers themselves, or set some internal access boundaries (for the security-conscious).

@jsenecal
Copy link
Contributor

One way to acheive this is per-object permissions, maybe based on Django-Rules (https://github.com/dfunckt/django-rules) or similar

@mryauch
Copy link
Contributor

mryauch commented Oct 24, 2016

I know this was mentioned on #323 but I'd like to add my voice. I just setup NetBox and found it is 100% what my org needs, however not having permissions at the site level is a deal breaker. With 30+ national sites we don't want to give everyone that needs to change IP prefixes at their site the ability to change prefixes across the country. Either way, awesome job Jeremy, this is an impressive and well thought out tool.

@jeremystretch jeremystretch changed the title Implement Multy-tenancy Access controls: restrict users to specific areas of interest Implement Multi-tenancy access controls: restrict users to specific areas of interest Dec 21, 2016
@jeremystretch jeremystretch changed the title Implement Multi-tenancy access controls: restrict users to specific areas of interest Implement multi-tenancy access controls: restrict users to specific areas of interest Dec 21, 2016
@Armadill0
Copy link

This would be (if I understood aright) a useful feature to allow IT sub teams the management e.g. of their own site(s)/IP space.

@GregoryVirrel
Copy link

Hi all.

I also strongly support this feature. This is one of the most important feature since it is about cloisoning et security.

@TheGuyDanish
Copy link

I'd love to bring this up again. I'm wanting to deploy NetBox within my organization, but a requirement from my higher ups is that there must be a way for our customers to see their components in our datacenter/network.

@Schnatterowski
Copy link

We too are planning to give our customers access to their own racks in our datacenter spaces. I'd very much like this feature to be implemented. As it is now, we'd have to run one netbox instance per customer. It would be great if it would be possible to give user account access to one or more tenants.

BTW, great job!

@tduehr
Copy link

tduehr commented Jan 24, 2018

Implementing this would allow me to use Netbox to replace teampass along with IPAM and Racktables. I need to control access to secrets along side the devices they pertain to but I don't want other groups to have access to an secrets but their own.

@jeremystretch jeremystretch added the status: accepted This issue has been accepted for implementation label Jan 26, 2018
@i1caro
Copy link

i1caro commented Feb 7, 2018

Hey @jeremystretch I have a rough implementation of this feature but would like your opinion and some direction.

  • I've added User to Tenants as a ManyToMany, this was the easiest for me but I think it should be the other way Tenants to User.
  • Added a filter_access function to ModelManagers
class ObjectFilterQuerySet(models.QuerySet):
    def build_args(self, user):
        return models.Q(tenant__users__in=[user])

    def filter_access(self, user):
        if not user.is_superuser:
            try:
                return self.filter(
                    self.build_args(user)
                )
            except TypeError:
                return self.none()
        return self
  • to Models:
    x Aggregate
    x Circuit
    x CircuitTermination
    x ConsolePort
    x ConsolePortTemplate
    x ConsoleServerPort
    x ConsoleServerPortTemplate
    x Device
    x DeviceBay
    x DeviceBayTemplate
    x DeviceRole
    x DeviceType
    x Interface
    x InterfaceConnection
    x InterfaceTemplate
    x InventoryItem
    x IPAddress
    x Platform
    x PowerOutlet
    x PowerOutletTemplate
    x PowerPort
    x PowerPortTemplate
    x Prefix
    x Provider
    x Rack
    x RackGroup
    x RackReservation
    x RackRole
    x Role
    x Service
    x Site
    x Tenant
    x TenantGroup
    x TopologyMap
    x VLAN
    x VLANGroup
    x VRF
    x ImageAttachment
  • The modules with no reference to User or Tenant I've added a Tenant field like DeviceType
  • Hide Tenant and TenantGroup from user in objects and selected the only available one in forms
  • Created a Middleware to extract the user as a global object connected to the request
  • Added filter_access to all get, post requests and general querysets that I could find
  • Added tenants to UserAdmin

@puck
Copy link

puck commented Feb 19, 2018

I'd find this useful as well!

@icaro with the more generic models (like DeviceType), have you made the Tenant a Many-to-Many? There are cases where I'd like to grant access at the TenantGroup level. Have you given any thought to that? In my case I have:

TenantGroup: Business Unit A
Tenants: Unit A Internal, Customer A.1, Customer A.2, Customer A.3
TenantGroup: Business Unit B
Tenants: Unit B Internal, Customer B.1, Customer B.2, Customer B.3

(My ideal would be to have Tenants nestable and ignore TenantGroups...)

@orgito
Copy link

orgito commented Aug 22, 2018

Hello @i1caro , do you still working on that? That is a very critical feature. I'd love to see it implemented. Right now I'm forced to keep a legacy system for a few prefixes that I need to give write access for internal teams. Just waiting for this feature to finally get rid of the legacy system.

@0xf10e
Copy link

0xf10e commented Jan 31, 2019

@orgito @shugotek I'm afraid this feature would need some kind of sponsor. Something like a company paying someone to work on a mergable implementation.

@jeremystretch
Copy link
Member

FYI I won't accept any sponsorship or merge any sponsored work on the project. Anyone who wishes to do so will need to maintain their own fork.

@0xf10e
Copy link

0xf10e commented Feb 1, 2019

I just meant if folks need this at work then maybe $WORK should have someone implementing this on company time.
And a company running this customer facing (sounds like @Schnatterowski's would) has more reason to maintain this code than one where it's just running internally for different departments ;)

@sdktr
Copy link
Contributor

sdktr commented Feb 5, 2019

FYI I won't accept any sponsorship or merge any sponsored work on the project. Anyone who wishes to do so will need to maintain their own fork.

Can you specify what a 'sponsorship' means in this context? When something is contributed in company time or when $company wants it's name/sponsorship included in some kind of 'wall-of-fame' somewhere?

@jeremystretch
Copy link
Member

I've made a lot of progress on this feature recently, visible in the 554-object-permissions branch. Although much work remains, I'm happy to report that the core functionality appears to be working very well.

At a high level, object-based permission assignment is done by crafting one or more ObjectPermission objects mapping users and/or groups to object types. Each instance has four boolean fields indicating the type(s) of action it permits; some set of view, add, change, and delete. Each instance also has a JSON representation of Django query filters, and therein lies the magic.

Say, for example, you want to allow a user to view and edit only devices assigned to a specific tenant. You would assign the ObjectPermission to the Device model and enter the following attribute to identify the tenant by slug:

{
    "tenant__slug": "acme-inc",
}

Maybe we want to further restrict the user to only devices of particular roles:

{
    "tenant__slug": "acme-inc",
    "device_role__slug__in": ["testing", "lab", "etc"],
}

As you can see, this approach is very flexible. All attributes within a single ObjectPermission are combined using a logical AND. To OR multiple attributes, simply create multiple ObjectPermissions.

These parameters are enforced whenever a user retrieves objects from NetBox. For example, when viewing the devices list, the user will only see devices matching the specified parameters. These parameters are similarly applied when modifying or deleting an object: the action will be permitted only if the resulting object matches the specified parameters.

The evaluation of object-level permissions is deferential to the model-level permissions currently provided by NetBox (and built-into the Django framework). That is to say, if a user has the dcim.view_device model-level permission assigned, all devices will be visible to that user. Otherwise, the user will be able to view only devices matching the prescribed ObjectPermission attributes assigned to that user, if any.

@sdktr
Copy link
Contributor

sdktr commented May 22, 2020

Awesome work!

At a high level, object-based permission assignment is done by crafting one or more ObjectPermission objects mapping users and/or groups to object types.

Would these 'ObjectPermission' objects only be mapped 1:1 to Users/Groups, or could this be implemented on some sort of session level ObjectPermission as well?

The scenario I'm thinking about is more of a view filter then strict user based permission.
For example (assuming tenant assignment as the only filter configured): with multiple tenants using the same system, an overall administrator could have a seperate ObjectPermission for each tenant in the system. In the GUI this list of ObjectPermissions could manifest as a dropdown -list for this users session. Selecting a permission ('context') would result in a 1:1 view of what the respective tenant could see. The results for each of the views would be filtered accordingly without specifying the filters setup in the ObjectPermission by hand on each click etc.

^^^ the above might be solvable with a plugin that changes a user's mapping to the ObjectPermission as well? Or dynamic group membership even, but I think this requires a re-logon.

Curious to your thoughts about this scenario.

@jeremystretch
Copy link
Member

All permissions are assigned based on users and groups. There is no mechanism to support what you describe.

@edylam
Copy link

edylam commented May 24, 2020

All permissions are assigned based on users and groups. There is no mechanism to support what you describe.

Awesome work!
I've tried the 554-object-permissions branch accroding to your example, but I got the error as below:

Request Method: POST
http://test.com:8099/admin/users/objectpermission/add/
3.0.6
TypeError
filter() argument after ** must be a mapping, not NoneType
/home/netbox554/netbox/users/models.py in clean, line 269
/usr/bin/python3
3.7.5
['/home/netbox554/netbox', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/usr/local/lib/python3.7/dist-packages', '/usr/lib/python3/dist-packages']
............
Any suggest?

1

@edylam
Copy link

edylam commented May 24, 2020

All permissions are assigned based on users and groups. There is no mechanism to support what you describe.

Awesome work!
I've tried the 554-object-permissions branch accroding to your example, but I got the error as below:

I've deleted the "," in line 2, just like
{
"tenant__slug": "test-aa"
}
then the POST success.

@edylam
Copy link

edylam commented May 24, 2020

In utility views, the object permission working well, but in home view as follow

    stats = {

        # Organization
        'site_count': Site.objects.count(),
        'tenant_count': Tenant.objects.count(),

        # DCIM
        'rack_count': Rack.objects.count(),
        'devicetype_count': DeviceType.objects.count(),
        'device_count': Device.objects.count(),
        'interface_connections_count': connected_interfaces.count(),
        'cable_count': cables.count(),
        'console_connections_count': connected_consoleports.count(),
        'power_connections_count': connected_powerports.count(),
        'powerpanel_count': PowerPanel.objects.count(),
        'powerfeed_count': PowerFeed.objects.count(),

the counts are still the counts of all the objects of a model. So, firstly, in home view, it should get all the counts of objects of models which the user has permission to view. change and so on, is it right?

@jeremystretch
Copy link
Member

This is a work in progress. It is currently broken in many places. Tomorrow, it will be broken in different places. Please do not attempt to troubleshoot.

@hsluoyz
Copy link

hsluoyz commented May 26, 2020

Have we tried PyCasbin? It supports RBAC with tenant model, which I think fulfilled our goal well.

https://casbin.org/

@edylam
Copy link

edylam commented May 27, 2020

This is a work in progress. It is currently broken in many places. Tomorrow, it will be broken in different places. Please do not attempt to troubleshoot.

OK

jeremystretch added a commit that referenced this issue Jun 1, 2020
jeremystretch added a commit that referenced this issue Jun 3, 2020
@jeremystretch
Copy link
Member

This has been implemented in PR #4705. There is almost certain to be further testing and mild development to be done, however the functionality is in place.

@jeremystretch
Copy link
Member

FYI v2.9-beta1 has been released. The community's support in testing this implementation will be crucial to its success. https://github.com/netbox-community/netbox/releases/tag/v2.9-beta1

@bluikko
Copy link
Contributor

bluikko commented Jul 24, 2020

Excellent, this feature could make my life much easier.
I need to ask: does this work together with LDAP authentication - more specifically LDAP groups?

@LeSnowTiger
Copy link

Excellent, this feature could make my life much easier.
I need to ask: does this work together with LDAP authentication - more specifically LDAP groups?

I'm testing it currently and as far as I see it works with LDAP groups.

@jeremystretch
Copy link
Member

It helps to recognize authentication and authorization as two discrete aspects: The former entails proving the identify of a user and the groups to which he or she belongs, whereas the later determines what actions the user is permitted to take. As far as NetBox is concerned, LDAP handles the first part, authenticating a user's credentials and (optionally) assigning the user to one or more groups. NetBox's permissions system handles the mapping of users and groups to permitted actions. So, as long as the authentication system is successful in assigning a user to a group, the permissions system can take it from there.

@bluikko
Copy link
Contributor

bluikko commented Jul 24, 2020

Agreed. I am not familiar at all with the Django authentication/authorization system - which seems to be not clearly separated (at least in NetBox). For example the configuration only talks about authentication. I wish there would be clear separate authentication and authorization so that I could authenticate with RemoteUserBackend and still use django-auth-ldap for authorization (which specifically says that group mirroring does not work together with RemoteUserBackend - but still talks about authorizing together with RemoteUserBackend). Will need to do more reading and testing...

This is now strongly straying to the "support" direction which I tried to avoid.

@LeSnowTiger
Copy link

FYI v2.9-beta1 has been released. The community's support in testing this implementation will be crucial to its success. https://github.com/netbox-community/netbox/releases/tag/v2.9-beta1

I played around with it the last few days. Overall its really solid.
But I see some flaws regarding devices and components.

For example:
I limit the permission to a device with {"tenant__slug": "test-tenant"}.
Now I only see the devices and no components (for example interfaces). So I have to add "view interface, dcim" permissions.
But now I can see every interface which was created in Netbox. Since its currently not possible to set a tenant on the interface I would have to work with additional permissions and tags.

I think (if possible) view permissions on components should be inherited from the parent device. So if I have permissions to device A and device B I only should see the components from these devices.

@jeremystretch
Copy link
Member

Since its currently not possible to set a tenant on the interface I would have to work with additional permissions and tags.

You can create a new permission for all device component types with the constraint {"device__tenant__slug": "test-tenant"}.

@LeSnowTiger
Copy link

Thank you! That seems to do exactly what I wanted. I´ve said nothing!

@jeremystretch
Copy link
Member

I mean, it's a valid point: shouldn't permitted devices "automatically" include child objects? There are two issues with the potential implementation though:

  1. It would require nonstandard application of permissions for certain types of objects (i.e. device components), which is best avoided wherever possible
  2. It's easy to imagine a use case where you don't want to automatically show all device components. For example, maybe you have on-site contractors that need access to power and console connections, but should not be able to see interfaces or device bays.

@LeSnowTiger
Copy link

It would be really cool to have something like this. But since i really have no clue about how django works I can`t say more than this. Your provided solution works fine for me.

Another thing I came across. While playing around with the "Create" permission I noticed that there is no easy way to limit an user to only create addresses in a specific prefix. (Maybe I`m missing something again)

The solution I came up with is with regex:
{"address__regex": "192.1.8[0-9].*/24"}, which would let the user only create addresses from 192.168.80.1/24 - 192.168.89.254/24. But I think it does not really scale well.

Anyway, thank you very much for this feature!

@jeremystretch
Copy link
Member

Honestly I'm just behind on the documentation for this (said every developer ever, right?). Prefixes and IP addresses do have filters available for stuff like this, but you have to dig a bit to find them. For example, if you want to restrict IP addresses to 192.168.0.0/24:

{"address__net_contained": "192.168.0.0/24"}

The net_contained filter matches any addresses contained by the specified network. For now, you can find the complete list of available filters here; hopefully we can get some proper documentation in place soon.

@LeSnowTiger
Copy link

That looks good! I'll have a look tomorrow. And no problem, your documentation is on the better end of documentations. 😉

@cpmills1975
Copy link
Contributor

Thanks for this! This is a huge step forward and waiting on this has been a blocker to me merging two NetBox instances in to one. I've played briefly with a dev instance I've just thrown up (docker using develop-2.9 so forgive me if I'm some commits behind).

There are a couple of things that strike me as potentially painful or a bit odd. Firstly, the method of granting permissions - one assumes the majority use case for this will be to limit tenants to their own 'stuff'. I started out creating three devices, one of which was assigned tenant 'tenant-A'. I created a group for 'tenant-a' and added user 'bob'. I then granted filtered permissions for devices to tenant-a using {"tenant__slug": "tenant-a"}. So far so good. I then came across the challenges noted above for components of the devices so added additional permissions for {"device__tenant__slug": "tenant-a"} on things like interfaces, console ports, power ports, etc. It occurred to me at this point that while this is awesomely powerful, it would be painful to scale. When I inevitably have to create a tenant-B, I already need to replicate and modify two rules for group 'tenant-b' etc. Would it be possible to consider applying permissions through the group create/edit page or provide a means to duplicate permissions or duplicate a group with all existing permissions that I can then tweak for the amended slugs? Would permissions be able to be OR'd? i.e. be able to specify [{"tenant__slug": "tenant-a"}, {"device__tenant__slug": "tenant-a}] and when applied to have NetBox ignore those rules which make no sense - I see it currently throws an error for filters that don't exist for a given object type hence the need for multiple distinct rules.

Secondly, having granted read permissions to all racks, 'bob' can now see details for devices they have no permissions over. I get that showing empty rack would be misleading, but could perhaps devices be rendered in a similar way to rack reservations to indicate that the space is used, but permissions don't grant access to know what by?

As a result of this, clicking on a device in the rack elevation view that I don't have permissions over results in a 404 error. Would a 403 forbidden be possible? Even better, could that redirect to the login page to make it clear permissions are not granted? Of course, if the devices/rack units were not clickable, the problem would go away anyway.

@jeremystretch
Copy link
Member

@cpmills1975 thanks for the feedback, but there's a lot to unpack in your comments above. Please consider opening separate issues for each topic so that we can have a more organized conversation around each.

@jeremystretch
Copy link
Member

In fact, I'm going to lock the conversation here just to ensure that additional new commentary doesn't get lost at the bottom of a four-year-old issue. For any bugs or improvements concerning this feature in the v2.9 beta, please open a new issue. Thanks!

@netbox-community netbox-community locked as resolved and limited conversation to collaborators Jul 28, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
status: accepted This issue has been accepted for implementation
Projects
None yet
Development

No branches or pull requests