Compare commits

...

17 Commits

Author SHA1 Message Date
3324473cd0 new release: 0.12.5-stable 2020-10-22 14:22:32 +02:00
39d8038533 e2e: Fix @retry decorator not truncating database 2020-10-22 14:05:29 +02:00
bbcf58705f lib: add configurable avatars, set to none mode for tests 2020-10-22 14:03:31 +02:00
7b5a0964b2 outposts: handle docker connection error on init 2020-10-22 12:50:06 +02:00
8eca76e464 root: fix docker permission error 2020-10-22 11:54:23 +02:00
fb9ab368f8 root: fix typo in docker-compose 2020-10-22 11:30:53 +02:00
877279b2ee build(deps): bump rollup in /passbook/static/static (#292)
Bumps [rollup](https://github.com/rollup/rollup) from 2.32.0 to 2.32.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.32.0...v2.32.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-22 11:30:03 +02:00
301be4b411 build(deps): bump boto3 from 1.16.1 to 1.16.2 (#291)
Bumps [boto3](https://github.com/boto/boto3) from 1.16.1 to 1.16.2.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.16.1...1.16.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-22 08:05:29 +02:00
728f527ccb build(deps): bump drf-yasg2 from 1.19.2 to 1.19.3 (#290)
Bumps [drf-yasg2](https://github.com/JoelLefkowitz/drf-yasg) from 1.19.2 to 1.19.3.
- [Release notes](https://github.com/JoelLefkowitz/drf-yasg/releases)
- [Changelog](https://github.com/JoelLefkowitz/drf-yasg/blob/master/docs/changelog.rst)
- [Commits](https://github.com/JoelLefkowitz/drf-yasg/compare/1.19.2...1.19.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-21 09:51:39 +02:00
3f1c790b1d build(deps): bump boto3 from 1.16.0 to 1.16.1 (#289)
Bumps [boto3](https://github.com/boto/boto3) from 1.16.0 to 1.16.1.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.16.0...1.16.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-21 09:12:13 +02:00
b00573bde2 new release: 0.12.4-stable 2020-10-20 22:31:31 +02:00
aeee3ad7f9 e2e: add @retry decorator to make e2e tests more reliable 2020-10-20 18:51:17 +02:00
ef021495ef flows: revert evaluate_on_call rename for backwards compatibility 2020-10-20 15:41:50 +02:00
061eab4b36 docs: fix keys for example flows 2020-10-20 15:14:41 +02:00
870e01f836 flows: rename re_evaluate_policies to evaluate_on_call, add evaluate_on_plan 2020-10-20 15:06:36 +02:00
e2ca72adf0 stages/user_login: only show successful login message at login stage 2020-10-20 12:11:59 +02:00
395ef43eae policies/expression: fix ip_network not being imported by default 2020-10-20 12:05:56 +02:00
46 changed files with 267 additions and 110 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.12.3-stable
current_version = 0.12.5-stable
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)

View File

@ -18,11 +18,11 @@ jobs:
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:0.12.3-stable
-t beryju/passbook:0.12.5-stable
-t beryju/passbook:latest
-f Dockerfile .
- 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)
run: docker push beryju/passbook:latest
build-proxy:
@ -48,11 +48,11 @@ jobs:
cd proxy
docker build \
--no-cache \
-t beryju/passbook-proxy:0.12.3-stable \
-t beryju/passbook-proxy:0.12.5-stable \
-t beryju/passbook-proxy:latest \
-f Dockerfile .
- 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)
run: docker push beryju/passbook-proxy:latest
build-static:
@ -77,11 +77,11 @@ jobs:
run: docker build
--no-cache
--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
-f static.Dockerfile .
- 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)
run: docker push beryju/passbook-static:latest
test-release:
@ -114,5 +114,5 @@ jobs:
SENTRY_PROJECT: passbook
SENTRY_URL: https://sentry.beryju.org
with:
tagName: 0.12.3-stable
tagName: 0.12.5-stable
environment: beryjuorg-prod

View File

@ -25,7 +25,14 @@ RUN apt-get update && \
pip install -r /requirements.txt --no-cache-dir && \
apt-get remove --purge -y build-essential && \
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 ./manage.py /

18
Pipfile.lock generated
View File

@ -74,18 +74,18 @@
},
"boto3": {
"hashes": [
"sha256:2e16f02c8b832d401d958d7ca0a14c5bc7da17827918e6b24e5bc43dce8f496e",
"sha256:ab5353a968a4e664b9da2dd950169b755066525fcbfdfc90e7e49c8333d95c19"
"sha256:a4784b01f545c8bd23df9369b24bcd31fb8d1b6256288b1b5680daefa2e33374",
"sha256:fb0e3dc534d6e34371c3b471fb3de8c287b18f700382b7b9bdb56e8c32ef83e4"
],
"index": "pypi",
"version": "==1.16.0"
"version": "==1.16.2"
},
"botocore": {
"hashes": [
"sha256:226effa72e3ddd0a802e812c0e204999393ca7982fee754cc0c770a7a1caef3a",
"sha256:9bf8586b69f20cf0a8ed1e27338cd10ce847751d1a2fd98b92662565c8a2df24"
"sha256:dc52d4eb5c2a4360506bdd8a99aca7ebc31c56849faf98c707e5201fcbb56957",
"sha256:edb4292afe8c66099d45b3650da4757a228d38d25dbe884040cc1804a03d5020"
],
"version": "==1.19.0"
"version": "==1.19.2"
},
"cachetools": {
"hashes": [
@ -373,11 +373,11 @@
},
"drf-yasg2": {
"hashes": [
"sha256:c4aa21d52f3964f99748eed68eb24be0fdad65e55bb56b99ae85c950718bac64",
"sha256:e880b3fa298a614360f4d882e8bc1712b51e1b28696acbd2684ac0ab18275a62"
"sha256:65826bf19e5222d38b84380468303c8c389d0b9e2335ee6efa4151ba87ca0a3f",
"sha256:6c662de6e0ffd4f74c49c06a88b8a9d1eb4bc9d7bfe82dac9f80a51a23cacecb"
],
"index": "pypi",
"version": "==1.19.2"
"version": "==1.19.3"
},
"eight": {
"hashes": [

View File

@ -19,7 +19,7 @@ services:
networks:
- internal
server:
image: beryju/passbook:${PASSBOOK_TAG:-0.12.3-stable}
image: beryju/passbook:${PASSBOOK_TAG:-0.12.5-stable}
command: server
environment:
PASSBOOK_REDIS__HOST: redis
@ -40,7 +40,7 @@ services:
env_file:
- .env
worker:
image: beryju/passbook:${PASSBOOK_TAG:-0.12.3-stable}
image: beryju/passbook:${PASSBOOK_TAG:-0.12.5-stable}
command: worker
networks:
- internal
@ -50,11 +50,11 @@ services:
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
volumes:
- ./backups:/backups
- /var/run/docker.socket:/var/run/docker.socket
- /var/run/docker.sock:/var/run/docker.sock
env_file:
- .env
static:
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.3-stable}
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.5-stable}
networks:
- internal
labels:

View File

@ -95,7 +95,8 @@
},
"model": "passbook_flows.flowstagebinding",
"attrs": {
"re_evaluate_policies": false
"evaluate_on_plan": false,
"re_evaluate_policies": true
}
},
{

View File

@ -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 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:

View File

@ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th
image:
name: beryju/passbook
name_static: beryju/passbook-static
tag: 0.12.3-stable
tag: 0.12.5-stable
nameOverride: ""

View File

@ -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.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. 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.

View File

@ -8,7 +8,7 @@ from docker.types import Healthcheck
from selenium.webdriver.common.by import By
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.stages.email.models import EmailStage, EmailTemplates
from passbook.stages.identification.models import IdentificationStage
@ -34,6 +34,7 @@ class TestFlowsEnroll(SeleniumTestCase):
),
}
@retry()
def test_enroll_2_step(self):
"""Test 2-step enroll flow"""
# First stage fields
@ -119,6 +120,7 @@ class TestFlowsEnroll(SeleniumTestCase):
"foo@bar.baz",
)
@retry()
@override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
def test_enroll_email(self):
"""Test enroll with Email verification"""

View File

@ -5,13 +5,14 @@ from unittest.case import skipUnless
from selenium.webdriver.common.by import By
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")
class TestFlowsLogin(SeleniumTestCase):
"""test default login flow"""
@retry()
def test_login(self):
"""test default login flow"""
self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/")

View File

@ -12,7 +12,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
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.stages.otp_validate.models import OTPValidateStage
@ -21,6 +21,7 @@ from passbook.stages.otp_validate.models import OTPValidateStage
class TestFlowsOTP(SeleniumTestCase):
"""test flow with otp stages"""
@retry()
def test_otp_validate(self):
"""test flow with otp stages"""
sleep(1)
@ -52,6 +53,7 @@ class TestFlowsOTP(SeleniumTestCase):
USER().username,
)
@retry()
def test_otp_totp_setup(self):
"""test TOTP Setup stage"""
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())
@retry()
def test_otp_static_setup(self):
"""test Static OTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")

View File

@ -5,7 +5,7 @@ from unittest.case import skipUnless
from selenium.webdriver.common.by import By
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.flows.models import Flow, FlowDesignation
from passbook.providers.oauth2.generators import generate_client_secret
@ -16,6 +16,7 @@ from passbook.stages.password.models import PasswordStage
class TestFlowsStageSetup(SeleniumTestCase):
"""test stage setup flows"""
@retry()
def test_password_change(self):
"""test password change flow"""
# Ensure that password stage has change_flow set

View File

@ -9,7 +9,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
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.flows.models import Flow
from passbook.policies.expression.models import ExpressionPolicy
@ -61,6 +61,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
},
}
@retry()
def test_authorization_consent_implied(self):
"""test OAuth Provider flow (default authorization flow with implied consent)"""
# Bootstrap all needed objects
@ -115,6 +116,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
USER().username,
)
@retry()
def test_authorization_consent_explicit(self):
"""test OAuth Provider flow (default authorization flow with explicit consent)"""
# Bootstrap all needed objects
@ -184,6 +186,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
USER().username,
)
@retry()
def test_denied(self):
"""test OAuth Provider flow (default authorization flow, denied)"""
# Bootstrap all needed objects

View File

@ -10,7 +10,7 @@ from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
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.crypto.models import CertificateKeyPair
from passbook.flows.models import Flow
@ -80,6 +80,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
},
}
@retry()
def test_redirect_uri_error(self):
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
sleep(1)
@ -122,6 +123,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
"Redirect URI Error",
)
@retry()
def test_authorization_consent_implied(self):
"""test OpenID Provider flow (default authorization flow with implied consent)"""
sleep(1)
@ -183,6 +185,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
USER().email,
)
@retry()
def test_authorization_logout(self):
"""test OpenID Provider flow with logout"""
sleep(1)
@ -252,6 +255,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
)
self.driver.find_element(By.ID, "logout").click()
@retry()
def test_authorization_consent_explicit(self):
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
sleep(1)
@ -325,6 +329,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
USER().email,
)
@retry()
def test_authorization_denied(self):
"""test OpenID Provider flow (default authorization with access deny)"""
sleep(1)

View File

@ -12,7 +12,7 @@ from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
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.crypto.models import CertificateKeyPair
from passbook.flows.models import Flow
@ -76,6 +76,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
LOGGER.info("Container failed healthcheck")
sleep(1)
@retry()
def test_redirect_uri_error(self):
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
sleep(1)
@ -119,6 +120,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
"Redirect URI Error",
)
@retry()
def test_authorization_consent_implied(self):
"""test OpenID Provider flow (default authorization flow with implied consent)"""
sleep(1)
@ -169,6 +171,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
self.assertEqual(body["UserInfo"]["email"], USER().email)
@retry()
def test_authorization_consent_explicit(self):
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
sleep(1)
@ -229,6 +232,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
self.assertEqual(body["UserInfo"]["email"], USER().email)
@retry()
def test_authorization_denied(self):
"""test OpenID Provider flow (default authorization with access deny)"""
sleep(1)

View File

@ -11,7 +11,7 @@ from docker.models.containers import Container
from selenium.webdriver.common.by import By
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.core.models import Application
from passbook.flows.models import Flow
@ -57,6 +57,7 @@ class TestProviderProxy(SeleniumTestCase):
)
return container
@retry()
def test_proxy_simple(self):
"""Test simple outpost setup with single provider"""
proxy: ProxyProvider = ProxyProvider.objects.create(
@ -110,6 +111,7 @@ class TestProviderProxy(SeleniumTestCase):
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
"""Test Proxy connectivity over websockets"""
@retry()
def test_proxy_connectivity(self):
"""Test proxy connectivity over websocket"""
SeleniumTestCase().apply_default_data()

View File

@ -12,7 +12,7 @@ from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
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.crypto.models import CertificateKeyPair
from passbook.flows.models import Flow
@ -66,6 +66,7 @@ class TestProviderSAML(SeleniumTestCase):
LOGGER.info("Container failed healthcheck")
sleep(1)
@retry()
def test_sp_initiated_implicit(self):
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
@ -105,6 +106,7 @@ class TestProviderSAML(SeleniumTestCase):
self.assertEqual(body["attr"]["mail"], [USER().email])
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
@retry()
def test_sp_initiated_explicit(self):
"""test SAML Provider flow SP-initiated flow (explicit consent)"""
# Bootstrap all needed objects
@ -150,6 +152,7 @@ class TestProviderSAML(SeleniumTestCase):
self.assertEqual(body["attr"]["mail"], [USER().email])
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
@retry()
def test_idp_initiated_implicit(self):
"""test SAML Provider flow IdP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
@ -195,6 +198,7 @@ class TestProviderSAML(SeleniumTestCase):
self.assertEqual(body["attr"]["mail"], [USER().email])
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
@retry()
def test_sp_initiated_denied(self):
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
# Bootstrap all needed objects

View File

@ -14,7 +14,7 @@ from selenium.webdriver.support import expected_conditions as ec
from structlog import get_logger
from yaml import safe_dump
from e2e.utils import SeleniumTestCase
from e2e.utils import SeleniumTestCase, retry
from passbook.flows.models import Flow
from passbook.providers.oauth2.generators import (
generate_client_id,
@ -106,6 +106,7 @@ class TestSourceOAuth2(SeleniumTestCase):
consumer_secret=self.client_secret,
)
@retry()
def test_oauth_enroll(self):
"""test OAuth Source With With OIDC"""
self.create_objects()
@ -159,6 +160,7 @@ class TestSourceOAuth2(SeleniumTestCase):
"admin@example.com",
)
@retry()
@override_settings(SESSION_COOKIE_SAMESITE="strict")
def test_oauth_samesite_strict(self):
"""test OAuth Source With SameSite set to strict
@ -195,6 +197,7 @@ class TestSourceOAuth2(SeleniumTestCase):
"Authentication Failed.",
)
@retry()
def test_oauth_enroll_auth(self):
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
self.test_oauth_enroll()
@ -291,6 +294,7 @@ class TestSourceOAuth1(SeleniumTestCase):
consumer_secret=self.client_secret,
)
@retry()
def test_oauth_enroll(self):
"""test OAuth Source With With OIDC"""
self.create_objects()

View File

@ -10,7 +10,7 @@ from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
from structlog import get_logger
from e2e.utils import SeleniumTestCase
from e2e.utils import SeleniumTestCase, retry
from passbook.crypto.models import CertificateKeyPair
from passbook.flows.models import Flow
from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource
@ -92,6 +92,7 @@ class TestSourceSAML(SeleniumTestCase):
},
}
@retry()
def test_idp_redirect(self):
"""test SAML Source With redirect binding"""
# Bootstrap all needed objects
@ -141,6 +142,7 @@ class TestSourceSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
)
@retry()
def test_idp_post(self):
"""test SAML Source With post binding"""
# Bootstrap all needed objects
@ -192,6 +194,7 @@ class TestSourceSAML(SeleniumTestCase):
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
)
@retry()
def test_idp_post_auto(self):
"""test SAML Source With post binding (auto redirect)"""
# Bootstrap all needed objects

View File

@ -1,19 +1,22 @@
"""passbook e2e testing utilities"""
from functools import wraps
from glob import glob
from importlib.util import module_from_spec, spec_from_file_location
from inspect import getmembers, isfunction
from os import environ, makedirs
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.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.db import connection, transaction
from django.db.utils import IntegrityError
from django.shortcuts import reverse
from django.test.testcases import TransactionTestCase
from docker import DockerClient, from_env
from docker.models.containers import Container
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
@ -123,3 +126,41 @@ class SeleniumTestCase(StaticLiveServerTestCase):
func(apps, schema_editor)
except IntegrityError:
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

View File

@ -1,8 +1,8 @@
apiVersion: v2
appVersion: "0.12.3-stable"
appVersion: "0.12.5-stable"
description: A Helm chart for 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
dependencies:
- name: postgresql

View File

@ -4,7 +4,7 @@
image:
name: beryju/passbook
name_static: beryju/passbook-static
tag: 0.12.3-stable
tag: 0.12.5-stable
nameOverride: ""

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = "0.12.3-stable"
__version__ = "0.12.5-stable"

View File

@ -53,7 +53,7 @@
{{ user.username }}
</a>
</div>
<img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
<img class="pf-c-avatar" src="{% avatar user %}" alt="">
</div>
</header>
{% block page_content %}

View File

@ -7,7 +7,7 @@
<div class="pf-c-form__group">
<div class="form-control-static">
<div class="left">
<img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
<img class="pf-c-avatar" src="{% avatar user %}" alt="">
{{ user.username }}
</div>
<div class="right">

View File

@ -27,7 +27,15 @@ class FlowStageBindingSerializer(ModelSerializer):
class Meta:
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):

View File

@ -50,12 +50,10 @@ class FlowStageBindingForm(forms.ModelForm):
fields = [
"target",
"stage",
"evaluate_on_plan",
"re_evaluate_policies",
"order",
]
labels = {
"re_evaluate_policies": _("Re-evaluate Policies"),
}
widgets = {
"name": forms.TextInput(),
}

View File

@ -2,6 +2,7 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional
from django.http.request import HttpRequest
from structlog import get_logger
from passbook.core.models import User
@ -20,7 +21,9 @@ class StageMarker:
"""Base stage marker class, no extra attributes, and has no special handler."""
# 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.
If a stage should be removed, return None."""
return stage
@ -33,10 +36,14 @@ class ReevaluateMarker(StageMarker):
binding: PolicyBinding
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"""
engine = PolicyEngine(self.binding, self.user)
engine.use_cache = False
if http_request:
engine.request.http_request = http_request
engine.request.context = plan.context
engine.build()
result = engine.result

View File

@ -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.",
),
),
]

View File

@ -154,15 +154,19 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
target = models.ForeignKey("Flow", on_delete=models.CASCADE)
stage = InheritanceForeignKey(Stage, on_delete=models.CASCADE)
re_evaluate_policies = models.BooleanField(
default=False,
evaluate_on_plan = models.BooleanField(
default=True,
help_text=_(
(
"When this option is enabled, the planner will re-evaluate "
"policies bound to this binding."
"Evaluate policies during the Flow planning process. "
"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()

View File

@ -46,7 +46,7 @@ class FlowPlan:
self.stages.append(stage)
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"""
if not self.has_stages:
return None
@ -55,7 +55,7 @@ class FlowPlan:
if marker.__class__ is not StageMarker:
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:
LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage)
self.stages.remove(stage)
@ -63,7 +63,7 @@ class FlowPlan:
if not self.has_stages:
return None
# pylint: disable=not-callable
return self.next()
return self.next(http_request)
return marked_stage
def pop(self):
@ -159,23 +159,41 @@ class FlowPlanner:
for binding in FlowStageBinding.objects.filter(
target__pk=self.flow.pk
).order_by("order"):
engine = PolicyEngine(binding, user, request)
engine.request.context = plan.context
engine.build()
if engine.passing:
binding: FlowStageBinding
stage = binding.stage
marker = StageMarker()
if binding.evaluate_on_plan:
LOGGER.debug(
"f(plan): Stage passing", stage=binding.stage, flow=self.flow
"f(plan): evaluating on plan",
stage=binding.stage,
flow=self.flow,
)
plan.stages.append(binding.stage)
marker = StageMarker()
if binding.re_evaluate_policies:
engine = PolicyEngine(binding, user, request)
engine.request.context = plan.context
engine.build()
if engine.passing:
LOGGER.debug(
"f(plan): Stage has re-evaluate marker",
"f(plan): Stage passing",
stage=binding.stage,
flow=self.flow,
)
marker = ReevaluateMarker(binding=binding, user=user)
plan.markers.append(marker)
else:
stage = None
else:
LOGGER.debug(
"f(plan): not evaluating on plan",
stage=binding.stage,
flow=self.flow,
)
if binding.re_evaluate_policies and stage:
LOGGER.debug(
"f(plan): Stage has re-evaluate marker",
stage=binding.stage,
flow=self.flow,
)
marker = ReevaluateMarker(binding=binding, user=user)
if stage:
plan.append(stage, marker)
LOGGER.debug(
"f(plan): Finished building",
flow=self.flow,

View File

@ -86,7 +86,7 @@ class FlowExecutorView(View):
return to_stage_response(self.request, self.handle_invalid_flow(exc))
# We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet
next_stage = self.plan.next()
next_stage = self.plan.next(self.request)
if not next_stage:
LOGGER.debug("f(exec): no more stages, flow is done.")
return self._flow_done()

View File

@ -22,6 +22,7 @@ error_reporting:
send_pii: false
passbook:
avatars: gravatar # gravatar or none
branding:
title: passbook
title_show: true

View File

@ -6,15 +6,19 @@ from django import template
from django.db.models import Model
from django.http.request import HttpRequest
from django.template import Context
from django.templatetags.static import static
from django.utils.html import escape, mark_safe
from structlog import get_logger
from passbook.core.models import User
from passbook.lib.config import CONFIG
from passbook.lib.utils.urls import is_url_absolute
register = template.Library()
LOGGER = get_logger()
GRAVATAR_URL = "https://secure.gravatar.com"
@register.simple_tag(takes_context=True)
def back(context: Context) -> str:
@ -54,37 +58,23 @@ def css_class(field, css):
@register.simple_tag
def gravatar(email, size=None, rating=None):
"""
Generates a Gravatar URL for the given email address.
Syntax::
{% 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 = [
p
for p in (
("s", size or "158"),
("r", rating or "g"),
def avatar(user: User) -> str:
"""Get avatar, depending on passbook.avatar setting"""
mode = CONFIG.raw.get("passbook").get("avatars")
if mode == "none":
return static("passbook/user-default.png")
if mode == "gravatar":
parameters = [
("s", "158"),
("r", "g"),
]
# gravatar uses md5 for their URLs, so md5 can't be avoided
mail_hash = md5(user.email.encode("utf-8")).hexdigest() # nosec
gravatar_url = (
f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
)
if p[1]
]
if parameters:
gravatar_url += "?" + urlencode(parameters, doseq=True)
return escape(gravatar_url)
return escape(gravatar_url)
raise ValueError(f"Invalid avatar mode {mode}")
@register.filter

View File

@ -24,7 +24,10 @@ class DockerController(BaseController):
def __init__(self, outpost: Outpost) -> None:
super().__init__(outpost)
self.client = from_env()
try:
self.client = from_env()
except DockerException as exc:
raise ControllerException from exc
def _get_labels(self) -> Dict[str, str]:
return {}

View File

@ -24,6 +24,7 @@
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">PASSBOOK_TOKEN</span>
</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="" />
</div>
<h3>{% trans 'If your passbook Instance is using a self-signed certificate, set this value.' %}</h3>

View File

@ -1,5 +1,5 @@
"""passbook expression policy evaluator"""
from ipaddress import ip_address
from ipaddress import ip_address, ip_network
from typing import List
from django.http import HttpRequest
@ -22,6 +22,8 @@ class PolicyEvaluator(BaseEvaluator):
super().__init__()
self._messages = []
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"
def expr_func_message(self, message: str):

View File

@ -1,6 +1,8 @@
"""Integrate ./manage.py test with pytest"""
from django.conf import settings
from passbook.lib.config import CONFIG
class PytestTestRunner:
"""Runs pytest to discover and run tests."""
@ -11,6 +13,7 @@ class PytestTestRunner:
self.keepdb = keepdb
settings.TEST = True
settings.CELERY_TASK_ALWAYS_EAGER = True
CONFIG.raw.get("passbook")["avatars"] = "none"
def run_tests(self, test_labels):
"""Run pytest and return the exitcode.

View File

@ -1,7 +1,6 @@
"""passbook password stage"""
from typing import Any, Dict, List, Optional
from django.contrib import messages
from django.contrib.auth import _clean_credentials
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.signals import user_login_failed
@ -122,5 +121,4 @@ class PasswordStageView(FormView, StageView):
self.executor.plan.context[
PLAN_CONTEXT_AUTHENTICATION_BACKEND
] = user.backend
messages.success(self.request, _("Successfully logged in!"))
return self.executor.stage_ok()

View File

@ -39,4 +39,5 @@ class UserLoginStageView(StageView):
flow_slug=self.executor.flow.slug,
session_duration=self.executor.current_stage.session_duration,
)
messages.success(self.request, _("Successfully logged in!"))
return self.executor.stage_ok()

View File

@ -442,9 +442,9 @@
}
},
"rollup": {
"version": "2.32.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.32.0.tgz",
"integrity": "sha512-0FIG1jY88uhCP2yP4CfvtKEqPDRmsUwfY1kEOOM+DH/KOGATgaIFd/is1+fQOxsvh62ELzcFfKonwKWnHhrqmw==",
"version": "2.32.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.32.1.tgz",
"integrity": "sha512-Op2vWTpvK7t6/Qnm1TTh7VjEZZkN8RWgf0DHbkKzQBwNf748YhXbozHVefqpPp/Fuyk/PQPAnYsBxAEtlMvpUw==",
"requires": {
"fsevents": "~2.1.2"
}

View File

@ -11,7 +11,7 @@
"codemirror": "^5.58.1",
"lit-element": "^2.4.0",
"lit-html": "^1.3.0",
"rollup": "^2.32.0"
"rollup": "^2.32.1"
},
"devDependencies": {
"rollup-plugin-commonjs": "^10.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -1,3 +1,3 @@
package pkg
const VERSION = "0.12.3-stable"
const VERSION = "0.12.5-stable"

View File

@ -833,6 +833,11 @@ paths:
description: ''
required: false
type: string
- name: evaluate_on_plan
in: query
description: ''
required: false
type: string
- name: re_evaluate_policies
in: query
description: ''
@ -6337,10 +6342,14 @@ definitions:
title: Stage
type: string
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:
title: Re evaluate policies
description: When this option is enabled, the planner will re-evaluate policies
bound to this binding.
description: Evaluate policies when the Stage is present to the user.
type: boolean
order:
title: Order