Jeremy Stretch - Leveraging the ORM to enforce object level permissions

DjangoCon US
23 Oct 202127:52

Summary

TLDRJeremy Stretch's DjangoCon 2021 presentation introduces an advanced object-based permissions system for Django, developed for NetBox, a network infrastructure modeling application. The talk outlines the limitations of Django's default permissions and explores third-party packages before diving into a custom solution using JSON-stored QuerySet filters. This approach allows dynamic permissions based on object attributes, offering scalability and flexibility for diverse user needs. Stretch also discusses the implementation of a custom authentication backend and atomic transactions for secure object modifications, with plans to release the solution as a standalone package for the Django community.

Takeaways

  • 😀 Jeremy Stretch, a former network engineer turned software developer, is the founder and lead maintainer of NetBox, an application for modeling network infrastructure.
  • 🔧 NetBox is built on Django and has around 70 complex models with various relationships, making it an advanced application popular for managing large-scale network infrastructures.
  • 🌐 NetBox's user base is diverse, with users having large networks and unique use cases, leading to the need for more robust permission systems beyond Django's native capabilities.
  • 🛠️ Jeremy discussed the limitations of Django's default permissions system, which are based on models and do not support object-level permissions out of the box.
  • 🔒 The talk focused on implementing object-based permissions in Django, allowing permissions to be granted for subsets of objects on a dynamic basis, even for objects not yet created.
  • 📝 The default Django permissions are automatically created with four CRUD actions (view, add, change, delete) for each model, named in the format 'app_label.action_model_name'.
  • 📦 Third-party packages like django-guardian and django-rules were considered for object-based permissions but did not meet NetBox's requirements due to scalability and flexibility issues.
  • 💡 A custom solution was developed for NetBox that uses JSON to store QuerySet filters in the database, allowing for complex, attribute-based permissions.
  • 🔄 The custom permissions system involves creating an ObjectPermission model that captures the flexibility of Django's ORM for permissions enforcement, using JSON to define constraints.
  • 🔑 A custom authentication backend was implemented to support the new permissions logic, overriding Django's ModelBackend to handle object-level permissions evaluation.
  • 🔍 The permissions are applied using a RestrictedQuerySet manager that simplifies the process of filtering objects based on user permissions and object attributes.

Q & A

  • What is the main topic of Jeremy Stretch's presentation at DjangoCon 2021?

    -Jeremy Stretch's presentation is about implementing object-based permissions in Django, focusing on how to grant permissions to users or groups for subsets of objects dynamically.

  • What is Jeremy Stretch's professional background before becoming a software developer?

    -Jeremy Stretch was a former network engineer before transitioning into full-time software development.

  • What is NetBox and why was it created?

    -NetBox is an application for modeling network infrastructure, including data center infrastructure and IP address management. It was conceived at DigitalOcean and released as an open source project under the Apache 2 license in 2016.

  • How does the diversity of NetBox's user base present challenges in terms of permissions?

    -The diversity of NetBox's user base, which includes users with large networks and unpredictable use cases across different industries and organizational sizes, presents challenges in creating a permissions system that can accommodate unique approaches and requirements.

  • What is the default permissions backend in Django and how does it work?

    -The default permissions backend in Django is ModelBackend, which automatically creates four default permissions (view, add, change, delete) for each model, following a specific naming structure.

  • Why did Jeremy Stretch and his team decide to build their own permissions system for NetBox?

    -They decided to build their own permissions system because existing packages like django-guardian and django-rules did not meet their needs for scalability and flexibility, especially considering the unpredictable and diverse use cases of NetBox users.

  • What is the key idea behind the custom permissions system developed for NetBox?

    -The key idea is to declare QuerySet filters in JSON, store them in the database, and use these filters to enforce permissions on objects based on their attributes, leveraging the flexibility of Django's ORM.

  • How does the ObjectPermission model differ from Django's stock Permission model?

    -The ObjectPermission model bundles model-based permissions together, allowing for more streamlined permission assignments, while the stock Permission model has one instance per content type per action.

  • What is the purpose of the RestrictedQuerySet manager in the custom permissions system?

    -The RestrictedQuerySet manager simplifies the process of applying permissions to a QuerySet by providing a .restrict method that takes the user and the desired action as parameters, handling the permissions retrieval and caching behind the scenes.

  • How does the custom permissions system handle the modification of objects to ensure they remain in a permitted state?

    -The system uses Atomic Transactions to check the pre-modified and post-modified states of an object. If the modified object no longer meets the permissions criteria, a custom exception is raised, undoing the transaction and preventing the changes from being saved to the database.

  • What is the current status of the custom permissions system developed for NetBox?

    -The custom permissions system has been implemented in NetBox version 2.10 and is being worked on to be extracted as a standalone package, expected to be available by the end of 2021 or early 2022.

Outlines

00:00

😀 Introduction to Object-Based Permissions in Django

Jeremy Stretch introduces his talk at DjangoCon 2021, focusing on implementing object-based permissions in Django. He shares his background as a former network engineer turned software developer and mentions his work at NS1 and as the founder of NetBox, an application for modeling network infrastructure. Jeremy highlights the diverse user base of NetBox and the challenges in managing permissions for various use cases and organizational structures. The talk aims to explore how to grant permissions dynamically to users or groups for subsets of objects within NetBox.

05:01

📚 Understanding Django's Default Permissions System

The speaker provides an overview of Django's default permissions framework, explaining the ModelBackend and the automatic creation of CRUD permissions for each model. He discusses the syntax and structure of permission names and how to evaluate permissions in views and templates. Jeremy also touches on the limitations of Django's built-in permissions when it comes to object-specific permissions, noting that the has_perm method does not support object-level permissions out of the box.

10:03

🔍 Exploring Third-Party Packages for Advanced Permissions

Jeremy reviews third-party packages like django-guardian and django-rules, which offer object-based permissions and flexible rule-based permissions, respectively. He explains how django-guardian assigns permissions to specific objects for users or groups, while django-rules allows developers to define predicates and rules for permissions enforcement. However, he concludes that these solutions do not fully meet the needs of NetBox due to their limitations in scalability and the need for static, predetermined rules.

15:05

🛠 Building a Custom Object-Based Permission System

After finding existing solutions inadequate, Jeremy describes the process of building a custom object-based permission system within NetBox. He outlines the design goals, which include compatibility with Django's stock permissions, flexibility in granting permissions based on object attributes, providing a user-friendly interface and API, and ensuring immediate application of permission changes. The custom solution leverages Django's ORM to filter objects by attributes and stores these filters in a new ObjectPermission model as JSON data.

20:07

🔄 Implementing QuerySet Filters for Permissions

The speaker details the implementation of the custom permission system, explaining how to apply QuerySet filters to enforce permissions. He describes the process of retrieving Object Permissions for a user, building a QuerySet with OR logic to combine constraints, and using this to filter objects. Jeremy also introduces the RestrictedQuerySet manager to simplify the application of permissions and discusses modifying the has_perm method to support the new system.

25:08

🚦 Handling Object Modifications with Atomic Transactions

Jeremy addresses the challenge of modifying objects while respecting permissions, explaining the use of atomic transactions to ensure that objects are not altered into non-permitted states. He outlines the steps for checking the pre- and post-modification states of an object and using exceptions to roll back changes that violate permissions. The speaker also highlights the need to secure all avenues for object modification, including APIs and bulk operations.

📘 Future Developments and Community Involvement

In the concluding part of the talk, Jeremy announces the ongoing work to extract the custom permission system from NetBox and prepare it as a standalone package for the Django community. He shares the timeline for the release and invites community members to contribute to the development and testing of the package. Jeremy also offers his availability for further questions and engagement on various platforms.

Mindmap

Keywords

💡Django

Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. In the context of the video, Django is the framework in which the speaker, Jeremy Stretch, is discussing the implementation of object-based permissions. The video revolves around enhancing Django's capabilities to handle complex permission scenarios, particularly in the NetBox application, which is built on Django.

💡Object-based permissions

Object-based permissions refer to a system where access rights are granted to users or groups for specific objects within an application, rather than just model-level permissions. In the video, Jeremy discusses the need for such permissions in NetBox to allow dynamic and granular control over what users can do with particular network infrastructure objects.

💡NetBox

NetBox is an open-source application for modeling network infrastructure, such as data center infrastructure and IP address management. It was mentioned as the application for which the speaker is implementing advanced permissions. The video emphasizes the complexity and diversity of NetBox's user base, which necessitates the development of robust permission systems.

💡ModelBackend

ModelBackend is the default permissions backend in Django, which automatically creates permissions for each model in the system. The video script discusses the limitations of ModelBackend when it comes to object-level permissions, leading to the exploration of alternative solutions.

💡CRUD

CRUD stands for Create, Read, Update, and Delete, which are the four basic operations that can be performed on data in a system. In the script, CRUD actions are related to the default permissions Django provides for each model: view, add, change, and delete, which correspond to the CRUD operations.

💡django-guardian

Django-guardian is a third-party package that provides object-level permissions in Django by allowing direct assignment of objects to specific users or groups. In the video, Jeremy mentions django-guardian as one of the solutions considered but ultimately not suitable for NetBox due to its requirement of explicit relationships.

💡django-rules

Django-rules is another third-party package that allows developers to define predicates and rules for permissions enforcement. It offers flexibility but relies on static and predetermined rules. Jeremy discusses it as an alternative to django-guardian, but it was also not a perfect fit for NetBox's needs due to the unpredictable use cases of its users.

💡QuerySet Filters

QuerySet Filters in Django are used to retrieve specific subsets of data from the database based on certain conditions. In the script, Jeremy describes how they implemented a custom permissions system using QuerySet Filters stored as JSON in the database, allowing for dynamic and flexible permission constraints.

💡Atomic Transactions

Atomic Transactions ensure that database operations are processed reliably without partial updates, which can be critical for maintaining data integrity. In the context of the video, Atomic Transactions are used to check the state of an object before and after modifications to ensure that changes do not violate the defined permissions.

💡Custom Authentication Backend

A Custom Authentication Backend in Django allows developers to override the default authentication process, including permission checks. Jeremy explains that they created a custom backend to support their new object-based permissions system, extending the built-in ModelBackend and overriding its methods to incorporate the new permission logic.

💡PermissionsViolation

PermissionsViolation is a custom exception used in the video's context to handle cases where a user attempts to perform an action that violates the object-based permissions rules. It is raised when an object's post-modification state no longer complies with the assigned permissions, and the transaction is rolled back to maintain the object's integrity.

Highlights

Jeremy Stretch, a former network engineer turned software developer, presents at DjangoCon 2021 on implementing object-based permissions in Django.

NetBox, an application for modeling network infrastructure, was conceived at DigitalOcean and released as an open-source project under the Apache 2 license in 2016.

NetBox, built on Django, has around 70 complex models with numerous relationships, making it an advanced application popular in recent years.

The diverse user base of NetBox includes large networks with tens or hundreds of thousands of nodes, presenting unique challenges in permissions management.

The standard Django permissions system, based on the ModelBackend, provides four default permissions (view, add, change, delete) for each model.

Django's has_perm method can evaluate permissions for a specific object, but the default ModelBackend returns false for any object other than none.

Packages like django-guardian and django-rules were considered for object-based permissions but did not meet the scalability and flexibility needs of NetBox.

Jeremy Stretch outlines the design goals for a custom permissions system in NetBox, emphasizing compatibility, flexibility, user-friendly interfaces, and immediate application of changes.

A new ObjectPermission model was created to store QuerySet filters as JSON in the database, allowing for dynamic permissions based on object attributes.

The ObjectPermission model correlates content types, models, users, groups, and actions, with optional constraints for more granular permissions.

A RestrictedQuerySet manager was developed to simplify the application of permissions to QuerySets, retrieving and caching permissions as needed.

A custom authentication backend was implemented to support the built-in permissions evaluation logic, overriding the default has_perm method.

Atomic transactions are used to ensure that objects cannot be modified into a non-permitted state, maintaining data integrity and permissions enforcement.

The permissions system is designed to work with bulk operations and API views, such as Django Rest Framework or GraphQL, ensuring comprehensive security.

Jeremy Stretch discusses the ongoing development of extracting the permissions system from NetBox into a standalone package for the Django community.

The standalone permissions package is expected to be available by the end of 2021 or early 2022, with contributions and community feedback encouraged.

Jeremy Stretch concludes the presentation by offering his availability for questions and future engagement on Twitter, Slack, or other channels.

Transcripts

play00:31

- Hi everyone. This is Jeremy Stretch presenting

play00:32

for DjangoCon 2021.

play00:35

Today I'll be talking about implementing object based

play00:37

permissions in Django. So let's go ahead and jump in.

play00:44

First off a little bit about myself.

play00:45

I am a former network engineer turned software developer.

play00:48

So I originally got my start in my career in traditional

play00:52

network infrastructure and engineering.

play00:54

And over the past few years I've transitioned

play00:55

into full-time software development.

play00:58

Currently at the NS1, have been working with Django,

play01:01

however, since around early 2008, I think,

play01:03

when I originally had for, you know, just a personal blog.

play01:08

These days though, I am probably best known

play01:10

as the founder and lead maintainer of NetBox.

play01:18

So what is NetBox?

play01:20

It's an application for basically

play01:23

modeling network infrastructure.

play01:24

So if you think about traditional data center

play01:27

infrastructure and IP address management,

play01:29

performs all of those tasks, as well as some other roles.

play01:33

It was originally conceived at DigitalOcean,

play01:35

back when I was a network engineer

play01:36

on the infrastructure team there,

play01:39

and we released it as an open source project

play01:41

under the Apache 2 license back in 2016.

play01:45

So it's completely built on Django.

play01:47

We've got about 70 or so complex models,

play01:50

lots of relationships among them.

play01:53

It's a pretty advanced application and it's gotten

play01:55

really popular, especially in recent years.

play01:57

If it sounds like something you're interested by all means,

play01:59

check us out on GitHub.

play02:04

But in developing NetBox,

play02:06

we've noticed a few things about our users.

play02:09

We have a very, very wide diverse user base.

play02:12

Many of them have very, very large networks

play02:15

to the tunes of tens or even hundreds of thousands

play02:17

of nodes on a network or addresses on the network.

play02:20

They also have unpredictable use cases, meaning, you know,

play02:24

a financial company might use something different

play02:27

than a service provider,

play02:28

which would be different than, like,

play02:29

higher education and so on.

play02:32

So, we see wildly different use cases

play02:37

among industries and different organizational sizes,

play02:39

and depending on even what they're using it for.

play02:42

Different organizational structures as well,

play02:44

some people, some organizations out there

play02:46

are a one or two person shops.

play02:48

Some are huge teams running their infrastructure,

play02:50

and every step along the way between those.

play02:54

And so we've seen plenty of unique approaches around working

play02:57

or working around limitations. So for example,

play02:59

if an organization wants to model something

play03:01

that NetBox doesn't support natively,

play03:04

they have some clever work around sometimes

play03:06

to make it do what they want it to.

play03:11

This leads to some interesting challenges,

play03:13

specifically around permissions.

play03:15

So a couple of years back,

play03:17

we started looking at, uh,

play03:20

doing, seeing what we could do to provide

play03:23

more robust permissions

play03:24

in NetBox, beyond what Django provides natively

play03:27

as a framework.

play03:28

And really what the question boiled down to is just this:

play03:31

It's how do we grant permissions to users

play03:33

and or groups for subsets of objects

play03:36

within NetBox on a dynamic basis?

play03:39

Meaning we want them to apply,

play03:41

we want the permissions to apply even to objects that

play03:43

haven't been created yet.

play03:47

So that's what I'm gonna be talking about today.

play03:49

First, we'll start with a quick dive

play03:51

into de Django permissions as they

play03:52

exist in the framework today.

play03:55

To provide some scope and context of this conversation.

play03:59

If you're familiar with the acronym, triple A,

play04:02

that's authentication authorization and accounting,

play04:04

pretty standard term in security,

play04:06

basically, we're going to be looking

play04:07

at that second point right there.

play04:09

So authentication is who you are.

play04:10

Authorization is what you can do.

play04:12

And accounting is what you did.

play04:13

Permissions is pertinent to the second point is

play04:16

authorization.

play04:17

So we're not going to be talking about authentication

play04:19

at all. Just, just the permissions framework.

play04:23

The Default Permissions Backend

play04:26

in Django is the ModelBackend

play04:27

that's provided under cotrib.auth.backends.

play04:30

Permissions are created automatically.

play04:31

Every time we create a model and we get

play04:34

four default permissions out of the box for each model.

play04:38

Those are the,

play04:39

what we'll refer to sometimes as CRUD actions being,

play04:42

create, read, update, delete.

play04:43

In Django vernacular those are going to be

play04:45

view, add, change, and delete.

play04:46

So the four basic options or operations or others

play04:49

that you can do with an instance.

play04:52

Permission names or follow the structure below here.

play04:54

This is app label, dot, action, underscore,

play04:57

name of the model.

play05:00

Here's an example. We have a model called Book.

play05:03

It's an application called Library.

play05:05

It's got some fields on there and under the meta class,

play05:07

we have permissions defined. So here we're actually,

play05:09

we are defining a custom permission as well.

play05:12

So Django is very flexible in that,

play05:14

it lets you do this as well, pretty easily.

play05:17

And the permission here is publish underscore book.

play05:21

So this model will result in five

play05:23

ultimate permissions being created.

play05:25

Whenever you run the migration to introduce the model

play05:27

within the Django application.

play05:29

So you've got view book, add book,

play05:31

change, book, delete book, and our custom permission,

play05:34

which is published book.

play05:37

When we evaluate these permissions,

play05:39

if we're doing it in the view,

play05:40

we're typically going to call it user dot has perm

play05:42

or has perms with an S on the end

play05:44

for evaluating multiple permissions within a template.

play05:48

And a couple of ways we can do that.

play05:49

Typically we're going to do it, perms dot

play05:50

and then the permission name,

play05:51

or look for the permission name

play05:53

as a string within the perms context variable.

play05:58

So none of this should be new.

play05:59

Hopefully this is a quick refresher for folks who may

play06:02

not have much use to mess

play06:04

with permissions on a regular basis.

play06:06

This is really, the kind of the crux of the matter,

play06:09

everything we just talked about is for evaluating

play06:11

permissions on a model basis. So for example,

play06:13

you saw the book model, right?

play06:14

And this applies to all instances,

play06:16

all books that we might create in our Django application.

play06:19

What if we want to enforce permissions

play06:20

for a specific object?

play06:23

Well, let's take a look at the has perm method.

play06:26

Remember we call it user dos has perm

play06:27

to evaluate that.

play06:29

Within the doc string of this,

play06:31

we see a note at the end.

play06:33

So first off it has perm is going to just return true

play06:35

if the user has the specified permission.

play06:37

So I call has perm,

play06:39

you know, view, underscore

play06:40

or, sorry,

play06:41

library dot view underscore book,

play06:43

if they have that permission, it's gonna return it.

play06:45

You'll note though that

play06:47

the method signature has the option to pass an object

play06:50

as well, but default it's going to be none.

play06:52

And this line at the end

play06:54

of the dock string here is kinda interesting.

play06:56

So if an object is provided check permissions

play06:59

for that object.

play07:00

Now the documentation actually doesn't make any

play07:02

mention of this.

play07:03

So this is pretty interesting and encouraging.

play07:05

So maybe, maybe this is already kind of built in,

play07:08

let's see what we can do.

play07:10

So starting from the has per method here

play07:12

on the user instance,

play07:13

we did a little bit of digging and it turns out

play07:15

the rabbit hole goes pretty far.

play07:17

The has perm method actually calls a utility function,

play07:21

which references a method on the ModelBackend,

play07:24

which in turn references,

play07:27

a parent class of base Backend,

play07:29

and it goes down a few more levels.

play07:31

But ultimately what you'll find is that

play07:33

it will return false, has perm, the initial method,

play07:36

will return false because a ModelBackend dot get permissions

play07:41

is saying, if you pass any object,

play07:43

regardless of what's past there, if it's not none,

play07:45

it's going to return an empty set,

play07:46

because I don't know how, me being a ModelBackend is saying,

play07:51

Hey, I don't know how to evaluate permissions

play07:53

for a specific object. So I'm just going to say,

play07:55

no, you don't have any. Right?

play07:56

It's just feeling secure in that,

play07:57

in that manner, which is great.

play07:59

It's a good design choice. Unfortunately, it's,

play08:01

not going to obviously give us anything to work with

play08:04

out of the box.

play08:08

All right. So what solutions are available if Django,

play08:10

if it's not native in the framework?

play08:13

The first package we looked at

play08:15

you may be familiar with, is called django-guardian.

play08:18

And it provides basically object-based permissions

play08:21

through a direct assignment of objects

play08:24

to specific users or groups.

play08:27

So you can see from the code here,

play08:28

what we're doing is basically

play08:30

importing this user Object Permission.

play08:32

And then we're going to assign this permission to a user

play08:36

and a specific object.

play08:38

And then we can evaluate that for a specific object.

play08:40

And that's great. They've overridden that has perm method.

play08:45

This is great if you have what I'll call

play08:48

owner centric model.

play08:50

So think about, like,

play08:51

content management systems, blogs,

play08:52

where the author of a piece of content should typically

play08:55

be the owner. That's a great use case.

play08:58

Unfortunately, it doesn't quite work for us

play08:59

because we want to avoid those

play09:00

explicit relationships just because of the scale and breadth

play09:04

of which of resources that NetBox models.

play09:08

It doesn't really scale for us.

play09:11

Another package we looked at was django-rules.

play09:14

So this is really cool in that it allows the developer

play09:16

to define predicates and, and their terminology,

play09:19

which can then be referenced to enforce permissions

play09:23

or referenced by rules rather to enforce permissions.

play09:25

So here's an example of a predicate being set,

play09:28

is book author, which is basically some piece

play09:31

of logic that will determine

play09:33

whether or not a given user is an author of a book.

play09:36

Now, obviously that's a very kind of trite example,

play09:38

but you can imagine the flexibility as it forwards.

play09:41

And then you have rules here that can be set and evaluated.

play09:47

Tons of flexibility. Very cool.

play09:49

Unfortunately it does rely on static

play09:50

and predetermined rules, which weren't quite suitable

play09:53

for our case, because as I alluded to earlier,

play09:56

we don't really know how users

play09:57

are going to use NetBox sometimes,

play09:59

or how they're going to set up permissions

play10:02

and what kind of assignments they're actually

play10:04

going to be interested in evaluating.

play10:07

So unfortunately that didn't quite work out for us either.

play10:12

Can we build our own?

play10:14

Nothing out there, at least I don't think

play10:15

at the time suited to our needs.

play10:17

So we set about trying to do it on our own.

play10:20

Before we did that, we outlined some design goals.

play10:23

So first off we wanted to make sure we retained

play10:24

compatibility with a stock model-based permissions,

play10:27

that you get out of Django and out of the box.

play10:30

Second, we wanted to ensure that granting permissions

play10:33

could be done based on arbitrary object attributes,

play10:37

meaning we can say,

play10:38

okay, you have, there's the concept of a site

play10:41

in NetBox and say, you can assign users

play10:44

to just have access to sites in the Europe region

play10:46

or sites that have a status of active, for example.

play10:50

We want to make it that

play10:51

kind of to afford that degree of flexibility.

play10:56

We also want to provide a user-friendly interface and an API

play11:00

to make those assignments meaning that, you know,

play11:02

we don't want users to have to go and edit code somewhere

play11:05

to set up their rules for permission assignments.

play11:07

We want that all to be done through the user interface so

play11:09

that no one has to be a developer

play11:11

just to assign permissions.

play11:13

And finally, we want to make sure

play11:14

permissions apply immediately upon changes.

play11:17

And that's true in Django today.

play11:21

Yeah, so we thought for some time about how best to do this.

play11:26

And I think the aha moment really came about

play11:28

when we realized this mechanism for filtering objects

play11:31

by attributes, really already exists

play11:34

in the form of the Django ORM.

play11:36

That does exactly what we want.

play11:37

Think about if we want to find,

play11:39

if I want to find sites that are in Europe, I can do that.

play11:41

I can filter by the region to which the site is assigned,

play11:43

or I can filter by its status or,

play11:45

or any number of other attributes.

play11:47

I can traverse a related objects too.

play11:50

So all that functionality is kind of there.

play11:52

And we said, basically,

play11:53

we just want a way to capture that flexibility of their ORM

play11:57

during permissions enforcement.

play12:00

So the idea was, let's declare QuerySet Filters in JSon,

play12:05

and then just store them in the database.

play12:06

So we're basically just storing QuerySet Filters.

play12:10

Here's an example of that. So we've got JSon data there,

play12:13

this is just a string at top,

play12:16

we're filtering both region name, is Europe

play12:19

and its status is active.

play12:21

So this is a QuerySet Filter that applies the site model.

play12:24

So what we're going to do is take that string,

play12:26

which is just raw JSon,

play12:28

load it using the loads string functionality

play12:31

of the JSon library, into a dictionary called params.

play12:34

This is now a Python dictionary.

play12:37

Then we're going to pass that dictionary as a keyword

play12:40

filters to the um, I'm sorry,

play12:42

keyword arguments to the filter method

play12:43

for the site objects QuerySet.

play12:46

So pretty straightforward.

play12:51

To do this though, we need a way to save

play12:53

that JSon data in the database.

play12:54

So the answer there of course, was to create a new model.

play12:57

This is our ObjectPermission model,

play12:59

or rather a very simplified version of it.

play13:01

You can see some,

play13:02

some fields that you probably would expect first off,

play13:04

it has a name as a Boolean

play13:06

to indicate whether or not it's enabled.

play13:08

And then we have a bunch of many, too many fields.

play13:10

So basically, we're mapping object types, groups

play13:13

and users as many to many relationships.

play13:16

And then we have an array field

play13:17

on which we're

play13:20

recording a set of actions that apply to this permission.

play13:24

And we'll talk about that in a second.

play13:25

And then finally at the bottom,

play13:26

we see a JSon field called constraints.

play13:28

And that is where we are in the previous example here

play13:31

that the params, that's where we're saving

play13:34

that in the database.

play13:39

So basically this model is correlating content types

play13:44

or models with users and or groups and actions.

play13:49

So actions, again, going back to the view, add, change,

play13:51

delete that are provided by default from Django,

play13:54

and also, you can specify your custom actions if needed,

play13:58

and they're stored as just an array of strings.

play14:02

The constraints are optional.

play14:04

If you don't provide constraints that essentially replicates

play14:06

the built-in functionality of the Django permissions as they

play14:08

exist today, just in a different manner.

play14:11

But with constraints there,

play14:12

basically we're doing a logical AND, when combining them

play14:15

within an instance.

play14:15

So for our previous example we had a region and status.

play14:20

So it's going to be region is Europe AND status is active

play14:23

because they're defined as one,

play14:25

they're all defined as one set of constraints

play14:27

in a specific Object Permission instance.

play14:30

Whereas if we have multiple instances each

play14:32

with their own constraints,

play14:33

we're going to be doing a logical OR.

play14:34

So if they were two different permissions,

play14:36

we would say, you can, you know,

play14:37

you have access to sites in Europe,

play14:39

OR let's say any site that is active.

play14:45

It's important to note that this model really supplants

play14:47

Django's Stock Permission model.

play14:49

So we're not, we're no longer using that.

play14:53

And here they are side by side to give you a better idea

play14:56

just of the differences.

play14:57

So it's important to call out the Stock Permission model has

play15:01

one instance per content type per action.

play15:04

Whereas we kind of bundle all those up together

play15:06

in ObjectPermission to make things

play15:07

a little bit more streamlined.

play15:09

Typically within NetBox, you know, we see most,

play15:14

most of the right actions. So add change,

play15:16

delete are all going to be bumbled together.

play15:19

Not always the case, but where it is,

play15:21

and that's going to be 90 plus percent of the time.

play15:23

It's a little more efficient to do it in that manner.

play15:25

It's also important to call out the, the groups and users,

play15:28

many fields are actually on the Permission Object here,

play15:31

whereas in a Stock model,

play15:32

they're on the user and the group objects.

play15:37

Okay. So we have our model here.

play15:39

We have a way of conveying a QuerySet parameters,

play15:42

or filters rather.

play15:43

How do we actually apply them to a QuerySet?

play15:47

So there's a few steps.

play15:48

First, once we have these defined,

play15:49

we're going to retrieve all of the Object Permissions

play15:51

for a given user.

play15:53

And we do that by filtering on both the user

play15:55

or a group of which the user is a member

play15:58

for the specific action that we care about.

play16:00

And that double underscore contains,

play16:03

is referencing the view action.

play16:05

So here we care about view and of course we need

play16:07

to make sure they are enabled.

play16:09

That'll give us all instances of Object Permissions that

play16:11

apply for this user ,

play16:14

for the action that we care about.

play16:18

Second, we're going to build a QuerySet,

play16:20

that's filtered by all these constraints

play16:21

using again, that OR logic.

play16:23

So we start with a queue object and that allows us to do

play16:25

complex filtering on the OR,

play16:27

and basically iterate through these permissions

play16:29

that we retrieved from the database

play16:31

and build this constraints dictionary.

play16:32

So for every constraint that we find,

play16:35

every Object Permission instance, rather,

play16:37

we are OR-ing its constraints.

play16:39

So that all becomes this one big queue object

play16:41

that we can then apply directly

play16:43

to the filter method on a QuerySet.

play16:45

So model the objects that filter,

play16:46

and then just passing into strengths,

play16:48

which again is a queue object. It's not a dictionary.

play16:50

So we are just passing it directly.

play16:54

Once we have that, if we need to check

play16:56

for a specific object,

play16:57

so that's going to give us all objects.

play16:59

If we wanted to check for a specific object,

play17:00

we just filter by the primary key.

play17:02

So we're filtering by both primary key

play17:04

and the assigned permissions.

play17:05

So whether or not that primary key exists,

play17:07

it's only going to return,

play17:10

if the permissions actually apply to it.

play17:13

So basically we can check,

play17:14

we can call it dot first or dot exists to check

play17:16

whether those permissions are in fact valid.

play17:23

Obviously that's all a little bit unwieldy.

play17:25

So we created a QuerySet manager called

play17:28

RestrictedQuerySet. All this is doing is,

play17:31

is providing a dot restrict method to just allow passing

play17:34

the user and this desired action in when doing a query,

play17:38

obviously simplifies that quite a bit.

play17:41

And the permissions retrieval is actually being done

play17:44

behind the scenes or it's being cashed.

play17:48

Okay, so that kinda makes sense,

play17:51

or hopefully it's starting to make sense.

play17:53

What about user dot has perm? Right?

play17:54

That's kind of where we started with all this.

play17:56

What does that look like, with this implementation?

play18:01

So it became apparent pretty quickly that we would need

play18:03

a Custom Authentication Backend to support

play18:05

the built-in permissions evaluation logic.

play18:08

So what we did was extend Django's ModelBackend

play18:11

meaning the name of the class and then override.

play18:14

So we did two things.

play18:15

First, we overrode the has perm method and we overrid

play18:21

get all permissions.

play18:22

And this was the one, if you remember from earlier,

play18:24

this is the one that was actually responsible for returning

play18:28

an empty set, whenever an object was passed.

play18:30

So obviously we needed to remove, remove that check,

play18:34

but yeah, the meat of it is under the has perm method.

play18:39

So this method is really going to check a few things.

play18:41

First off it the user's inactive,

play18:43

we're going to return false.

play18:45

If it's a super user we're going to return true,

play18:48

then we're going to look for any model level permission

play18:50

assigned to the user.

play18:52

Because if you don't have a model of a level of permission,

play18:54

we don't need the next step,

play18:56

which is to check for the assignment

play19:00

of specific constraints within those permissions.

play19:04

So basically we're checking model level first,

play19:06

if you don't have, for example,

play19:07

if there are no permissions that will allow a user to mod-

play19:11

to modify any site,

play19:13

then we can go ahead and return false.

play19:14

We don't have to check for the constraints.

play19:16

But if we do find one or more,

play19:18

then we'll go ahead and check those constraints.

play19:20

It's also important to call out that

play19:22

the query is being done to object state in the database.

play19:25

Not in the instance.

play19:30

That's the, sorry, that's the attributes

play19:33

of the object being evaluated.

play19:35

So if I'm looking at a site, for example, and applying this,

play19:39

using this Backend for Object Permissions,

play19:41

it's going to be looking at the database representation

play19:43

of the site, not whatever might be instantiated

play19:47

in memory at the moment.

play19:51

Cool. So what about, so that's querying objects.

play19:54

What about modifying them?

play19:57

Queuering is fine if you just need to view obviously

play19:59

or delete, because we know what deleting

play20:02

will do to an object.

play20:04

However, it doesn't address modifying objects.

play20:06

So when we go to modify an object,

play20:08

we need to look at both the pre modification,

play20:10

so the initial state,

play20:13

what it looks like before we'd done anything

play20:15

and then we need to look at the post modification state.

play20:17

So after we've made our changes check that it's still

play20:20

in a permitted state.

play20:23

So for example, if let's say I have access to,

play20:26

I can change any active site,

play20:28

but then I go to an active site and I change it to be

play20:31

inactive or reserved, or, you know,

play20:32

some other status,

play20:34

the end condition then no longer is permitted.

play20:38

So the, the stance that we've taken is basically

play20:41

you can't modify an object

play20:43

into a non-permitted state because obviously

play20:46

then you would no longer be able to modify the object.

play20:48

And that probably disagrees with the reason why you weren't

play20:51

allowed to do that in the first place.

play20:54

So how do we, how do we do that?

play20:56

Right? Once our users change something,

play20:57

how do we go and figure out whether the modified version

play21:02

of that object agrees with the permissions?

play21:10

The answer we came up with was to use Atomic Transactions.

play21:13

So if you're not familiar just a real quick primer

play21:15

on Atomic Transactions.

play21:16

Basically we are going to wrap

play21:18

an underlying database transaction into an atomic action.

play21:22

So here we've invoked the context manager

play21:25

with transaction dot atomic.

play21:27

Then we're gonna obviously do some modify object in some way

play21:30

and then call save on it.

play21:32

And then we're looking for a specific exception

play21:34

if something bad happens.

play21:35

Then we can,

play21:37

that transaction gets aborted.

play21:40

So no change is being written to the database permanently.

play21:42

And then we can handle whatever exception we need to.

play21:46

This hasn't anything to do with permission specifically,

play21:48

this is again, just a general primer on atomic transactions.

play21:52

So the idea is you can make a,

play21:53

you can make a temporary modification to the database,

play21:56

run some other logic, and then if something went wrong,

play21:59

go ahead and report that.

play22:01

So here's how we're using Atomic Transactions

play22:03

for permissions enforcement.

play22:05

First, we're going to check the pre modified object state,

play22:08

Right? So this is us using that restrict method

play22:10

on the QuerySet.

play22:11

So first we're going to find

play22:14

the object.

play22:15

So we're going to filter the QuerySet,

play22:17

but using restrict by user and the desired action

play22:21

and by primary key, and to get that object.

play22:24

So if it doesn't exist, we're done.

play22:25

There's no more going off from here.

play22:27

But assuming we do find that valid object,

play22:29

we move on to the second step.

play22:31

So then we're going to modify it.

play22:32

So let's say I changed the object status to something bad,

play22:34

something I'm not allowed to do.

play22:37

And then I try to save it within an Atomic Transaction.

play22:43

After we call save, we then try that query again.

play22:45

So we're making another database query to retrieve

play22:48

the now modified state of that object.

play22:51

So we're running the same query again and just checking

play22:53

whether or not it exists. So it's a very lightweight query,

play22:55

but basically we're saying if it no longer exists,

play22:57

we know that whatever happened to that object during this,

play23:00

during the previous step, altered some attributes to a state

play23:04

that is not permitted by the assigned permissions.

play23:07

So now I can just say, raise PermissionsViolation

play23:10

and then in doing so undo the transaction.

play23:17

This ensures that the object is no longer model,

play23:19

it remains unmodified in the database.

play23:21

We do use a custom exception here to avoid accidentally

play23:24

catching it or catching

play23:27

unrelated exceptions from, you know,

play23:30

for other reasons.

play23:32

We do have to wrap

play23:34

and, you know, handle that exception.

play23:36

So we have to do something with that exception once it's

play23:38

been raised, ideally,

play23:40

or depending on what you're doing specifically,

play23:42

you might just raise and catch that. And then in turn,

play23:45

raise PermissionDenied now for API views and so forth.

play23:51

So other considerations to be, to be aware of,

play23:55

we have to predict all avenues for object modification,

play23:58

right? So this is pretty straightforward

play23:59

when you're just looking at, like, the user interface

play24:01

and traditional views that are serving a human.

play24:05

But when you bring in something like

play24:06

Django Rest Framework or graphing for Graph QL,

play24:10

all those API views potentially provide an avenue for users

play24:14

to modify objects. So all of those have to be,

play24:18

diligently protected in the same manner.

play24:21

As well, anywhere that, basically anywhere

play24:23

you're calling safe, bulk update, etcetera,

play24:25

any method that can modify the database has to be,

play24:29

has to be controlled and secured in some way.

play24:33

This approach does also work for bulk creating and editing,

play24:36

by the way, we just, you know,

play24:38

our examples have dealt with a single object,

play24:40

but you can do it in bulk, just as well.

play24:41

The only catches that reporting errors can be a little bit

play24:45

tricky if you're trying to update ten things at once,

play24:47

and then only like maybe two of the ten have,

play24:51

are no longer valid for the, you know,

play24:53

per the permissions policy.

play24:54

It can be kind of tricky to figure out, okay, well,

play24:57

how do I do it? Do I call out those specific two,

play24:59

for example, or do I just say,

play25:00

Hey, something you did went wrong.

play25:02

Unfortunately right now we're doing the latter,

play25:04

basically saying, Hey, you know,

play25:05

one of those thousand objects that you,

play25:08

that you just modified doesn't really doesn't really agree

play25:10

with the policy. So try that again.

play25:13

And the way in doing that is just using filter

play25:15

and then passing any list of primary keys versus

play25:17

a single primary key.

play25:20

There is plenty of room for improvement, of course,

play25:24

in the current implementation.

play25:25

And that's something we're, we're working on today.

play25:31

So as I mentioned,

play25:32

we're working on basically taking our implementation

play25:35

inside NetBox.

play25:36

It's been, it was introduced back in NetBox two dot 10,

play25:39

I believe. So about a year or so ago,

play25:43

and it's been, it's been working pretty well.

play25:46

Now we're actually starting to work

play25:48

on extracting the implementation, kind of prettying it up

play25:52

a bit and making it available as a standalone package.

play25:56

So, you know, decoupling it from the NetBox

play25:58

implementation into something that can stand on its own.

play26:04

We're, it's still a work in progress.

play26:07

We're shooting for end of 2021, early 2022.

play26:11

And this is going to be available as a repo,

play26:14

there you see the URL, Django object permissions

play26:16

under NS1 Labs.

play26:18

Any, you know, community help

play26:20

with development and testing would be greatly appreciated.

play26:23

We're pretty excited to have this split out from NetBox

play26:26

and to be able to make it accessible for the, you know,

play26:28

the greater Django community.

play26:32

So if you're interested in doing that by all means,

play26:34

you know, check it out and stop by, help us test it,

play26:37

document it.

play26:38

Anything else you think should be addressed or however

play26:41

we can extend it to make it more usable,

play26:43

more durable is obviously

play26:45

of great interest to us.

play26:49

So if you're interested, please check us out.

play26:52

All right. With that said,

play26:53

that should be wrapping up the talk.

play26:55

Again my name is Jeremy Stretch.

play26:57

Thank you everyone so much for attending

play26:59

my virtual presentation.

play27:01

I really appreciate it. I'll be available in chat

play27:03

and I think a few other avenues here to answer questions,

play27:07

any follow up questions, or again,

play27:09

you can always just catch me on Twitter or Slack

play27:12

and I'll see you around. Thanks again.

Rate This

5.0 / 5 (0 votes)

Etiquetas Relacionadas
DjangoPermissionsNetBoxSoftware DevelopmentAuthenticationAuthorizationORMQuerySetAPIDjangoCon
¿Necesitas un resumen en inglés?