Skip to content

Commit

Permalink
update permissions and query docs and bump version (#197)
Browse files Browse the repository at this point in the history
Changes:
- update docs
- bump version
  • Loading branch information
devkral authored Oct 14, 2024
1 parent 84f6b68 commit 72c31bd
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 10 deletions.
46 changes: 41 additions & 5 deletions docs/permissions/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,77 @@
One of the most basic needs in the database world is permission handling. Some approaches work via
database users but this is neither portable nor has the flexibility of users which reside as a normal table.

## Permission scopes
## Permission objects

### Users

Users are the main entrypoint in nearly every application. For every user multiple attributes can be checked.
Users are the main entrypoint in nearly every application. Permissions require such a class. The class itself is referenced via
a ManyToMany field named `users`.

### Groups

Groups can be useful for organizing permissions in sets which can be applied to users. In the Permission template they are optional.
When used the Permission object needs a ManyToMany field named `groups`.

### Model names

Model names are a scope limiter for permssions. Instead of allowing e.g. users to edit everything they can only edit blogs. Like groups they are optional.
They are enabled via a CharField or TextField named `name_model`. Reason: pydantic occupies the `model_` prefix.
When not wanting an extra field and having only object related permissions you can also check model names against objects.

### Objects

Even stricter than model names are object related permissions. For this we use a ContentType to represent all possible model objects. Like groups they are optional.
Permissions can be directly assigned to object instances via ContentTypes. This is useful for per object permissions.
Again this feature is optional. However if `name_model` is not specified `model_names` are checked against the ContentType.
The feature can be enabled via a ForeignKey named `obj` to ContentType.

## How to use

Permission models detect automatically which features they have. This is why there are field names which are strictly enforced.

E.g. the field `groups` must be a ManyToMany field which points to `Group` when using groups.
There are 3 extra manager methods:

## Quickstart
- permissions_of(sources)
- users(...)
- groups(...)

### Parameters of users and groups

Except permissions all of the parameters are optional

- permissions (str/Sequence[str]) - Permission names.
- model_names (str/Sequence[str/None]) - Model names. Only available with `name_model` or `obj`.
- objects (Object/Sequence[Object/None]) - Objects permissions are tied to.
- include_null_model_name (Default: True) - When model_names are not None automatically add a check for a null model_name.
- include_null_object (Default: True) - When objects are not None automatically add a check for a null model_name.

Why the last ones? If you want to untie a Permission you can simply set `obj` or `name_model` to None and voila the Permission has now a broader scope.

## Quickstart

```python
{!> ../docs_src/permissions/quickstart.py !}
```

Despite not necessary it is recommended to use unique_together for the fields used to identify a Permission.

## Advanced

Here an advanced example with all possible fields set.

```python
{!> ../docs_src/permissions/advanced.py !}
```

## Advanced with primary keys

Edgy has a very flexible overwrite logic. Instead of using unique_together, following code is possible:


```python
{!> ../docs_src/permissions/primary_key.py !}
```

However permissions cannot change their scope this way and there is a little overhead because the primary keys are used for the foreign keys.

You can also just overwrite name with a primary key field. This way the implicit id is removed.
2 changes: 1 addition & 1 deletion docs/queries/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ It is the merge of the former methods `using` (with a positional argument) and `

### Using with `with_schema`

This is an **alternative** to [using](#using) and serves solely as the purpose of avoiding
This is an **alternative** to `[using](#selecting-the-database-and-schema)` and serves solely as the purpose of avoiding
writing all the time `Model.query.using(...)`.

You can use `with_schema(...)` to tell the query to always query
Expand Down
28 changes: 27 additions & 1 deletion docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ hide:
- Autogenerated `id` wasn't added in model_dump.
- Tenants worked only till the first query.

### Breaking changes (upgrade path)

``` python
set_tenant("foo")
```

Becomes now

``` python
with with_tenant("foo"):
...
```


``` python
activate_schema("foo")
...
deactivate_schema()
```

Becomes now

``` python
with with_schema("foo"):
...
```

## 0.17.4

Expand Down Expand Up @@ -382,7 +408,7 @@ field with `auto_now`.

### Added

- Added new experimental [activate_schema](./tenancy/edgy.md#using-with-activate_schema) for tenant models using the `using` queryset operator.
- Added new experimental `activate_schema` for tenant models using the `using` queryset operator.

### Fixed

Expand Down
7 changes: 7 additions & 0 deletions docs_src/permissions/advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,10 @@ class Permission(BasePermission):
class Meta:
registry = models
unique_together = [("name", "name_model", "obj")]


user = User.query.create(name="edgy")
group = Group.query.create(name="edgy", users=[user])
permission = await Permission.query.create(users=[user], groups=[group], name="view", obj=user)
assert await Permission.query.users("view", objects=user).get() == user
await Permission.query.permissions_of(user)
38 changes: 38 additions & 0 deletions docs_src/permissions/primary_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import edgy
from edgy.contrib.permissions import BasePermission

models = edgy.Registry("sqlite:///foo.sqlite3")


class User(edgy.Model):
name = edgy.fields.CharField(max_length=100)

class Meta:
registry = models


class Group(edgy.Model):
name = edgy.fields.CharField(max_length=100)
users = edgy.fields.ManyToMany("User", embed_through=False)

class Meta:
registry = models


class Permission(BasePermission):
# overwrite name of BasePermission with a CharField with primary_key=True
name: str = edgy.fields.CharField(max_length=100, primary_key=True)
users = edgy.fields.ManyToMany("User", embed_through=False)
groups = edgy.fields.ManyToMany("Group", embed_through=False)
name_model: str = edgy.fields.CharField(max_length=100, null=True, primary_key=True)
obj = edgy.fields.ForeignKey("ContentType", null=True, primary_key=True)

class Meta:
registry = models


user = User.query.create(name="edgy")
group = Group.query.create(name="edgy", users=[user])
permission = await Permission.query.create(users=[user], groups=[group], name="view", obj=user)
assert await Permission.query.users("view", objects=user).get() == user
await Permission.query.permissions_of(user)
2 changes: 2 additions & 0 deletions docs_src/permissions/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ class Meta:
unique_together = [("name",)]


user = User.query.create(name="edgy")
permission = await Permission.query.create(users=[user], name="view")
assert await Permission.query.users("view").get() == user
await Permission.query.permissions_of(user)
2 changes: 1 addition & 1 deletion docs_src/queries/clauses/style/or.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
await User.query.create(name="Adam", email="[email protected]")
await User.query.create(name="Eve", email="[email protected]")

# Query using the or_
# Query using the global or_ with multiple ANDed field queries
await User.query.or_(name="Adam", email="[email protected]")
5 changes: 4 additions & 1 deletion docs_src/queries/clauses/style/or_two.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
await User.query.create(name="Eve", email="[email protected]")

# Query using the multiple or_
await User.query.or_({"email__icontains": "edgy"}, {"name__icontains": "a"})

# QUery using the global or
await User.query.or_(email__icontains="edgy").or_(name__icontains="a")

# Query using the or_ with multiple fields
# Query using the or_ with multiple ANDed field queries
await User.query.or_(email__icontains="edgy", name__icontains="a")
2 changes: 1 addition & 1 deletion edgy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.17.4"
__version__ = "0.18.0"

from .cli.base import Migrate
from .conf import settings
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ nav:
- ContentTypes:
- Introduction: "contenttypes/intro.md"
- ContentTags - or how to replace elastic search: "contenttypes/replace-elasticsearch.md"
- Permissions:
- Introduction: "permissions/intro.md"
- Tenancy:
- Edgy: "tenancy/edgy.md"
- Contrib: "tenancy/contrib.md"
Expand Down

0 comments on commit 72c31bd

Please sign in to comment.