Compare commits
11 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| 566ebae065 | |||
| 9b62a6403b | |||
| 8c465b2026 | |||
| 6b7da71aa8 | |||
| e95bbfab9a | |||
| e401575894 | |||
| 6428801270 | |||
| 3e13c13619 | |||
| 92f79eb30e | |||
| e7472de4bf | |||
| 494950ac65 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.10.1-stable
|
current_version = 0.10.2-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.10.1-stable
|
-t beryju/passbook:0.10.2-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.10.1-stable
|
run: docker push beryju/passbook:0.10.2-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.10.1-stable \
|
-t beryju/passbook-proxy:0.10.2-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.10.1-stable
|
run: docker push beryju/passbook-proxy:0.10.2-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.10.1-stable
|
-t beryju/passbook-static:0.10.2-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.10.1-stable
|
run: docker push beryju/passbook-static:0.10.2-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.10.1-stable
|
tagName: 0.10.2-stable
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
|
|||||||
@ -20,7 +20,7 @@ wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
|||||||
# Optionally enable Error-reporting
|
# Optionally enable Error-reporting
|
||||||
# export PASSBOOK_ERROR_REPORTING=true
|
# export PASSBOOK_ERROR_REPORTING=true
|
||||||
# Optionally deploy a different version
|
# Optionally deploy a different version
|
||||||
# export PASSBOOK_TAG=0.10.1-stable
|
# export PASSBOOK_TAG=0.10.2-stable
|
||||||
# If this is a productive installation, set a different PostgreSQL Password
|
# If this is a productive installation, set a different PostgreSQL Password
|
||||||
# export PG_PASS=$(pwgen 40 1)
|
# export PG_PASS=$(pwgen 40 1)
|
||||||
docker-compose pull
|
docker-compose pull
|
||||||
|
|||||||
@ -23,7 +23,7 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- traefik.enable=false
|
- traefik.enable=false
|
||||||
server:
|
server:
|
||||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.1-stable}
|
image: beryju/passbook:${PASSBOOK_TAG:-0.10.2-stable}
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
PASSBOOK_REDIS__HOST: redis
|
PASSBOOK_REDIS__HOST: redis
|
||||||
@ -41,7 +41,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
worker:
|
worker:
|
||||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.1-stable}
|
image: beryju/passbook:${PASSBOOK_TAG:-0.10.2-stable}
|
||||||
command: worker
|
command: worker
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
@ -55,7 +55,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
static:
|
static:
|
||||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.1-stable}
|
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.2-stable}
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@ -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=true >> .env`
|
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING=true >> .env`
|
||||||
|
|
||||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.1-stable >> .env`
|
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.2-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.10.1-stable
|
tag: 0.10.2-stable
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from unittest.case import skipUnless
|
|||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
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 selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
@ -214,7 +215,10 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||||
"Permission denied",
|
"Permission denied",
|
||||||
|
|||||||
@ -285,7 +285,10 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||||
"Permission denied",
|
"Permission denied",
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from docker.models.containers import Container
|
|||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
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 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
|
||||||
@ -206,7 +207,10 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||||
"Permission denied",
|
"Permission denied",
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: "0.10.1-stable"
|
appVersion: "0.10.2-stable"
|
||||||
description: A Helm chart for passbook.
|
description: A Helm chart for passbook.
|
||||||
name: passbook
|
name: passbook
|
||||||
version: "0.10.1-stable"
|
version: "0.10.2-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
|
||||||
|
|||||||
@ -25,21 +25,23 @@ spec:
|
|||||||
affinity:
|
affinity:
|
||||||
podAntiAffinity:
|
podAntiAffinity:
|
||||||
preferredDuringSchedulingIgnoredDuringExecution:
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
- labelSelector:
|
- weight: 1
|
||||||
matchExpressions:
|
podAffinityTerm:
|
||||||
- key: app.kubernetes.io/name
|
labelSelector:
|
||||||
operator: In
|
matchExpressions:
|
||||||
values:
|
- key: app.kubernetes.io/name
|
||||||
- {{ include "passbook.name" . }}
|
operator: In
|
||||||
- key: app.kubernetes.io/instance
|
values:
|
||||||
operator: In
|
- {{ include "passbook.name" . }}
|
||||||
values:
|
- key: app.kubernetes.io/instance
|
||||||
- {{ .Release.Name }}
|
operator: In
|
||||||
- key: k8s.passbook.beryju.org/component
|
values:
|
||||||
operator: In
|
- {{ .Release.Name }}
|
||||||
values:
|
- key: k8s.passbook.beryju.org/component
|
||||||
- web
|
operator: In
|
||||||
topologyKey: "kubernetes.io/hostname"
|
values:
|
||||||
|
- web
|
||||||
|
topologyKey: "kubernetes.io/hostname"
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: passbook-database-migrations
|
- name: passbook-database-migrations
|
||||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||||
@ -109,7 +111,7 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 100m
|
cpu: 100m
|
||||||
memory: 200M
|
memory: 300M
|
||||||
limits:
|
limits:
|
||||||
cpu: 300m
|
cpu: 300m
|
||||||
memory: 350M
|
memory: 500M
|
||||||
|
|||||||
@ -25,21 +25,23 @@ spec:
|
|||||||
affinity:
|
affinity:
|
||||||
podAntiAffinity:
|
podAntiAffinity:
|
||||||
preferredDuringSchedulingIgnoredDuringExecution:
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
- labelSelector:
|
- weight: 1
|
||||||
matchExpressions:
|
podAffinityTerm:
|
||||||
- key: app.kubernetes.io/name
|
labelSelector:
|
||||||
operator: In
|
matchExpressions:
|
||||||
values:
|
- key: app.kubernetes.io/name
|
||||||
- {{ include "passbook.name" . }}
|
operator: In
|
||||||
- key: app.kubernetes.io/instance
|
values:
|
||||||
operator: In
|
- {{ include "passbook.name" . }}
|
||||||
values:
|
- key: app.kubernetes.io/instance
|
||||||
- {{ .Release.Name }}
|
operator: In
|
||||||
- key: k8s.passbook.beryju.org/component
|
values:
|
||||||
operator: In
|
- {{ .Release.Name }}
|
||||||
values:
|
- key: k8s.passbook.beryju.org/component
|
||||||
- worker
|
operator: In
|
||||||
topologyKey: "kubernetes.io/hostname"
|
values:
|
||||||
|
- worker
|
||||||
|
topologyKey: "kubernetes.io/hostname"
|
||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}
|
- name: {{ .Chart.Name }}
|
||||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||||
@ -68,7 +70,7 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 150m
|
cpu: 150m
|
||||||
memory: 300M
|
memory: 400M
|
||||||
limits:
|
limits:
|
||||||
cpu: 300m
|
cpu: 300m
|
||||||
memory: 500M
|
memory: 600M
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
image:
|
image:
|
||||||
name: beryju/passbook
|
name: beryju/passbook
|
||||||
name_static: beryju/passbook-static
|
name_static: beryju/passbook-static
|
||||||
tag: 0.10.1-stable
|
tag: 0.10.2-stable
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap",
|
|||||||
if [[ "$1" == "server" ]]; then
|
if [[ "$1" == "server" ]]; then
|
||||||
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
|
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
|
||||||
elif [[ "$1" == "worker" ]]; then
|
elif [[ "$1" == "worker" ]]; then
|
||||||
celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule
|
celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule -Q passbook,passbook_scheduled
|
||||||
elif [[ "$1" == "migrate" ]]; then
|
elif [[ "$1" == "migrate" ]]; then
|
||||||
# Run system migrations first, run normal migrations after
|
# Run system migrations first, run normal migrations after
|
||||||
python -m lifecycle.migrate
|
python -m lifecycle.migrate
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
"""passbook"""
|
"""passbook"""
|
||||||
__version__ = "0.10.1-stable"
|
__version__ = "0.10.2-stable"
|
||||||
|
|||||||
@ -45,7 +45,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs["application_count"] = len(Application.objects.all())
|
kwargs["application_count"] = len(Application.objects.all())
|
||||||
kwargs["policy_count"] = len(Policy.objects.all())
|
kwargs["policy_count"] = len(Policy.objects.all())
|
||||||
kwargs["user_count"] = len(User.objects.all())
|
kwargs["user_count"] = len(User.objects.all()) - 1 # Remove anonymous user
|
||||||
kwargs["provider_count"] = len(Provider.objects.all())
|
kwargs["provider_count"] = len(Provider.objects.all())
|
||||||
kwargs["source_count"] = len(Source.objects.all())
|
kwargs["source_count"] = len(Source.objects.all())
|
||||||
kwargs["stage_count"] = len(Stage.objects.all())
|
kwargs["stage_count"] = len(Stage.objects.all())
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
"""flow views tests"""
|
"""flow views tests"""
|
||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
|
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
from passbook.flows.markers import ReevaluateMarker, StageMarker
|
from passbook.flows.markers import ReevaluateMarker, StageMarker
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from passbook.flows.planner import FlowPlan
|
from passbook.flows.planner import FlowPlan
|
||||||
from passbook.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
|
from passbook.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
from passbook.policies.dummy.models import DummyPolicy
|
from passbook.policies.dummy.models import DummyPolicy
|
||||||
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
from passbook.policies.models import PolicyBinding
|
from passbook.policies.models import PolicyBinding
|
||||||
from passbook.policies.types import PolicyResult
|
from passbook.policies.types import PolicyResult
|
||||||
from passbook.stages.dummy.models import DummyStage
|
from passbook.stages.dummy.models import DummyStage
|
||||||
@ -19,6 +22,15 @@ POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False))
|
|||||||
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
||||||
|
|
||||||
|
|
||||||
|
def to_stage_response(request: HttpRequest, source: HttpResponse):
|
||||||
|
"""Mock for to_stage_response that returns the original response, so we can check
|
||||||
|
inheritance and member attributes"""
|
||||||
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response)
|
||||||
|
|
||||||
|
|
||||||
class TestFlowExecutor(TestCase):
|
class TestFlowExecutor(TestCase):
|
||||||
"""Test views logic"""
|
"""Test views logic"""
|
||||||
|
|
||||||
@ -50,6 +62,9 @@ class TestFlowExecutor(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(cancel_mock.call_count, 2)
|
self.assertEqual(cancel_mock.call_count, 2)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
@patch(
|
@patch(
|
||||||
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
|
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
|
||||||
)
|
)
|
||||||
@ -66,11 +81,12 @@ class TestFlowExecutor(TestCase):
|
|||||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
force_str(response.content),
|
self.assertInHTML(FlowNonApplicableException.__doc__, response.rendered_content)
|
||||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
def test_invalid_empty_flow(self):
|
def test_invalid_empty_flow(self):
|
||||||
"""Tests that an empty flow returns the correct error message"""
|
"""Tests that an empty flow returns the correct error message"""
|
||||||
flow = Flow.objects.create(
|
flow = Flow.objects.create(
|
||||||
@ -84,10 +100,8 @@ class TestFlowExecutor(TestCase):
|
|||||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
force_str(response.content),
|
self.assertInHTML(EmptyFlowException.__doc__, response.rendered_content)
|
||||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_invalid_flow_redirect(self):
|
def test_invalid_flow_redirect(self):
|
||||||
"""Tests that an invalid flow still redirects"""
|
"""Tests that an invalid flow still redirects"""
|
||||||
@ -101,8 +115,10 @@ class TestFlowExecutor(TestCase):
|
|||||||
dest = "/unique-string"
|
dest = "/unique-string"
|
||||||
url = reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug})
|
url = reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug})
|
||||||
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
|
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.url, dest)
|
self.assertJSONEqual(
|
||||||
|
force_str(response.content), {"type": "redirect", "to": dest},
|
||||||
|
)
|
||||||
|
|
||||||
def test_multi_stage_flow(self):
|
def test_multi_stage_flow(self):
|
||||||
"""Test a full flow with multiple stages"""
|
"""Test a full flow with multiple stages"""
|
||||||
|
|||||||
@ -6,12 +6,10 @@ from passbook.flows.views import (
|
|||||||
CancelView,
|
CancelView,
|
||||||
FlowExecutorShellView,
|
FlowExecutorShellView,
|
||||||
FlowExecutorView,
|
FlowExecutorView,
|
||||||
FlowPermissionDeniedView,
|
|
||||||
ToDefaultFlow,
|
ToDefaultFlow,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("-/denied/", FlowPermissionDeniedView.as_view(), name="denied"),
|
|
||||||
path(
|
path(
|
||||||
"-/default/authentication/",
|
"-/default/authentication/",
|
||||||
ToDefaultFlow.as_view(designation=FlowDesignation.AUTHENTICATION),
|
ToDefaultFlow.as_view(designation=FlowDesignation.AUTHENTICATION),
|
||||||
|
|||||||
@ -9,10 +9,9 @@ from django.http import (
|
|||||||
HttpResponseRedirect,
|
HttpResponseRedirect,
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
)
|
)
|
||||||
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||||
from django.views.generic import TemplateView, View
|
from django.views.generic import TemplateView, View
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
@ -24,6 +23,7 @@ from passbook.flows.models import Flow, FlowDesignation, Stage
|
|||||||
from passbook.flows.planner import FlowPlan, FlowPlanner
|
from passbook.flows.planner import FlowPlan, FlowPlanner
|
||||||
from passbook.lib.utils.reflection import class_to_path
|
from passbook.lib.utils.reflection import class_to_path
|
||||||
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
|
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||||
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
# Argument used to redirect user after login
|
# Argument used to redirect user after login
|
||||||
@ -31,8 +31,6 @@ NEXT_ARG_NAME = "next"
|
|||||||
SESSION_KEY_PLAN = "passbook_flows_plan"
|
SESSION_KEY_PLAN = "passbook_flows_plan"
|
||||||
SESSION_KEY_APPLICATION_PRE = "passbook_flows_application_pre"
|
SESSION_KEY_APPLICATION_PRE = "passbook_flows_application_pre"
|
||||||
SESSION_KEY_GET = "passbook_flows_get"
|
SESSION_KEY_GET = "passbook_flows_get"
|
||||||
SESSION_KEY_DENIED_ERROR = "passbook_flows_denied_error"
|
|
||||||
SESSION_KEY_DENIED_POLICY_RESULT = "passbook_flows_denied_policy_result"
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(xframe_options_sameorigin, name="dispatch")
|
@method_decorator(xframe_options_sameorigin, name="dispatch")
|
||||||
@ -56,9 +54,7 @@ class FlowExecutorView(View):
|
|||||||
LOGGER.debug("f(exec): Redirecting to next on fail")
|
LOGGER.debug("f(exec): Redirecting to next on fail")
|
||||||
return redirect(self.request.GET.get(NEXT_ARG_NAME))
|
return redirect(self.request.GET.get(NEXT_ARG_NAME))
|
||||||
message = exc.__doc__ if exc.__doc__ else str(exc)
|
message = exc.__doc__ if exc.__doc__ else str(exc)
|
||||||
return to_stage_response(
|
return self.stage_invalid(error_message=message)
|
||||||
self.request, self.stage_invalid(error_message=message)
|
|
||||||
)
|
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
||||||
# Early check if theres an active Plan for the current session
|
# Early check if theres an active Plan for the current session
|
||||||
@ -83,10 +79,10 @@ class FlowExecutorView(View):
|
|||||||
self.plan = self._initiate_plan()
|
self.plan = self._initiate_plan()
|
||||||
except FlowNonApplicableException as exc:
|
except FlowNonApplicableException as exc:
|
||||||
LOGGER.warning("f(exec): Flow not applicable to current user", exc=exc)
|
LOGGER.warning("f(exec): Flow not applicable to current user", exc=exc)
|
||||||
return self.handle_invalid_flow(exc)
|
return to_stage_response(self.request, self.handle_invalid_flow(exc))
|
||||||
except EmptyFlowException as exc:
|
except EmptyFlowException as exc:
|
||||||
LOGGER.warning("f(exec): Flow is empty", exc=exc)
|
LOGGER.warning("f(exec): Flow is empty", exc=exc)
|
||||||
return 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()
|
||||||
@ -119,14 +115,7 @@ class FlowExecutorView(View):
|
|||||||
return to_stage_response(request, stage_response)
|
return to_stage_response(request, stage_response)
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
LOGGER.exception(exc)
|
LOGGER.exception(exc)
|
||||||
return to_stage_response(
|
return to_stage_response(request, FlowErrorResponse(request, exc))
|
||||||
request,
|
|
||||||
render(
|
|
||||||
request,
|
|
||||||
"flows/error.html",
|
|
||||||
{"error": exc, "tb": "".join(format_tb(exc.__traceback__))},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
"""pass post request to current stage"""
|
"""pass post request to current stage"""
|
||||||
@ -141,14 +130,7 @@ class FlowExecutorView(View):
|
|||||||
return to_stage_response(request, stage_response)
|
return to_stage_response(request, stage_response)
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
LOGGER.exception(exc)
|
LOGGER.exception(exc)
|
||||||
return to_stage_response(
|
return to_stage_response(request, FlowErrorResponse(request, exc))
|
||||||
request,
|
|
||||||
render(
|
|
||||||
request,
|
|
||||||
"flows/error.html",
|
|
||||||
{"error": exc, "tb": "".join(format_tb(exc.__traceback__))},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _initiate_plan(self) -> FlowPlan:
|
def _initiate_plan(self) -> FlowPlan:
|
||||||
planner = FlowPlanner(self.flow)
|
planner = FlowPlanner(self.flow)
|
||||||
@ -205,12 +187,9 @@ class FlowExecutorView(View):
|
|||||||
is a superuser."""
|
is a superuser."""
|
||||||
LOGGER.debug("f(exec): Stage invalid", flow_slug=self.flow.slug)
|
LOGGER.debug("f(exec): Stage invalid", flow_slug=self.flow.slug)
|
||||||
self.cancel()
|
self.cancel()
|
||||||
if self.request.user and self.request.user.is_authenticated:
|
response = AccessDeniedResponse(self.request)
|
||||||
if self.request.user.is_superuser or self.request.user.attributes.get(
|
response.error_message = error_message
|
||||||
PASSBOOK_USER_DEBUG, False
|
return response
|
||||||
):
|
|
||||||
self.request.session[SESSION_KEY_DENIED_ERROR] = error_message
|
|
||||||
return redirect_with_qs("passbook_flows:denied", self.request.GET)
|
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
"""Cancel current execution and return a redirect"""
|
"""Cancel current execution and return a redirect"""
|
||||||
@ -224,21 +203,30 @@ class FlowExecutorView(View):
|
|||||||
del self.request.session[key]
|
del self.request.session[key]
|
||||||
|
|
||||||
|
|
||||||
class FlowPermissionDeniedView(TemplateView):
|
class FlowErrorResponse(TemplateResponse):
|
||||||
"""User could not be authenticated"""
|
"""Response class when an unhandled error occurs during a stage. Normal users
|
||||||
|
are shown an error message, superusers are shown a full stacktrace."""
|
||||||
|
|
||||||
template_name = "flows/denied.html"
|
error: Exception
|
||||||
title = _("Permission denied.")
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def __init__(self, request: HttpRequest, error: Exception) -> None:
|
||||||
kwargs["title"] = self.title
|
# For some reason pyright complains about keyword argument usage here
|
||||||
if SESSION_KEY_DENIED_ERROR in self.request.session:
|
# pyright: reportGeneralTypeIssues=false
|
||||||
kwargs["error"] = self.request.session[SESSION_KEY_DENIED_ERROR]
|
super().__init__(request=request, template="flows/error.html")
|
||||||
if SESSION_KEY_DENIED_POLICY_RESULT in self.request.session:
|
self.error = error
|
||||||
kwargs["policy_result"] = self.request.session[
|
|
||||||
SESSION_KEY_DENIED_POLICY_RESULT
|
def resolve_context(
|
||||||
]
|
self, context: Optional[Dict[str, Any]]
|
||||||
return super().get_context_data(**kwargs)
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
if not context:
|
||||||
|
context = {}
|
||||||
|
context["error"] = self.error
|
||||||
|
if self._request.user and self._request.user.is_authenticated:
|
||||||
|
if self._request.user.is_superuser or self._request.user.attributes.get(
|
||||||
|
PASSBOOK_USER_DEBUG, False
|
||||||
|
):
|
||||||
|
context["tb"] = "".join(format_tb(self.error.__traceback__))
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class FlowExecutorShellView(TemplateView):
|
class FlowExecutorShellView(TemplateView):
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"""passbook sentry integration"""
|
"""passbook sentry integration"""
|
||||||
from billiard.exceptions import WorkerLostError
|
from billiard.exceptions import WorkerLostError
|
||||||
from botocore.client import ClientError
|
from botocore.client import ClientError
|
||||||
|
from celery.exceptions import CeleryError
|
||||||
from django.core.exceptions import DisallowedHost, ValidationError
|
from django.core.exceptions import DisallowedHost, ValidationError
|
||||||
from django.db import InternalError, OperationalError, ProgrammingError
|
from django.db import InternalError, OperationalError, ProgrammingError
|
||||||
from django_redis.exceptions import ConnectionInterrupted
|
from django_redis.exceptions import ConnectionInterrupted
|
||||||
@ -8,6 +9,7 @@ from redis.exceptions import ConnectionError as RedisConnectionError
|
|||||||
from redis.exceptions import RedisError
|
from redis.exceptions import RedisError
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
from websockets.exceptions import WebSocketException
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -35,6 +37,8 @@ def before_send(event, hint):
|
|||||||
OSError,
|
OSError,
|
||||||
RedisError,
|
RedisError,
|
||||||
SentryIgnoredException,
|
SentryIgnoredException,
|
||||||
|
WebSocketException,
|
||||||
|
CeleryError,
|
||||||
)
|
)
|
||||||
if "exc_info" in hint:
|
if "exc_info" in hint:
|
||||||
_, exc_value, _ = hint["exc_info"]
|
_, exc_value, _ = hint["exc_info"]
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
"""Outposts Settings"""
|
"""Outposts Settings"""
|
||||||
from celery.schedules import crontab
|
# from celery.schedules import crontab
|
||||||
|
|
||||||
CELERY_BEAT_SCHEDULE = {
|
# CELERY_BEAT_SCHEDULE = {
|
||||||
"outposts_k8s": {
|
# "outposts_k8s": {
|
||||||
"task": "passbook.outposts.tasks.outpost_k8s_controller",
|
# "task": "passbook.outposts.tasks.outpost_k8s_controller",
|
||||||
"schedule": crontab(minute="*/5"), # Run every 5 minutes
|
# "schedule": crontab(minute="*/5"), # Run every 5 minutes
|
||||||
"options": {"queue": "passbook_scheduled"},
|
# "options": {"queue": "passbook_scheduled"},
|
||||||
}
|
# }
|
||||||
}
|
# }
|
||||||
|
|||||||
44
passbook/policies/http.py
Normal file
44
passbook/policies/http.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""policy http response"""
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from passbook.core.models import PASSBOOK_USER_DEBUG
|
||||||
|
from passbook.policies.types import PolicyResult
|
||||||
|
|
||||||
|
|
||||||
|
class AccessDeniedResponse(TemplateResponse):
|
||||||
|
"""Response used for access denied messages. Can optionally show an error message,
|
||||||
|
and if the user is a superuser or has user_debug enabled, shows a policy result."""
|
||||||
|
|
||||||
|
title: str
|
||||||
|
|
||||||
|
error_message: Optional[str] = None
|
||||||
|
policy_result: Optional[PolicyResult] = None
|
||||||
|
|
||||||
|
def __init__(self, request: HttpRequest) -> None:
|
||||||
|
# For some reason pyright complains about keyword argument usage here
|
||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
super().__init__(request=request, template="policies/denied.html")
|
||||||
|
self.title = _("Access denied")
|
||||||
|
|
||||||
|
def resolve_context(
|
||||||
|
self, context: Optional[Dict[str, Any]]
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
if not context:
|
||||||
|
context = {}
|
||||||
|
context["title"] = self.title
|
||||||
|
if self.error_message:
|
||||||
|
context["error"] = self.error_message
|
||||||
|
# Only show policy result if user is authenticated and
|
||||||
|
# either superuser or has PASSBOOK_USER_DEBUG set
|
||||||
|
if self.policy_result:
|
||||||
|
if self._request.user and self._request.user.is_authenticated:
|
||||||
|
if (
|
||||||
|
self._request.user.is_superuser
|
||||||
|
or self._request.user.attributes.get(PASSBOOK_USER_DEBUG, False)
|
||||||
|
):
|
||||||
|
context["policy_result"] = self.policy_result
|
||||||
|
return context
|
||||||
@ -5,16 +5,13 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.mixins import AccessMixin
|
from django.contrib.auth.mixins import AccessMixin
|
||||||
from django.contrib.auth.views import redirect_to_login
|
from django.contrib.auth.views import redirect_to_login
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Application, Provider, User
|
from passbook.core.models import Application, Provider, User
|
||||||
from passbook.flows.views import (
|
from passbook.flows.views import SESSION_KEY_APPLICATION_PRE
|
||||||
SESSION_KEY_APPLICATION_PRE,
|
|
||||||
SESSION_KEY_DENIED_POLICY_RESULT,
|
|
||||||
)
|
|
||||||
from passbook.policies.engine import PolicyEngine
|
from passbook.policies.engine import PolicyEngine
|
||||||
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
from passbook.policies.types import PolicyResult
|
from passbook.policies.types import PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -31,6 +28,9 @@ class PolicyAccessMixin(BaseMixin, AccessMixin):
|
|||||||
Provider functions to check application access, etc"""
|
Provider functions to check application access, etc"""
|
||||||
|
|
||||||
def handle_no_permission(self, application: Optional[Application] = None):
|
def handle_no_permission(self, application: Optional[Application] = None):
|
||||||
|
"""User has no access and is not authenticated, so we remember the application
|
||||||
|
they try to access and redirect to the login URL. The application is saved to show
|
||||||
|
a hint on the Identification Stage what the user should login for."""
|
||||||
if application:
|
if application:
|
||||||
self.request.session[SESSION_KEY_APPLICATION_PRE] = application
|
self.request.session[SESSION_KEY_APPLICATION_PRE] = application
|
||||||
return redirect_to_login(
|
return redirect_to_login(
|
||||||
@ -43,10 +43,10 @@ class PolicyAccessMixin(BaseMixin, AccessMixin):
|
|||||||
self, result: Optional[PolicyResult] = None
|
self, result: Optional[PolicyResult] = None
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
"""Function called when user has no permissions but is authenticated"""
|
"""Function called when user has no permissions but is authenticated"""
|
||||||
|
response = AccessDeniedResponse(self.request)
|
||||||
if result:
|
if result:
|
||||||
self.request.session[SESSION_KEY_DENIED_POLICY_RESULT] = result
|
response.policy_result = result
|
||||||
# TODO: Remove this URL and render the view instead
|
return response
|
||||||
return redirect("passbook_flows:denied")
|
|
||||||
|
|
||||||
def provider_to_application(self, provider: Provider) -> Application:
|
def provider_to_application(self, provider: Provider) -> Application:
|
||||||
"""Lookup application assigned to provider, throw error if no application assigned"""
|
"""Lookup application assigned to provider, throw error if no application assigned"""
|
||||||
|
|||||||
14
passbook/sources/ldap/signals.py
Normal file
14
passbook/sources/ldap/signals.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""passbook ldap source signals"""
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from passbook.sources.ldap.models import LDAPSource
|
||||||
|
from passbook.sources.ldap.tasks import sync_single
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=LDAPSource)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
|
||||||
|
"""Ensure that source is synced on save (if enabled)"""
|
||||||
|
if instance.enabled:
|
||||||
|
sync_single.delay(instance.pk)
|
||||||
@ -8,7 +8,14 @@ from passbook.sources.ldap.models import LDAPSource
|
|||||||
def sync():
|
def sync():
|
||||||
"""Sync all sources"""
|
"""Sync all sources"""
|
||||||
for source in LDAPSource.objects.filter(enabled=True):
|
for source in LDAPSource.objects.filter(enabled=True):
|
||||||
connector = Connector(source)
|
sync_single.delay(source.pk)
|
||||||
connector.sync_users()
|
|
||||||
connector.sync_groups()
|
|
||||||
connector.sync_membership()
|
@CELERY_APP.task()
|
||||||
|
def sync_single(source_pk):
|
||||||
|
"""Sync a single source"""
|
||||||
|
source = LDAPSource.objects.get(pk=source_pk)
|
||||||
|
connector = Connector(source)
|
||||||
|
connector.sync_users()
|
||||||
|
connector.sync_groups()
|
||||||
|
connector.sync_membership()
|
||||||
|
|||||||
@ -10,7 +10,9 @@ from passbook.core.models import User
|
|||||||
from passbook.flows.markers import StageMarker
|
from passbook.flows.markers import StageMarker
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
|
from passbook.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||||
from passbook.flows.views import SESSION_KEY_PLAN
|
from passbook.flows.views import SESSION_KEY_PLAN
|
||||||
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
from passbook.stages.invitation.forms import InvitationStageForm
|
from passbook.stages.invitation.forms import InvitationStageForm
|
||||||
from passbook.stages.invitation.models import Invitation, InvitationStage
|
from passbook.stages.invitation.models import Invitation, InvitationStage
|
||||||
from passbook.stages.invitation.stage import INVITATION_TOKEN_KEY, PLAN_CONTEXT_PROMPT
|
from passbook.stages.invitation.stage import INVITATION_TOKEN_KEY, PLAN_CONTEXT_PROMPT
|
||||||
@ -38,6 +40,9 @@ class TestUserLoginStage(TestCase):
|
|||||||
data = {"name": "test"}
|
data = {"name": "test"}
|
||||||
self.assertEqual(InvitationStageForm(data).is_valid(), True)
|
self.assertEqual(InvitationStageForm(data).is_valid(), True)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
def test_without_invitation_fail(self):
|
def test_without_invitation_fail(self):
|
||||||
"""Test without any invitation, continue_flow_without_invitation not set."""
|
"""Test without any invitation, continue_flow_without_invitation not set."""
|
||||||
plan = FlowPlan(
|
plan = FlowPlan(
|
||||||
@ -56,12 +61,8 @@ class TestUserLoginStage(TestCase):
|
|||||||
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
force_str(response.content),
|
|
||||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_without_invitation_continue(self):
|
def test_without_invitation_continue(self):
|
||||||
"""Test without any invitation, continue_flow_without_invitation is set."""
|
"""Test without any invitation, continue_flow_without_invitation is set."""
|
||||||
|
|||||||
@ -12,7 +12,9 @@ from passbook.core.models import User
|
|||||||
from passbook.flows.markers import StageMarker
|
from passbook.flows.markers import StageMarker
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
|
from passbook.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||||
from passbook.flows.views import SESSION_KEY_PLAN
|
from passbook.flows.views import SESSION_KEY_PLAN
|
||||||
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
from passbook.stages.password.models import PasswordStage
|
from passbook.stages.password.models import PasswordStage
|
||||||
|
|
||||||
MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test"))
|
MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test"))
|
||||||
@ -42,6 +44,9 @@ class TestPasswordStage(TestCase):
|
|||||||
)
|
)
|
||||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
def test_without_user(self):
|
def test_without_user(self):
|
||||||
"""Test without user"""
|
"""Test without user"""
|
||||||
plan = FlowPlan(
|
plan = FlowPlan(
|
||||||
@ -60,10 +65,7 @@ class TestPasswordStage(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
force_str(response.content),
|
|
||||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_recovery_flow_link(self):
|
def test_recovery_flow_link(self):
|
||||||
"""Test link to the default recovery flow"""
|
"""Test link to the default recovery flow"""
|
||||||
@ -129,6 +131,9 @@ class TestPasswordStage(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
@patch(
|
@patch(
|
||||||
"django.contrib.auth.backends.ModelBackend.authenticate",
|
"django.contrib.auth.backends.ModelBackend.authenticate",
|
||||||
MOCK_BACKEND_AUTHENTICATE,
|
MOCK_BACKEND_AUTHENTICATE,
|
||||||
@ -153,7 +158,4 @@ class TestPasswordStage(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
force_str(response.content),
|
|
||||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
"""delete tests"""
|
"""delete tests"""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
@ -7,7 +9,9 @@ from passbook.core.models import User
|
|||||||
from passbook.flows.markers import StageMarker
|
from passbook.flows.markers import StageMarker
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
|
from passbook.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||||
from passbook.flows.views import SESSION_KEY_PLAN
|
from passbook.flows.views import SESSION_KEY_PLAN
|
||||||
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
from passbook.stages.user_delete.models import UserDeleteStage
|
from passbook.stages.user_delete.models import UserDeleteStage
|
||||||
|
|
||||||
|
|
||||||
@ -28,6 +32,9 @@ class TestUserDeleteStage(TestCase):
|
|||||||
self.stage = UserDeleteStage.objects.create(name="delete")
|
self.stage = UserDeleteStage.objects.create(name="delete")
|
||||||
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
def test_no_user(self):
|
def test_no_user(self):
|
||||||
"""Test without user set"""
|
"""Test without user set"""
|
||||||
plan = FlowPlan(
|
plan = FlowPlan(
|
||||||
@ -43,10 +50,7 @@ class TestUserDeleteStage(TestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
force_str(response.content),
|
|
||||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_user_delete_get(self):
|
def test_user_delete_get(self):
|
||||||
"""Test Form render"""
|
"""Test Form render"""
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
"""login tests"""
|
"""login tests"""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
@ -7,7 +9,9 @@ from passbook.core.models import User
|
|||||||
from passbook.flows.markers import StageMarker
|
from passbook.flows.markers import StageMarker
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
|
from passbook.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||||
from passbook.flows.views import SESSION_KEY_PLAN
|
from passbook.flows.views import SESSION_KEY_PLAN
|
||||||
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
from passbook.stages.user_login.forms import UserLoginStageForm
|
from passbook.stages.user_login.forms import UserLoginStageForm
|
||||||
from passbook.stages.user_login.models import UserLoginStage
|
from passbook.stages.user_login.models import UserLoginStage
|
||||||
@ -54,6 +58,9 @@ class TestUserLoginStage(TestCase):
|
|||||||
{"type": "redirect", "to": reverse("passbook_core:overview")},
|
{"type": "redirect", "to": reverse("passbook_core:overview")},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
def test_without_user(self):
|
def test_without_user(self):
|
||||||
"""Test a plan without any pending user, resulting in a denied"""
|
"""Test a plan without any pending user, resulting in a denied"""
|
||||||
plan = FlowPlan(
|
plan = FlowPlan(
|
||||||
@ -70,11 +77,11 @@ class TestUserLoginStage(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
force_str(response.content),
|
|
||||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
def test_without_backend(self):
|
def test_without_backend(self):
|
||||||
"""Test a plan with pending user, without backend, resulting in a denied"""
|
"""Test a plan with pending user, without backend, resulting in a denied"""
|
||||||
plan = FlowPlan(
|
plan = FlowPlan(
|
||||||
@ -92,10 +99,7 @@ class TestUserLoginStage(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
force_str(response.content),
|
|
||||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_form(self):
|
def test_form(self):
|
||||||
"""Test Form"""
|
"""Test Form"""
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"""write tests"""
|
"""write tests"""
|
||||||
import string
|
import string
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
@ -10,7 +11,9 @@ from passbook.core.models import User
|
|||||||
from passbook.flows.markers import StageMarker
|
from passbook.flows.markers import StageMarker
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
|
from passbook.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||||
from passbook.flows.views import SESSION_KEY_PLAN
|
from passbook.flows.views import SESSION_KEY_PLAN
|
||||||
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
from passbook.stages.user_write.forms import UserWriteStageForm
|
from passbook.stages.user_write.forms import UserWriteStageForm
|
||||||
from passbook.stages.user_write.models import UserWriteStage
|
from passbook.stages.user_write.models import UserWriteStage
|
||||||
@ -107,6 +110,9 @@ class TestUserWriteStage(TestCase):
|
|||||||
self.assertTrue(user_qs.first().check_password(new_password))
|
self.assertTrue(user_qs.first().check_password(new_password))
|
||||||
self.assertEqual(user_qs.first().attributes["some-custom-attribute"], "test")
|
self.assertEqual(user_qs.first().attributes["some-custom-attribute"], "test")
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
def test_without_data(self):
|
def test_without_data(self):
|
||||||
"""Test without data results in error"""
|
"""Test without data results in error"""
|
||||||
plan = FlowPlan(
|
plan = FlowPlan(
|
||||||
@ -123,10 +129,7 @@ class TestUserWriteStage(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
force_str(response.content),
|
|
||||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_form(self):
|
def test_form(self):
|
||||||
"""Test Form"""
|
"""Test Form"""
|
||||||
|
|||||||
Reference in New Issue
Block a user