diff --git a/docs/permissions/intro.md b/docs/permissions/intro.md index 4c6a652d..3859c82f 100644 --- a/docs/permissions/intro.md +++ b/docs/permissions/intro.md @@ -3,37 +3,60 @@ 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. @@ -41,3 +64,16 @@ 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. diff --git a/docs/queries/queries.md b/docs/queries/queries.md index 71dade2d..decb654f 100644 --- a/docs/queries/queries.md +++ b/docs/queries/queries.md @@ -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 diff --git a/docs/release-notes.md b/docs/release-notes.md index 110ccc07..5b25e17a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -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 @@ -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 diff --git a/docs_src/permissions/advanced.py b/docs_src/permissions/advanced.py index bdb7922c..5899f79c 100644 --- a/docs_src/permissions/advanced.py +++ b/docs_src/permissions/advanced.py @@ -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) diff --git a/docs_src/permissions/primary_key.py b/docs_src/permissions/primary_key.py new file mode 100644 index 00000000..3dff0b05 --- /dev/null +++ b/docs_src/permissions/primary_key.py @@ -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) diff --git a/docs_src/permissions/quickstart.py b/docs_src/permissions/quickstart.py index c6c4daa7..efa415b8 100644 --- a/docs_src/permissions/quickstart.py +++ b/docs_src/permissions/quickstart.py @@ -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) diff --git a/docs_src/queries/clauses/style/or.py b/docs_src/queries/clauses/style/or.py index 2099bcd6..62bb2efb 100644 --- a/docs_src/queries/clauses/style/or.py +++ b/docs_src/queries/clauses/style/or.py @@ -5,5 +5,5 @@ await User.query.create(name="Adam", email="adam@edgy.dev") await User.query.create(name="Eve", email="eve@edgy.dev") -# Query using the or_ +# Query using the global or_ with multiple ANDed field queries await User.query.or_(name="Adam", email="adam@edgy.dev") diff --git a/docs_src/queries/clauses/style/or_two.py b/docs_src/queries/clauses/style/or_two.py index 22d2ca54..b423ee20 100644 --- a/docs_src/queries/clauses/style/or_two.py +++ b/docs_src/queries/clauses/style/or_two.py @@ -4,7 +4,10 @@ await User.query.create(name="Eve", email="eve@edgy.dev") # 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") diff --git a/edgy/__init__.py b/edgy/__init__.py index 62d1d738..8b2ae552 100644 --- a/edgy/__init__.py +++ b/edgy/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.17.4" +__version__ = "0.18.0" from .cli.base import Migrate from .conf import settings diff --git a/mkdocs.yml b/mkdocs.yml index 0b53ffb2..c610f61b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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"