Compare commits
17 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| 3324473cd0 | |||
| 39d8038533 | |||
| bbcf58705f | |||
| 7b5a0964b2 | |||
| 8eca76e464 | |||
| fb9ab368f8 | |||
| 877279b2ee | |||
| 301be4b411 | |||
| 728f527ccb | |||
| 3f1c790b1d | |||
| b00573bde2 | |||
| aeee3ad7f9 | |||
| ef021495ef | |||
| 061eab4b36 | |||
| 870e01f836 | |||
| e2ca72adf0 | |||
| 395ef43eae |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.12.3-stable
|
current_version = 0.12.5-stable
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||||
|
|||||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
|||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
run: docker build
|
run: docker build
|
||||||
--no-cache
|
--no-cache
|
||||||
-t beryju/passbook:0.12.3-stable
|
-t beryju/passbook:0.12.5-stable
|
||||||
-t beryju/passbook:latest
|
-t beryju/passbook:latest
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook:0.12.3-stable
|
run: docker push beryju/passbook:0.12.5-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook:latest
|
run: docker push beryju/passbook:latest
|
||||||
build-proxy:
|
build-proxy:
|
||||||
@ -48,11 +48,11 @@ jobs:
|
|||||||
cd proxy
|
cd proxy
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t beryju/passbook-proxy:0.12.3-stable \
|
-t beryju/passbook-proxy:0.12.5-stable \
|
||||||
-t beryju/passbook-proxy:latest \
|
-t beryju/passbook-proxy:latest \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook-proxy:0.12.3-stable
|
run: docker push beryju/passbook-proxy:0.12.5-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook-proxy:latest
|
run: docker push beryju/passbook-proxy:latest
|
||||||
build-static:
|
build-static:
|
||||||
@ -77,11 +77,11 @@ jobs:
|
|||||||
run: docker build
|
run: docker build
|
||||||
--no-cache
|
--no-cache
|
||||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||||
-t beryju/passbook-static:0.12.3-stable
|
-t beryju/passbook-static:0.12.5-stable
|
||||||
-t beryju/passbook-static:latest
|
-t beryju/passbook-static:latest
|
||||||
-f static.Dockerfile .
|
-f static.Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook-static:0.12.3-stable
|
run: docker push beryju/passbook-static:0.12.5-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook-static:latest
|
run: docker push beryju/passbook-static:latest
|
||||||
test-release:
|
test-release:
|
||||||
@ -114,5 +114,5 @@ jobs:
|
|||||||
SENTRY_PROJECT: passbook
|
SENTRY_PROJECT: passbook
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
tagName: 0.12.3-stable
|
tagName: 0.12.5-stable
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
|
|||||||
@ -25,7 +25,14 @@ RUN apt-get update && \
|
|||||||
pip install -r /requirements.txt --no-cache-dir && \
|
pip install -r /requirements.txt --no-cache-dir && \
|
||||||
apt-get remove --purge -y build-essential && \
|
apt-get remove --purge -y build-essential && \
|
||||||
apt-get autoremove --purge -y && \
|
apt-get autoremove --purge -y && \
|
||||||
adduser --system --no-create-home --uid 1000 --group --home /passbook passbook
|
# This is quite hacky, but docker has no guaranteed Group ID
|
||||||
|
# we could instead check for the GID of the socket and add the user dynamically,
|
||||||
|
# but then we have to drop permmissions later
|
||||||
|
groupadd -g 998 docker_998 && \
|
||||||
|
groupadd -g 999 docker_999 && \
|
||||||
|
adduser --system --no-create-home --uid 1000 --group --home /passbook passbook && \
|
||||||
|
usermod -a -G docker_998 passbook && \
|
||||||
|
usermod -a -G docker_999 passbook
|
||||||
|
|
||||||
COPY ./passbook/ /passbook
|
COPY ./passbook/ /passbook
|
||||||
COPY ./manage.py /
|
COPY ./manage.py /
|
||||||
|
|||||||
18
Pipfile.lock
generated
18
Pipfile.lock
generated
@ -74,18 +74,18 @@
|
|||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2e16f02c8b832d401d958d7ca0a14c5bc7da17827918e6b24e5bc43dce8f496e",
|
"sha256:a4784b01f545c8bd23df9369b24bcd31fb8d1b6256288b1b5680daefa2e33374",
|
||||||
"sha256:ab5353a968a4e664b9da2dd950169b755066525fcbfdfc90e7e49c8333d95c19"
|
"sha256:fb0e3dc534d6e34371c3b471fb3de8c287b18f700382b7b9bdb56e8c32ef83e4"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.16.0"
|
"version": "==1.16.2"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:226effa72e3ddd0a802e812c0e204999393ca7982fee754cc0c770a7a1caef3a",
|
"sha256:dc52d4eb5c2a4360506bdd8a99aca7ebc31c56849faf98c707e5201fcbb56957",
|
||||||
"sha256:9bf8586b69f20cf0a8ed1e27338cd10ce847751d1a2fd98b92662565c8a2df24"
|
"sha256:edb4292afe8c66099d45b3650da4757a228d38d25dbe884040cc1804a03d5020"
|
||||||
],
|
],
|
||||||
"version": "==1.19.0"
|
"version": "==1.19.2"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -373,11 +373,11 @@
|
|||||||
},
|
},
|
||||||
"drf-yasg2": {
|
"drf-yasg2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c4aa21d52f3964f99748eed68eb24be0fdad65e55bb56b99ae85c950718bac64",
|
"sha256:65826bf19e5222d38b84380468303c8c389d0b9e2335ee6efa4151ba87ca0a3f",
|
||||||
"sha256:e880b3fa298a614360f4d882e8bc1712b51e1b28696acbd2684ac0ab18275a62"
|
"sha256:6c662de6e0ffd4f74c49c06a88b8a9d1eb4bc9d7bfe82dac9f80a51a23cacecb"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.19.2"
|
"version": "==1.19.3"
|
||||||
},
|
},
|
||||||
"eight": {
|
"eight": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|||||||
@ -19,7 +19,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
server:
|
server:
|
||||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.3-stable}
|
image: beryju/passbook:${PASSBOOK_TAG:-0.12.5-stable}
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
PASSBOOK_REDIS__HOST: redis
|
PASSBOOK_REDIS__HOST: redis
|
||||||
@ -40,7 +40,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
worker:
|
worker:
|
||||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.3-stable}
|
image: beryju/passbook:${PASSBOOK_TAG:-0.12.5-stable}
|
||||||
command: worker
|
command: worker
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
@ -50,11 +50,11 @@ services:
|
|||||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
volumes:
|
volumes:
|
||||||
- ./backups:/backups
|
- ./backups:/backups
|
||||||
- /var/run/docker.socket:/var/run/docker.socket
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
static:
|
static:
|
||||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.3-stable}
|
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.5-stable}
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@ -95,7 +95,8 @@
|
|||||||
},
|
},
|
||||||
"model": "passbook_flows.flowstagebinding",
|
"model": "passbook_flows.flowstagebinding",
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"re_evaluate_policies": false
|
"evaluate_on_plan": false,
|
||||||
|
"re_evaluate_policies": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,7 +13,7 @@ Download the latest `docker-compose.yml` from [here](https://raw.githubuserconte
|
|||||||
|
|
||||||
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env`
|
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env`
|
||||||
|
|
||||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.12.3-stable >> .env`
|
To optionally deploy a different version run `echo PASSBOOK_TAG=0.12.5-stable >> .env`
|
||||||
|
|
||||||
If this is a fresh passbook install run the following commands to generate a password:
|
If this is a fresh passbook install run the following commands to generate a password:
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th
|
|||||||
image:
|
image:
|
||||||
name: beryju/passbook
|
name: beryju/passbook
|
||||||
name_static: beryju/passbook-static
|
name_static: beryju/passbook-static
|
||||||
tag: 0.12.3-stable
|
tag: 0.12.5-stable
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,11 @@ return False
|
|||||||
- `request.obj`: A Django Model instance. This is only set if the policy is ran against an object.
|
- `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.
|
- `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_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. Can be [compared](../expressions/index.md#comparing-ip-addresses)
|
- `pb_client_ip`: Client's IP Address or 255.255.255.255 if no IP Address could be extracted. Can be [compared](../expressions/index.md#comparing-ip-addresses), for example
|
||||||
|
|
||||||
|
```python
|
||||||
|
return pb_client_ip in ip_network('10.0.0.0/24')
|
||||||
|
```
|
||||||
|
|
||||||
Additionally, when the policy is executed from a flow, every variable from the flow's current context is accessible under the `context` object.
|
Additionally, when the policy is executed from a flow, every variable from the flow's current context is accessible under the `context` object.
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from docker.types import Healthcheck
|
|||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase, retry
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from passbook.stages.email.models import EmailStage, EmailTemplates
|
from passbook.stages.email.models import EmailStage, EmailTemplates
|
||||||
from passbook.stages.identification.models import IdentificationStage
|
from passbook.stages.identification.models import IdentificationStage
|
||||||
@ -34,6 +34,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_enroll_2_step(self):
|
def test_enroll_2_step(self):
|
||||||
"""Test 2-step enroll flow"""
|
"""Test 2-step enroll flow"""
|
||||||
# First stage fields
|
# First stage fields
|
||||||
@ -119,6 +120,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
|||||||
"foo@bar.baz",
|
"foo@bar.baz",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
@override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
|
@override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
|
||||||
def test_enroll_email(self):
|
def test_enroll_email(self):
|
||||||
"""Test enroll with Email verification"""
|
"""Test enroll with Email verification"""
|
||||||
|
|||||||
@ -5,13 +5,14 @@ from unittest.case import skipUnless
|
|||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestFlowsLogin(SeleniumTestCase):
|
class TestFlowsLogin(SeleniumTestCase):
|
||||||
"""test default login flow"""
|
"""test default login flow"""
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_login(self):
|
def test_login(self):
|
||||||
"""test default login flow"""
|
"""test default login flow"""
|
||||||
self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/")
|
self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/")
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from selenium.webdriver.common.by import By
|
|||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase, retry
|
||||||
from passbook.flows.models import Flow, FlowStageBinding
|
from passbook.flows.models import Flow, FlowStageBinding
|
||||||
from passbook.stages.otp_validate.models import OTPValidateStage
|
from passbook.stages.otp_validate.models import OTPValidateStage
|
||||||
|
|
||||||
@ -21,6 +21,7 @@ from passbook.stages.otp_validate.models import OTPValidateStage
|
|||||||
class TestFlowsOTP(SeleniumTestCase):
|
class TestFlowsOTP(SeleniumTestCase):
|
||||||
"""test flow with otp stages"""
|
"""test flow with otp stages"""
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_otp_validate(self):
|
def test_otp_validate(self):
|
||||||
"""test flow with otp stages"""
|
"""test flow with otp stages"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
@ -52,6 +53,7 @@ class TestFlowsOTP(SeleniumTestCase):
|
|||||||
USER().username,
|
USER().username,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_otp_totp_setup(self):
|
def test_otp_totp_setup(self):
|
||||||
"""test TOTP Setup stage"""
|
"""test TOTP Setup stage"""
|
||||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||||
@ -98,6 +100,7 @@ class TestFlowsOTP(SeleniumTestCase):
|
|||||||
|
|
||||||
self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists())
|
self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists())
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_otp_static_setup(self):
|
def test_otp_static_setup(self):
|
||||||
"""test Static OTP Setup stage"""
|
"""test Static OTP Setup stage"""
|
||||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from unittest.case import skipUnless
|
|||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase, retry
|
||||||
from passbook.core.models import User
|
from passbook.core.models import User
|
||||||
from passbook.flows.models import Flow, FlowDesignation
|
from passbook.flows.models import Flow, FlowDesignation
|
||||||
from passbook.providers.oauth2.generators import generate_client_secret
|
from passbook.providers.oauth2.generators import generate_client_secret
|
||||||
@ -16,6 +16,7 @@ from passbook.stages.password.models import PasswordStage
|
|||||||
class TestFlowsStageSetup(SeleniumTestCase):
|
class TestFlowsStageSetup(SeleniumTestCase):
|
||||||
"""test stage setup flows"""
|
"""test stage setup flows"""
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_password_change(self):
|
def test_password_change(self):
|
||||||
"""test password change flow"""
|
"""test password change flow"""
|
||||||
# Ensure that password stage has change_flow set
|
# Ensure that password stage has change_flow set
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from selenium.webdriver.common.by import By
|
|||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase, retry
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
from passbook.policies.expression.models import ExpressionPolicy
|
from passbook.policies.expression.models import ExpressionPolicy
|
||||||
@ -61,6 +61,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_authorization_consent_implied(self):
|
def test_authorization_consent_implied(self):
|
||||||
"""test OAuth Provider flow (default authorization flow with implied consent)"""
|
"""test OAuth Provider flow (default authorization flow with implied consent)"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
@ -115,6 +116,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
USER().username,
|
USER().username,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_authorization_consent_explicit(self):
|
def test_authorization_consent_explicit(self):
|
||||||
"""test OAuth Provider flow (default authorization flow with explicit consent)"""
|
"""test OAuth Provider flow (default authorization flow with explicit consent)"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
@ -184,6 +186,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
USER().username,
|
USER().username,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_denied(self):
|
def test_denied(self):
|
||||||
"""test OAuth Provider flow (default authorization flow, denied)"""
|
"""test OAuth Provider flow (default authorization flow, denied)"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from selenium.webdriver.common.keys import Keys
|
|||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase, retry
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.crypto.models import CertificateKeyPair
|
from passbook.crypto.models import CertificateKeyPair
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
@ -80,6 +80,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_redirect_uri_error(self):
|
def test_redirect_uri_error(self):
|
||||||
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
@ -122,6 +123,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
"Redirect URI Error",
|
"Redirect URI Error",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_authorization_consent_implied(self):
|
def test_authorization_consent_implied(self):
|
||||||
"""test OpenID Provider flow (default authorization flow with implied consent)"""
|
"""test OpenID Provider flow (default authorization flow with implied consent)"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
@ -183,6 +185,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
USER().email,
|
USER().email,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_authorization_logout(self):
|
def test_authorization_logout(self):
|
||||||
"""test OpenID Provider flow with logout"""
|
"""test OpenID Provider flow with logout"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
@ -252,6 +255,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
self.driver.find_element(By.ID, "logout").click()
|
self.driver.find_element(By.ID, "logout").click()
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_authorization_consent_explicit(self):
|
def test_authorization_consent_explicit(self):
|
||||||
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
|
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
@ -325,6 +329,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
USER().email,
|
USER().email,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_authorization_denied(self):
|
def test_authorization_denied(self):
|
||||||
"""test OpenID Provider flow (default authorization with access deny)"""
|
"""test OpenID Provider flow (default authorization with access deny)"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from selenium.webdriver.common.keys import Keys
|
|||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase, retry
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.crypto.models import CertificateKeyPair
|
from passbook.crypto.models import CertificateKeyPair
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
@ -76,6 +76,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
LOGGER.info("Container failed healthcheck")
|
LOGGER.info("Container failed healthcheck")
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_redirect_uri_error(self):
|
def test_redirect_uri_error(self):
|
||||||
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
@ -119,6 +120,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
"Redirect URI Error",
|
"Redirect URI Error",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_authorization_consent_implied(self):
|
def test_authorization_consent_implied(self):
|
||||||
"""test OpenID Provider flow (default authorization flow with implied consent)"""
|
"""test OpenID Provider flow (default authorization flow with implied consent)"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
@ -169,6 +171,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
|
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
|
||||||
self.assertEqual(body["UserInfo"]["email"], USER().email)
|
self.assertEqual(body["UserInfo"]["email"], USER().email)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_authorization_consent_explicit(self):
|
def test_authorization_consent_explicit(self):
|
||||||
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
|
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
@ -229,6 +232,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
|
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
|
||||||
self.assertEqual(body["UserInfo"]["email"], USER().email)
|
self.assertEqual(body["UserInfo"]["email"], USER().email)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_authorization_denied(self):
|
def test_authorization_denied(self):
|
||||||
"""test OpenID Provider flow (default authorization with access deny)"""
|
"""test OpenID Provider flow (default authorization with access deny)"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from docker.models.containers import Container
|
|||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase, retry
|
||||||
from passbook import __version__
|
from passbook import __version__
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
@ -57,6 +57,7 @@ class TestProviderProxy(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
return container
|
return container
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_proxy_simple(self):
|
def test_proxy_simple(self):
|
||||||
"""Test simple outpost setup with single provider"""
|
"""Test simple outpost setup with single provider"""
|
||||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||||
@ -110,6 +111,7 @@ class TestProviderProxy(SeleniumTestCase):
|
|||||||
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||||
"""Test Proxy connectivity over websockets"""
|
"""Test Proxy connectivity over websockets"""
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_proxy_connectivity(self):
|
def test_proxy_connectivity(self):
|
||||||
"""Test proxy connectivity over websocket"""
|
"""Test proxy connectivity over websocket"""
|
||||||
SeleniumTestCase().apply_default_data()
|
SeleniumTestCase().apply_default_data()
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from selenium.webdriver.common.keys import Keys
|
|||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase, retry
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.crypto.models import CertificateKeyPair
|
from passbook.crypto.models import CertificateKeyPair
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
@ -66,6 +66,7 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
LOGGER.info("Container failed healthcheck")
|
LOGGER.info("Container failed healthcheck")
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_sp_initiated_implicit(self):
|
def test_sp_initiated_implicit(self):
|
||||||
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
@ -105,6 +106,7 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
self.assertEqual(body["attr"]["mail"], [USER().email])
|
self.assertEqual(body["attr"]["mail"], [USER().email])
|
||||||
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_sp_initiated_explicit(self):
|
def test_sp_initiated_explicit(self):
|
||||||
"""test SAML Provider flow SP-initiated flow (explicit consent)"""
|
"""test SAML Provider flow SP-initiated flow (explicit consent)"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
@ -150,6 +152,7 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
self.assertEqual(body["attr"]["mail"], [USER().email])
|
self.assertEqual(body["attr"]["mail"], [USER().email])
|
||||||
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_idp_initiated_implicit(self):
|
def test_idp_initiated_implicit(self):
|
||||||
"""test SAML Provider flow IdP-initiated flow (implicit consent)"""
|
"""test SAML Provider flow IdP-initiated flow (implicit consent)"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
@ -195,6 +198,7 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
self.assertEqual(body["attr"]["mail"], [USER().email])
|
self.assertEqual(body["attr"]["mail"], [USER().email])
|
||||||
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_sp_initiated_denied(self):
|
def test_sp_initiated_denied(self):
|
||||||
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
|
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from selenium.webdriver.support import expected_conditions as ec
|
|||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
from yaml import safe_dump
|
from yaml import safe_dump
|
||||||
|
|
||||||
from e2e.utils import SeleniumTestCase
|
from e2e.utils import SeleniumTestCase, retry
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
from passbook.providers.oauth2.generators import (
|
from passbook.providers.oauth2.generators import (
|
||||||
generate_client_id,
|
generate_client_id,
|
||||||
@ -106,6 +106,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||||||
consumer_secret=self.client_secret,
|
consumer_secret=self.client_secret,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_oauth_enroll(self):
|
def test_oauth_enroll(self):
|
||||||
"""test OAuth Source With With OIDC"""
|
"""test OAuth Source With With OIDC"""
|
||||||
self.create_objects()
|
self.create_objects()
|
||||||
@ -159,6 +160,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||||||
"admin@example.com",
|
"admin@example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
@override_settings(SESSION_COOKIE_SAMESITE="strict")
|
@override_settings(SESSION_COOKIE_SAMESITE="strict")
|
||||||
def test_oauth_samesite_strict(self):
|
def test_oauth_samesite_strict(self):
|
||||||
"""test OAuth Source With SameSite set to strict
|
"""test OAuth Source With SameSite set to strict
|
||||||
@ -195,6 +197,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||||||
"Authentication Failed.",
|
"Authentication Failed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_oauth_enroll_auth(self):
|
def test_oauth_enroll_auth(self):
|
||||||
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
|
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
|
||||||
self.test_oauth_enroll()
|
self.test_oauth_enroll()
|
||||||
@ -291,6 +294,7 @@ class TestSourceOAuth1(SeleniumTestCase):
|
|||||||
consumer_secret=self.client_secret,
|
consumer_secret=self.client_secret,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_oauth_enroll(self):
|
def test_oauth_enroll(self):
|
||||||
"""test OAuth Source With With OIDC"""
|
"""test OAuth Source With With OIDC"""
|
||||||
self.create_objects()
|
self.create_objects()
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from selenium.webdriver.common.keys import Keys
|
|||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from e2e.utils import SeleniumTestCase
|
from e2e.utils import SeleniumTestCase, retry
|
||||||
from passbook.crypto.models import CertificateKeyPair
|
from passbook.crypto.models import CertificateKeyPair
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource
|
from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource
|
||||||
@ -92,6 +92,7 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_idp_redirect(self):
|
def test_idp_redirect(self):
|
||||||
"""test SAML Source With redirect binding"""
|
"""test SAML Source With redirect binding"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
@ -141,6 +142,7 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_idp_post(self):
|
def test_idp_post(self):
|
||||||
"""test SAML Source With post binding"""
|
"""test SAML Source With post binding"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
@ -192,6 +194,7 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
def test_idp_post_auto(self):
|
def test_idp_post_auto(self):
|
||||||
"""test SAML Source With post binding (auto redirect)"""
|
"""test SAML Source With post binding (auto redirect)"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
|
|||||||
43
e2e/utils.py
43
e2e/utils.py
@ -1,19 +1,22 @@
|
|||||||
"""passbook e2e testing utilities"""
|
"""passbook e2e testing utilities"""
|
||||||
|
from functools import wraps
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from importlib.util import module_from_spec, spec_from_file_location
|
from importlib.util import module_from_spec, spec_from_file_location
|
||||||
from inspect import getmembers, isfunction
|
from inspect import getmembers, isfunction
|
||||||
from os import environ, makedirs
|
from os import environ, makedirs
|
||||||
from time import sleep, time
|
from time import sleep, time
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Callable, Dict, Optional
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
|
from django.test.testcases import TransactionTestCase
|
||||||
from docker import DockerClient, from_env
|
from docker import DockerClient, from_env
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
|
from selenium.common.exceptions import TimeoutException
|
||||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||||
from selenium.webdriver.remote.webdriver import WebDriver
|
from selenium.webdriver.remote.webdriver import WebDriver
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
@ -123,3 +126,41 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
|||||||
func(apps, schema_editor)
|
func(apps, schema_editor)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def retry(max_retires=3, exceptions=None):
|
||||||
|
"""Retry test multiple times. Default to catching Selenium Timeout Exception"""
|
||||||
|
|
||||||
|
if not exceptions:
|
||||||
|
exceptions = [TimeoutException]
|
||||||
|
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
def retry_actual(func: Callable):
|
||||||
|
"""Retry test multiple times"""
|
||||||
|
count = 1
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(self: TransactionTestCase, *args, **kwargs):
|
||||||
|
"""Run test again if we're below max_retries, including tearDown and
|
||||||
|
setUp. Otherwise raise the error"""
|
||||||
|
nonlocal count
|
||||||
|
try:
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
# pylint: disable=catching-non-exception
|
||||||
|
except tuple(exceptions) as exc:
|
||||||
|
count += 1
|
||||||
|
if count > max_retires:
|
||||||
|
logger.debug("Exceeded retry count", exc=exc, test=self)
|
||||||
|
# pylint: disable=raising-non-exception
|
||||||
|
raise exc
|
||||||
|
logger.debug("Retrying on error", exc=exc, test=self)
|
||||||
|
self.tearDown()
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self._post_teardown()
|
||||||
|
self.setUp()
|
||||||
|
return wrapper(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return retry_actual
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: "0.12.3-stable"
|
appVersion: "0.12.5-stable"
|
||||||
description: A Helm chart for passbook.
|
description: A Helm chart for passbook.
|
||||||
name: passbook
|
name: passbook
|
||||||
version: "0.12.3-stable"
|
version: "0.12.5-stable"
|
||||||
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
|
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: postgresql
|
- name: postgresql
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
image:
|
image:
|
||||||
name: beryju/passbook
|
name: beryju/passbook
|
||||||
name_static: beryju/passbook-static
|
name_static: beryju/passbook-static
|
||||||
tag: 0.12.3-stable
|
tag: 0.12.5-stable
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
"""passbook"""
|
"""passbook"""
|
||||||
__version__ = "0.12.3-stable"
|
__version__ = "0.12.5-stable"
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
|
<img class="pf-c-avatar" src="{% avatar user %}" alt="">
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% block page_content %}
|
{% block page_content %}
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<div class="pf-c-form__group">
|
<div class="pf-c-form__group">
|
||||||
<div class="form-control-static">
|
<div class="form-control-static">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
|
<img class="pf-c-avatar" src="{% avatar user %}" alt="">
|
||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
|
|||||||
@ -27,7 +27,15 @@ class FlowStageBindingSerializer(ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = FlowStageBinding
|
model = FlowStageBinding
|
||||||
fields = ["pk", "target", "stage", "re_evaluate_policies", "order", "policies"]
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"target",
|
||||||
|
"stage",
|
||||||
|
"evaluate_on_plan",
|
||||||
|
"re_evaluate_policies",
|
||||||
|
"order",
|
||||||
|
"policies",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class FlowStageBindingViewSet(ModelViewSet):
|
class FlowStageBindingViewSet(ModelViewSet):
|
||||||
|
|||||||
@ -50,12 +50,10 @@ class FlowStageBindingForm(forms.ModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
"target",
|
"target",
|
||||||
"stage",
|
"stage",
|
||||||
|
"evaluate_on_plan",
|
||||||
"re_evaluate_policies",
|
"re_evaluate_policies",
|
||||||
"order",
|
"order",
|
||||||
]
|
]
|
||||||
labels = {
|
|
||||||
"re_evaluate_policies": _("Re-evaluate Policies"),
|
|
||||||
}
|
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"name": forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from django.http.request import HttpRequest
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import User
|
from passbook.core.models import User
|
||||||
@ -20,7 +21,9 @@ class StageMarker:
|
|||||||
"""Base stage marker class, no extra attributes, and has no special handler."""
|
"""Base stage marker class, no extra attributes, and has no special handler."""
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def process(self, plan: "FlowPlan", stage: Stage) -> Optional[Stage]:
|
def process(
|
||||||
|
self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest]
|
||||||
|
) -> Optional[Stage]:
|
||||||
"""Process callback for this marker. This should be overridden by sub-classes.
|
"""Process callback for this marker. This should be overridden by sub-classes.
|
||||||
If a stage should be removed, return None."""
|
If a stage should be removed, return None."""
|
||||||
return stage
|
return stage
|
||||||
@ -33,10 +36,14 @@ class ReevaluateMarker(StageMarker):
|
|||||||
binding: PolicyBinding
|
binding: PolicyBinding
|
||||||
user: User
|
user: User
|
||||||
|
|
||||||
def process(self, plan: "FlowPlan", stage: Stage) -> Optional[Stage]:
|
def process(
|
||||||
|
self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest]
|
||||||
|
) -> Optional[Stage]:
|
||||||
"""Re-evaluate policies bound to stage, and if they fail, remove from plan"""
|
"""Re-evaluate policies bound to stage, and if they fail, remove from plan"""
|
||||||
engine = PolicyEngine(self.binding, self.user)
|
engine = PolicyEngine(self.binding, self.user)
|
||||||
engine.use_cache = False
|
engine.use_cache = False
|
||||||
|
if http_request:
|
||||||
|
engine.request.http_request = http_request
|
||||||
engine.request.context = plan.context
|
engine.request.context = plan.context
|
||||||
engine.build()
|
engine.build()
|
||||||
result = engine.result
|
result = engine.result
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 3.1.2 on 2020-10-20 12:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_flows", "0014_auto_20200925_2332"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flowstagebinding",
|
||||||
|
name="re_evaluate_policies",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Evaluate policies when the Stage is present to the user.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="flowstagebinding",
|
||||||
|
name="evaluate_on_plan",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Evaluate policies during the Flow planning process. Disable this for input-based policies.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -154,15 +154,19 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
|
|||||||
target = models.ForeignKey("Flow", on_delete=models.CASCADE)
|
target = models.ForeignKey("Flow", on_delete=models.CASCADE)
|
||||||
stage = InheritanceForeignKey(Stage, on_delete=models.CASCADE)
|
stage = InheritanceForeignKey(Stage, on_delete=models.CASCADE)
|
||||||
|
|
||||||
re_evaluate_policies = models.BooleanField(
|
evaluate_on_plan = models.BooleanField(
|
||||||
default=False,
|
default=True,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
(
|
(
|
||||||
"When this option is enabled, the planner will re-evaluate "
|
"Evaluate policies during the Flow planning process. "
|
||||||
"policies bound to this binding."
|
"Disable this for input-based policies."
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
re_evaluate_policies = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text=_("Evaluate policies when the Stage is present to the user."),
|
||||||
|
)
|
||||||
|
|
||||||
order = models.IntegerField()
|
order = models.IntegerField()
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class FlowPlan:
|
|||||||
self.stages.append(stage)
|
self.stages.append(stage)
|
||||||
self.markers.append(marker or StageMarker())
|
self.markers.append(marker or StageMarker())
|
||||||
|
|
||||||
def next(self) -> Optional[Stage]:
|
def next(self, http_request: Optional[HttpRequest]) -> Optional[Stage]:
|
||||||
"""Return next pending stage from the bottom of the list"""
|
"""Return next pending stage from the bottom of the list"""
|
||||||
if not self.has_stages:
|
if not self.has_stages:
|
||||||
return None
|
return None
|
||||||
@ -55,7 +55,7 @@ class FlowPlan:
|
|||||||
|
|
||||||
if marker.__class__ is not StageMarker:
|
if marker.__class__ is not StageMarker:
|
||||||
LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker)
|
LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker)
|
||||||
marked_stage = marker.process(self, stage)
|
marked_stage = marker.process(self, stage, http_request)
|
||||||
if not marked_stage:
|
if not marked_stage:
|
||||||
LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage)
|
LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage)
|
||||||
self.stages.remove(stage)
|
self.stages.remove(stage)
|
||||||
@ -63,7 +63,7 @@ class FlowPlan:
|
|||||||
if not self.has_stages:
|
if not self.has_stages:
|
||||||
return None
|
return None
|
||||||
# pylint: disable=not-callable
|
# pylint: disable=not-callable
|
||||||
return self.next()
|
return self.next(http_request)
|
||||||
return marked_stage
|
return marked_stage
|
||||||
|
|
||||||
def pop(self):
|
def pop(self):
|
||||||
@ -159,23 +159,41 @@ class FlowPlanner:
|
|||||||
for binding in FlowStageBinding.objects.filter(
|
for binding in FlowStageBinding.objects.filter(
|
||||||
target__pk=self.flow.pk
|
target__pk=self.flow.pk
|
||||||
).order_by("order"):
|
).order_by("order"):
|
||||||
|
binding: FlowStageBinding
|
||||||
|
stage = binding.stage
|
||||||
|
marker = StageMarker()
|
||||||
|
if binding.evaluate_on_plan:
|
||||||
|
LOGGER.debug(
|
||||||
|
"f(plan): evaluating on plan",
|
||||||
|
stage=binding.stage,
|
||||||
|
flow=self.flow,
|
||||||
|
)
|
||||||
engine = PolicyEngine(binding, user, request)
|
engine = PolicyEngine(binding, user, request)
|
||||||
engine.request.context = plan.context
|
engine.request.context = plan.context
|
||||||
engine.build()
|
engine.build()
|
||||||
if engine.passing:
|
if engine.passing:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"f(plan): Stage passing", stage=binding.stage, flow=self.flow
|
"f(plan): Stage passing",
|
||||||
|
stage=binding.stage,
|
||||||
|
flow=self.flow,
|
||||||
)
|
)
|
||||||
plan.stages.append(binding.stage)
|
else:
|
||||||
marker = StageMarker()
|
stage = None
|
||||||
if binding.re_evaluate_policies:
|
else:
|
||||||
|
LOGGER.debug(
|
||||||
|
"f(plan): not evaluating on plan",
|
||||||
|
stage=binding.stage,
|
||||||
|
flow=self.flow,
|
||||||
|
)
|
||||||
|
if binding.re_evaluate_policies and stage:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"f(plan): Stage has re-evaluate marker",
|
"f(plan): Stage has re-evaluate marker",
|
||||||
stage=binding.stage,
|
stage=binding.stage,
|
||||||
flow=self.flow,
|
flow=self.flow,
|
||||||
)
|
)
|
||||||
marker = ReevaluateMarker(binding=binding, user=user)
|
marker = ReevaluateMarker(binding=binding, user=user)
|
||||||
plan.markers.append(marker)
|
if stage:
|
||||||
|
plan.append(stage, marker)
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"f(plan): Finished building",
|
"f(plan): Finished building",
|
||||||
flow=self.flow,
|
flow=self.flow,
|
||||||
|
|||||||
@ -86,7 +86,7 @@ class FlowExecutorView(View):
|
|||||||
return to_stage_response(self.request, self.handle_invalid_flow(exc))
|
return to_stage_response(self.request, self.handle_invalid_flow(exc))
|
||||||
# We don't save the Plan after getting the next stage
|
# We don't save the Plan after getting the next stage
|
||||||
# as it hasn't been successfully passed yet
|
# as it hasn't been successfully passed yet
|
||||||
next_stage = self.plan.next()
|
next_stage = self.plan.next(self.request)
|
||||||
if not next_stage:
|
if not next_stage:
|
||||||
LOGGER.debug("f(exec): no more stages, flow is done.")
|
LOGGER.debug("f(exec): no more stages, flow is done.")
|
||||||
return self._flow_done()
|
return self._flow_done()
|
||||||
|
|||||||
@ -22,6 +22,7 @@ error_reporting:
|
|||||||
send_pii: false
|
send_pii: false
|
||||||
|
|
||||||
passbook:
|
passbook:
|
||||||
|
avatars: gravatar # gravatar or none
|
||||||
branding:
|
branding:
|
||||||
title: passbook
|
title: passbook
|
||||||
title_show: true
|
title_show: true
|
||||||
|
|||||||
@ -6,15 +6,19 @@ from django import template
|
|||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
|
from django.templatetags.static import static
|
||||||
from django.utils.html import escape, mark_safe
|
from django.utils.html import escape, mark_safe
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.core.models import User
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
from passbook.lib.utils.urls import is_url_absolute
|
from passbook.lib.utils.urls import is_url_absolute
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def back(context: Context) -> str:
|
def back(context: Context) -> str:
|
||||||
@ -54,37 +58,23 @@ def css_class(field, css):
|
|||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def gravatar(email, size=None, rating=None):
|
def avatar(user: User) -> str:
|
||||||
"""
|
"""Get avatar, depending on passbook.avatar setting"""
|
||||||
Generates a Gravatar URL for the given email address.
|
mode = CONFIG.raw.get("passbook").get("avatars")
|
||||||
|
if mode == "none":
|
||||||
Syntax::
|
return static("passbook/user-default.png")
|
||||||
|
if mode == "gravatar":
|
||||||
{% gravatar <email> [size] [rating] %}
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
{% gravatar someone@example.com 48 pg %}
|
|
||||||
"""
|
|
||||||
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
|
||||||
gravatar_url = "%savatar/%s" % (
|
|
||||||
"https://secure.gravatar.com/",
|
|
||||||
md5(email.encode("utf-8")).hexdigest(), # nosec
|
|
||||||
)
|
|
||||||
|
|
||||||
parameters = [
|
parameters = [
|
||||||
p
|
("s", "158"),
|
||||||
for p in (
|
("r", "g"),
|
||||||
("s", size or "158"),
|
|
||||||
("r", rating or "g"),
|
|
||||||
)
|
|
||||||
if p[1]
|
|
||||||
]
|
]
|
||||||
|
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
||||||
if parameters:
|
mail_hash = md5(user.email.encode("utf-8")).hexdigest() # nosec
|
||||||
gravatar_url += "?" + urlencode(parameters, doseq=True)
|
gravatar_url = (
|
||||||
|
f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
|
||||||
|
)
|
||||||
return escape(gravatar_url)
|
return escape(gravatar_url)
|
||||||
|
raise ValueError(f"Invalid avatar mode {mode}")
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
|
|||||||
@ -24,7 +24,10 @@ class DockerController(BaseController):
|
|||||||
|
|
||||||
def __init__(self, outpost: Outpost) -> None:
|
def __init__(self, outpost: Outpost) -> None:
|
||||||
super().__init__(outpost)
|
super().__init__(outpost)
|
||||||
|
try:
|
||||||
self.client = from_env()
|
self.client = from_env()
|
||||||
|
except DockerException as exc:
|
||||||
|
raise ControllerException from exc
|
||||||
|
|
||||||
def _get_labels(self) -> Dict[str, str]:
|
def _get_labels(self) -> Dict[str, str]:
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||||
<span class="pf-c-form__label-text">PASSBOOK_TOKEN</span>
|
<span class="pf-c-form__label-text">PASSBOOK_TOKEN</span>
|
||||||
</label>
|
</label>
|
||||||
|
{# TODO: Only load key on modal open #}
|
||||||
<input class="pf-c-form-control" data-pb-fetch-key="key" data-pb-fetch-fill="{% url 'passbook_api:token-view-key' identifier=outpost.token_identifier %}" readonly type="text" value="" />
|
<input class="pf-c-form-control" data-pb-fetch-key="key" data-pb-fetch-fill="{% url 'passbook_api:token-view-key' identifier=outpost.token_identifier %}" readonly type="text" value="" />
|
||||||
</div>
|
</div>
|
||||||
<h3>{% trans 'If your passbook Instance is using a self-signed certificate, set this value.' %}</h3>
|
<h3>{% trans 'If your passbook Instance is using a self-signed certificate, set this value.' %}</h3>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
"""passbook expression policy evaluator"""
|
"""passbook expression policy evaluator"""
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address, ip_network
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
@ -22,6 +22,8 @@ class PolicyEvaluator(BaseEvaluator):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self._messages = []
|
self._messages = []
|
||||||
self._context["pb_message"] = self.expr_func_message
|
self._context["pb_message"] = self.expr_func_message
|
||||||
|
self._context["ip_address"] = ip_address
|
||||||
|
self._context["ip_network"] = ip_network
|
||||||
self._filename = policy_name or "PolicyEvaluator"
|
self._filename = policy_name or "PolicyEvaluator"
|
||||||
|
|
||||||
def expr_func_message(self, message: str):
|
def expr_func_message(self, message: str):
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
"""Integrate ./manage.py test with pytest"""
|
"""Integrate ./manage.py test with pytest"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
class PytestTestRunner:
|
class PytestTestRunner:
|
||||||
"""Runs pytest to discover and run tests."""
|
"""Runs pytest to discover and run tests."""
|
||||||
@ -11,6 +13,7 @@ class PytestTestRunner:
|
|||||||
self.keepdb = keepdb
|
self.keepdb = keepdb
|
||||||
settings.TEST = True
|
settings.TEST = True
|
||||||
settings.CELERY_TASK_ALWAYS_EAGER = True
|
settings.CELERY_TASK_ALWAYS_EAGER = True
|
||||||
|
CONFIG.raw.get("passbook")["avatars"] = "none"
|
||||||
|
|
||||||
def run_tests(self, test_labels):
|
def run_tests(self, test_labels):
|
||||||
"""Run pytest and return the exitcode.
|
"""Run pytest and return the exitcode.
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
"""passbook password stage"""
|
"""passbook password stage"""
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth import _clean_credentials
|
from django.contrib.auth import _clean_credentials
|
||||||
from django.contrib.auth.backends import BaseBackend
|
from django.contrib.auth.backends import BaseBackend
|
||||||
from django.contrib.auth.signals import user_login_failed
|
from django.contrib.auth.signals import user_login_failed
|
||||||
@ -122,5 +121,4 @@ class PasswordStageView(FormView, StageView):
|
|||||||
self.executor.plan.context[
|
self.executor.plan.context[
|
||||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
] = user.backend
|
] = user.backend
|
||||||
messages.success(self.request, _("Successfully logged in!"))
|
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|||||||
@ -39,4 +39,5 @@ class UserLoginStageView(StageView):
|
|||||||
flow_slug=self.executor.flow.slug,
|
flow_slug=self.executor.flow.slug,
|
||||||
session_duration=self.executor.current_stage.session_duration,
|
session_duration=self.executor.current_stage.session_duration,
|
||||||
)
|
)
|
||||||
|
messages.success(self.request, _("Successfully logged in!"))
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|||||||
6
passbook/static/static/package-lock.json
generated
6
passbook/static/static/package-lock.json
generated
@ -442,9 +442,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rollup": {
|
"rollup": {
|
||||||
"version": "2.32.0",
|
"version": "2.32.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.32.1.tgz",
|
||||||
"integrity": "sha512-0FIG1jY88uhCP2yP4CfvtKEqPDRmsUwfY1kEOOM+DH/KOGATgaIFd/is1+fQOxsvh62ELzcFfKonwKWnHhrqmw==",
|
"integrity": "sha512-Op2vWTpvK7t6/Qnm1TTh7VjEZZkN8RWgf0DHbkKzQBwNf748YhXbozHVefqpPp/Fuyk/PQPAnYsBxAEtlMvpUw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"fsevents": "~2.1.2"
|
"fsevents": "~2.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
"codemirror": "^5.58.1",
|
"codemirror": "^5.58.1",
|
||||||
"lit-element": "^2.4.0",
|
"lit-element": "^2.4.0",
|
||||||
"lit-html": "^1.3.0",
|
"lit-html": "^1.3.0",
|
||||||
"rollup": "^2.32.0"
|
"rollup": "^2.32.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"rollup-plugin-commonjs": "^10.1.0",
|
"rollup-plugin-commonjs": "^10.1.0",
|
||||||
|
|||||||
BIN
passbook/static/static/passbook/user-default.png
Normal file
BIN
passbook/static/static/passbook/user-default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
@ -1,3 +1,3 @@
|
|||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
const VERSION = "0.12.3-stable"
|
const VERSION = "0.12.5-stable"
|
||||||
|
|||||||
13
swagger.yaml
13
swagger.yaml
@ -833,6 +833,11 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
- name: evaluate_on_plan
|
||||||
|
in: query
|
||||||
|
description: ''
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
- name: re_evaluate_policies
|
- name: re_evaluate_policies
|
||||||
in: query
|
in: query
|
||||||
description: ''
|
description: ''
|
||||||
@ -6337,10 +6342,14 @@ definitions:
|
|||||||
title: Stage
|
title: Stage
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
evaluate_on_plan:
|
||||||
|
title: Evaluate on plan
|
||||||
|
description: Evaluate policies during the Flow planning process. Disable this
|
||||||
|
for input-based policies.
|
||||||
|
type: boolean
|
||||||
re_evaluate_policies:
|
re_evaluate_policies:
|
||||||
title: Re evaluate policies
|
title: Re evaluate policies
|
||||||
description: When this option is enabled, the planner will re-evaluate policies
|
description: Evaluate policies when the Stage is present to the user.
|
||||||
bound to this binding.
|
|
||||||
type: boolean
|
type: boolean
|
||||||
order:
|
order:
|
||||||
title: Order
|
title: Order
|
||||||
|
|||||||
Reference in New Issue
Block a user