diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b76df232..dbb0e4d62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,8 @@ jobs: python-version: '3.8' - name: Install pyright run: npm install -g pyright + - name: Show pyright version + run: pyright --version - name: Install dependencies run: sudo pip install -U wheel pipenv && pipenv install --dev - name: Lint with pyright diff --git a/Pipfile b/Pipfile index 5230760fc..5a6c688d9 100644 --- a/Pipfile +++ b/Pipfile @@ -40,7 +40,6 @@ signxml = "*" structlog = "*" swagger-spec-validator = "*" urllib3 = {extras = ["secure"],version = "*"} -jinja2 = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 3f4fd3420..f09bc6204 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,10 +18,10 @@ "default": { "amqp": { "hashes": [ - "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8", - "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d" + "sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b", + "sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139" ], - "version": "==2.5.2" + "version": "==2.6.0" }, "asgiref": { "hashes": [ @@ -53,26 +53,26 @@ }, "boto3": { "hashes": [ - "sha256:1bdab4f87ff39d5aab59b0aae69965bf604fa5608984c673877f4c62c1f16240", - "sha256:2b4924ccc1603d562969b9f3c8c74ff4a1f3bdbafe857c990422c73d8e2e229e" + "sha256:bcaa88b2f81b88741c47da52f3414c876236700441df87b6198f860e6a200d6f", + "sha256:e974e7a3bbdbd6a73ffc07bea5fa0c0744a5a8b87dcca94702597176e3de465e" ], "index": "pypi", - "version": "==1.13.18" + "version": "==1.13.23" }, "botocore": { "hashes": [ - "sha256:93574cf95a64c71d35c12c93a23f6214cf2f4b461be3bda3a436381cbe126a84", - "sha256:e65eb27cae262a510e335bc0c0e286e9e42381b1da0aafaa79fa13c1d8d74a95" + "sha256:5831068c9b49b4c91b0733e0ec784a7733d8732359d73c67a07a0b0868433cae", + "sha256:7778957bdc9a25dd33bb4383ebd6d45a8570a2cbff03d1edf430fdacec2b7437" ], - "version": "==1.16.18" + "version": "==1.16.23" }, "celery": { "hashes": [ - "sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f", - "sha256:5b4b37e276033fe47575107a2775469f0b721646a08c96ec2c61531e4fe45f2a" + "sha256:9ae2e73b93cc7d6b48b56aaf49a68c91752d0ffd7dfdcc47f842ca79a6f13eae", + "sha256:c2037b6a8463da43b19969a0fc13f9023ceca6352b4dd51be01c66fbbb13647e" ], "index": "pypi", - "version": "==4.4.2" + "version": "==4.4.4" }, "certifi": { "hashes": [ @@ -169,11 +169,11 @@ }, "django": { "hashes": [ - "sha256:051ba55d42daa3eeda3944a8e4df2bc96d4c62f94316dea217248a22563c3621", - "sha256:9aaa6a09678e1b8f0d98a948c56482eac3e3dd2ddbfb8de70a868135ef3b5e01" + "sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2", + "sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8" ], "index": "pypi", - "version": "==3.0.6" + "version": "==3.0.7" }, "django-cors-middleware": { "hashes": [ @@ -345,7 +345,6 @@ "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484", "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668" ], - "index": "pypi", "version": "==3.0.0a1" }, "jmespath": { @@ -364,11 +363,11 @@ }, "kombu": { "hashes": [ - "sha256:2d1cda774126a044d91a7ff5fa6d09edf99f46924ab332a810760fe6740e9b76", - "sha256:598e7e749d6ab54f646b74b2d2df67755dee13894f73ab02a2a9feb8870c7cb2" + "sha256:437b9cdea193cc2ed0b8044c85fd0f126bb3615ca2f4d4a35b39de7cacfa3c1a", + "sha256:dc282bb277197d723bccda1a9ba30a27a28c9672d0ab93e9e51bb05a37bd29c3" ], "index": "pypi", - "version": "==4.6.8" + "version": "==4.6.10" }, "ldap3": { "hashes": [ @@ -688,10 +687,10 @@ }, "redis": { "hashes": [ - "sha256:2ef11f489003f151777c064c5dbc6653dfb9f3eade159bcadc524619fddc2242", - "sha256:6d65e84bc58091140081ee9d9c187aab0480097750fac44239307a3bdf0b1251" + "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", + "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" ], - "version": "==3.5.2" + "version": "==3.5.3" }, "requests": { "hashes": [ @@ -858,10 +857,10 @@ }, "autopep8": { "hashes": [ - "sha256:152fd8fe47d02082be86e05001ec23d6f420086db56b17fc883f3f965fb34954" + "sha256:60fd8c4341bab59963dafd5d2a566e94f547e660b9b396f772afe67d8481dbf0" ], "index": "pypi", - "version": "==1.5.2" + "version": "==1.5.3" }, "bandit": { "hashes": [ @@ -948,11 +947,11 @@ }, "django": { "hashes": [ - "sha256:051ba55d42daa3eeda3944a8e4df2bc96d4c62f94316dea217248a22563c3621", - "sha256:9aaa6a09678e1b8f0d98a948c56482eac3e3dd2ddbfb8de70a868135ef3b5e01" + "sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2", + "sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8" ], "index": "pypi", - "version": "==3.0.6" + "version": "==3.0.7" }, "django-debug-toolbar": { "hashes": [ @@ -971,10 +970,10 @@ }, "gitpython": { "hashes": [ - "sha256:864a47472548f3ba716ca202e034c1900f197c0fb3a08f641c20c3cafd15ed94", - "sha256:da3b2cf819974789da34f95ac218ef99f515a928685db141327c09b73dd69c09" + "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a", + "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac" ], - "version": "==3.1.2" + "version": "==3.1.3" }, "isort": { "hashes": [ @@ -1133,10 +1132,10 @@ }, "stevedore": { "hashes": [ - "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b", - "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b" + "sha256:001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9", + "sha256:471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691" ], - "version": "==1.32.0" + "version": "==2.0.0" }, "toml": { "hashes": [ diff --git a/docs/expressions/index.md b/docs/expressions/index.md new file mode 100644 index 000000000..4355dfca3 --- /dev/null +++ b/docs/expressions/index.md @@ -0,0 +1,55 @@ +# Expressions + +Expressions allow you to write custom Logic using Python code. + +Expressions are used in different places throughout passbook, and can do different things. + +!!! info + These functions/objects are available wherever expressions are used. For more specific information, see [Expression Policies](../policies/expression.md) and [Property Mappings](../property-mappings/expression.md) + +## Global objects + +- `pb_logger`: structlog BoundLogger. ([ref](https://www.structlog.org/en/stable/api.html#structlog.BoundLogger)) +- `requests`: requests Session object. ([ref](https://requests.readthedocs.io/en/master/user/advanced/)) + +## Generally available functions + +### `regex_match(value: Any, regex: str) -> bool` + +Check if `value` matches Regular Expression `regex`. + +Example: + +```python +return regex_match(request.user.username, '.*admin.*') +``` + +### `regex_replace(value: Any, regex: str, repl: str) -> str` + +Replace anything matching `regex` within `value` with `repl` and return it. + +Example: + +```python +user_email_local = regex_replace(request.user.email, '(.+)@.+', '') +``` + +### `pb_is_group_member(user: User, **group_filters) -> bool` + +Check if `user` is member of a group matching `**group_filters`. + +Example: + +```python +return pb_is_group_member(request.user, name="test_group") +``` + +### `pb_user_by(**filters) -> Optional[User]` + +Fetch a user matching `**filters`. Returns None if no user was found. + +Example: + +```python +other_user = pb_user_by(username="other_user") +``` diff --git a/docs/property-mappings/reference/user-object.md b/docs/expressions/reference/user-object.md similarity index 84% rename from docs/property-mappings/reference/user-object.md rename to docs/expressions/reference/user-object.md index 8cc35c162..5cdb0780a 100644 --- a/docs/property-mappings/reference/user-object.md +++ b/docs/expressions/reference/user-object.md @@ -15,6 +15,7 @@ The User object has the following attributes: List all the User's Group Names -```jinja2 -[{% for group in user.groups.all() %}'{{ group.name }}',{% endfor %}] +```python +for group in user.groups.all(): + yield group.name ``` diff --git a/docs/policies/expression.md b/docs/policies/expression.md new file mode 100644 index 000000000..e3d0812cb --- /dev/null +++ b/docs/policies/expression.md @@ -0,0 +1,27 @@ +# Expression Policies + +The passing of the policy is determined by the return value of the code. Use `return True` to pass a policy and `return False` to fail it. + +### Available Functions + +#### `pb_message(message: str)` + +Add a message, visible by the end user. This can be used to show the reason why they were denied. + +Example: + +```python +pb_message("Access denied") +return False +``` + +### Context variables + +- `request`: A PolicyRequest object, which has the following properties: + - `request.user`: The current User, which the Policy is applied against. ([ref](../expressions/reference/user-object.md)) + - `request.http_request`: The Django HTTP Request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects)) + - `request.obj`: A Django Model instance. This is only set if the Policy is ran against an object. + - `request.context`: A dictionary with dynamic data. This depends on the origin of the execution. +- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external Provider. +- `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted. +- `pb_flow_plan`: Current Plan if Policy is called from the Flow Planner. diff --git a/docs/policies/expression/index.md b/docs/policies/expression/index.md deleted file mode 100644 index 2bf6b2fb0..000000000 --- a/docs/policies/expression/index.md +++ /dev/null @@ -1,22 +0,0 @@ -# Expression Policy - -Expression Policies allows you to write custom Policy Logic using Jinja2 Templating language. - -For a language reference, see [here](https://jinja.palletsprojects.com/en/2.11.x/templates/). - -The following objects are passed into the variable: - -- `request`: A PolicyRequest object, which has the following properties: - - `request.user`: The current User, which the Policy is applied against. ([ref](../../property-mappings/reference/user-object.md)) - - `request.http_request`: The Django HTTP Request, as documented [here](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects). - - `request.obj`: A Django Model instance. This is only set if the Policy is ran against an object. -- `pb_flow_plan`: Current Plan if Policy is called while a flow is active. -- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external Provider. -- `pb_is_group_member(user, group_name)`: Function which checks if `user` is member of a Group with Name `gorup_name`. -- `pb_logger`: Standard Python Logger Object, which can be used to debug expressions. -- `pb_client_ip`: Client's IP Address. - -There are also the following custom filters available: - -- `regex_match(regex)`: Return True if value matches `regex` -- `regex_replace(regex, repl)`: Replace string matched by `regex` with `repl` diff --git a/docs/policies/index.md b/docs/policies/index.md index b413a7794..362f43d54 100644 --- a/docs/policies/index.md +++ b/docs/policies/index.md @@ -8,10 +8,6 @@ There are two different Kind of policies, a Standard Policy and a Password Polic --- -### Group-Membership Policy - -This policy evaluates to True if the current user is a Member of the selected group. - ### Reputation Policy passbook keeps track of failed login attempts by Source IP and Attempted Username. These values are saved as scores. Each failed login decreases the Score for the Client IP as well as the targeted Username by one. @@ -20,11 +16,7 @@ This policy can be used to for example prompt Clients with a low score to pass a ## Expression Policy -See [Expression Policy](expression/index.md). - -### Webhook Policy - -This policy allows you to send an arbitrary HTTP Request to any URL. You can then use JSONPath to extract the result you need. +See [Expression Policy](expression.md). ## Password Policies diff --git a/docs/property-mappings/expression.md b/docs/property-mappings/expression.md new file mode 100644 index 000000000..a25ee2aad --- /dev/null +++ b/docs/property-mappings/expression.md @@ -0,0 +1,9 @@ +# Property Mapping Expressions + +The property mapping should return a value that is expected by the Provider/Source. What types are supported, is documented in the individual Provider/Source. Returning `None` is always accepted, this simply skips this mapping. + +### Context Variables + +- `user`: The current user, this might be `None` if there is no contextual user. ([ref](../expressions/reference/user-object.md)) +- `request`: The current request, this might be `None` if there is no contextual request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects)) +- Arbitrary other arguments given by the provider, this is documented on the Provider/Source. diff --git a/docs/property-mappings/index.md b/docs/property-mappings/index.md index 0eebcb0d7..7e7e21e17 100644 --- a/docs/property-mappings/index.md +++ b/docs/property-mappings/index.md @@ -12,10 +12,10 @@ You can find examples [here](integrations/) LDAP Property Mappings are used when you define a LDAP Source. These Mappings define which LDAP Property maps to which passbook Property. By default, these mappings are created: -- Autogenerated LDAP Mapping: givenName -> first_name -- Autogenerated LDAP Mapping: mail -> email -- Autogenerated LDAP Mapping: name -> name -- Autogenerated LDAP Mapping: sAMAccountName -> username -- Autogenerated LDAP Mapping: sn -> last_name +- Autogenerated LDAP Mapping: givenName -> first_name +- Autogenerated LDAP Mapping: mail -> email +- Autogenerated LDAP Mapping: name -> name +- Autogenerated LDAP Mapping: sAMAccountName -> username +- Autogenerated LDAP Mapping: sn -> last_name These are configured for the most common LDAP Setups. diff --git a/mkdocs.yml b/mkdocs.yml index cb31d86a1..2fd537455 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,14 +10,17 @@ nav: - Kubernetes: installation/kubernetes.md - Sources: sources.md - Providers: providers.md + - Expressions: + - Overview: expressions/index.md + - Reference: + - User Object: expressions/reference/user-object.md - Property Mappings: - Overview: property-mappings/index.md - - Reference: - - User Object: property-mappings/reference/user-object.md + - Expressions: property-mappings/expression.md - Factors: factors.md - Policies: - Overview: policies/index.md - - Expression: policies/expression/index.md + - Expression: policies/expression.md - Integrations: - as Provider: - Amazon Web Services: integrations/services/aws/index.md @@ -38,3 +41,11 @@ markdown_extensions: - toc: permalink: "ΒΆ" - admonition + - codehilite + - pymdownx.betterem: + smart_enable: all + - pymdownx.inlinehilite + - pymdownx.magiclink + +plugins: + - search diff --git a/passbook/admin/forms/source.py b/passbook/admin/forms/source.py index b18d7157a..2207dec1c 100644 --- a/passbook/admin/forms/source.py +++ b/passbook/admin/forms/source.py @@ -1,4 +1,17 @@ """passbook core source form fields""" -SOURCE_FORM_FIELDS = ["name", "slug", "enabled"] -SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"] +SOURCE_FORM_FIELDS = [ + "name", + "slug", + "enabled", + "authentication_flow", + "enrollment_flow", +] +SOURCE_SERIALIZER_FIELDS = [ + "pk", + "name", + "slug", + "enabled", + "authentication_flow", + "enrollment_flow", +] diff --git a/passbook/admin/templates/administration/base.html b/passbook/admin/templates/administration/base.html index 7c4ccc5f7..864c526c5 100644 --- a/passbook/admin/templates/administration/base.html +++ b/passbook/admin/templates/administration/base.html @@ -14,7 +14,7 @@ - + {% endblock %} {% block page_content %} diff --git a/passbook/admin/templates/administration/overview.html b/passbook/admin/templates/administration/overview.html index d053f8c3d..21dcb8906 100644 --- a/passbook/admin/templates/administration/overview.html +++ b/passbook/admin/templates/administration/overview.html @@ -55,15 +55,26 @@
{% trans 'No Stages configured. No Users will be able to login.' %}">
{% else %} - {{ factor_count }} + {{ stage_count }} {% endif %}+ {{ form.non_field_errors }} +
+{{ field.help_text|safe }}
+ {% endif %}{{ field.help_text }}
+{{ field.help_text|safe }}
{% endif %}{{ field.help_text }}
+{{ field.help_text|safe }}
{% endif %}