Compare commits
127 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
8a105cf5a0 | |||
9e384df79e | |||
c0bfd32d39 | |||
7be680cbe5 | |||
93bf8eaa82 | |||
1248585dca | |||
1319c480c4 | |||
1911e8e3a9 | |||
4198c5363f | |||
207aae15a8 | |||
50531b8a36 | |||
e5e4824920 | |||
085247e2dc | |||
f766594ab0 | |||
d1e469e282 | |||
79e4500827 | |||
42702fa96a | |||
9deb3ad80f | |||
9877ef99c4 | |||
c304b40e1b | |||
f0e6d6f417 | |||
54de5c981e | |||
a446775fe2 | |||
7393d8720b | |||
287cb72d6f | |||
c5eff4bdd6 | |||
e9a33ed8ab | |||
875173a86e | |||
df7642b365 | |||
3bc1c0aa8b | |||
8951f5695e | |||
7401278707 | |||
e99f6e289b | |||
07da6ffa69 | |||
dc18730094 | |||
a202679bfb | |||
1edcda58ba | |||
5cb7f0794e | |||
7e8e3893eb | |||
e91e286ebc | |||
ef4a115b61 | |||
b79b73f5c6 | |||
056e3ed15b | |||
fb5e210af8 | |||
e5e2615f15 | |||
6c72a9e2e8 | |||
c04d0a373a | |||
bd74e518a7 | |||
3b76af4eaa | |||
706448dc14 | |||
34793f7cef | |||
ba96c9526e | |||
617432deaa | |||
36bf2be16d | |||
912ed343e6 | |||
2e15df295a | |||
eaab3f62cb | |||
aa615b0fd6 | |||
b775f2788c | |||
9c28db3d89 | |||
67360bd6e9 | |||
4f6f8c7cae | |||
3b82ad798b | |||
8827f06ac1 | |||
251672a67d | |||
4ffc0e2a08 | |||
4e1808632d | |||
791627d3ce | |||
f3df3a0157 | |||
6aaae53a19 | |||
4d84f6d598 | |||
4e2349b6d9 | |||
cd57b8f7f3 | |||
40b1fc06b0 | |||
02fa217e28 | |||
6652514358 | |||
dcd3dc9744 | |||
d6afdc575e | |||
287b38efee | |||
e805fb62fb | |||
c92dda77f1 | |||
f12fd78822 | |||
caba183c9b | |||
3aeaa121a3 | |||
a9f3118a7d | |||
054b819262 | |||
6b3411f63b | |||
6a8000ea0d | |||
352d4db0d7 | |||
4b665cfb8f | |||
4e12003944 | |||
6bfd465855 | |||
e8670aa693 | |||
5263e750b1 | |||
a2a9d73296 | |||
6befc9d627 | |||
73497a27cc | |||
f3098418f2 | |||
a5197963b2 | |||
e4634bcc78 | |||
74da44a6a9 | |||
3324473cd0 | |||
39d8038533 | |||
bbcf58705f | |||
7b5a0964b2 | |||
8eca76e464 | |||
fb9ab368f8 | |||
877279b2ee | |||
301be4b411 | |||
728f527ccb | |||
3f1c790b1d | |||
b00573bde2 | |||
aeee3ad7f9 | |||
ef021495ef | |||
061eab4b36 | |||
870e01f836 | |||
e2ca72adf0 | |||
395ef43eae | |||
a4cc653757 | |||
db4ff20906 | |||
1f0fbd33b6 | |||
5de8d2721e | |||
0d65da9a9e | |||
4316ee4330 | |||
2ed9a1dbe3 | |||
8e03824d20 | |||
754dbdd0e5 |
@ -1,10 +1,10 @@
|
||||
[bumpversion]
|
||||
current_version = 0.12.2-stable
|
||||
current_version = 0.12.11-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
serialize = {major}.{minor}.{patch}-{release}
|
||||
message = new release: {new_version}
|
||||
message = release: {new_version}
|
||||
tag_name = version/{new_version}
|
||||
|
||||
[bumpversion:part:release]
|
||||
@ -15,9 +15,9 @@ values =
|
||||
beta
|
||||
stable
|
||||
|
||||
[bumpversion:file:docs/installation/docker-compose.md]
|
||||
[bumpversion:file:website/docs/installation/docker-compose.md]
|
||||
|
||||
[bumpversion:file:docs/installation/kubernetes.md]
|
||||
[bumpversion:file:website/docs/installation/kubernetes.md]
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
|
@ -6,7 +6,7 @@ omit =
|
||||
manage.py
|
||||
*/migrations/*
|
||||
*/apps.py
|
||||
docs/
|
||||
website/
|
||||
|
||||
[report]
|
||||
sort = Cover
|
||||
|
@ -3,4 +3,4 @@ helm
|
||||
passbook-ui
|
||||
static
|
||||
*.env.yml
|
||||
node_modules/
|
||||
**/node_modules
|
||||
|
16
.github/dependabot.yml
vendored
16
.github/dependabot.yml
vendored
@ -24,3 +24,19 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: docker
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: docker
|
||||
directory: "/proxy"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.12.2-stable
|
||||
-t beryju/passbook:0.12.11-stable
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.12.2-stable
|
||||
run: docker push beryju/passbook:0.12.11-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.2-stable \
|
||||
-t beryju/passbook-proxy:0.12.11-stable \
|
||||
-t beryju/passbook-proxy:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-proxy:0.12.2-stable
|
||||
run: docker push beryju/passbook-proxy:0.12.11-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.2-stable
|
||||
-t beryju/passbook-static:0.12.11-stable
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.12.2-stable
|
||||
run: docker push beryju/passbook-static:0.12.11-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.2-stable
|
||||
tagName: 0.12.11-stable
|
||||
environment: beryjuorg-prod
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -199,3 +199,4 @@ local.env.yml
|
||||
|
||||
# Selenium Screenshots
|
||||
selenium_screenshots/**
|
||||
backups/
|
||||
|
@ -9,3 +9,4 @@ ignore-paths:
|
||||
|
||||
uses:
|
||||
- django
|
||||
- celery
|
||||
|
21
.pylintrc
21
.pylintrc
@ -1,16 +1,29 @@
|
||||
[MASTER]
|
||||
|
||||
disable=arguments-differ,no-self-use,fixme,locally-disabled,too-many-ancestors,too-few-public-methods,import-outside-toplevel,bad-continuation,signature-differs,similarities,cyclic-import
|
||||
disable =
|
||||
arguments-differ,
|
||||
no-self-use,
|
||||
fixme,
|
||||
locally-disabled,
|
||||
too-many-ancestors,
|
||||
too-few-public-methods,
|
||||
import-outside-toplevel,
|
||||
bad-continuation,
|
||||
signature-differs,
|
||||
similarities,
|
||||
cyclic-import,
|
||||
protected-access,
|
||||
unsubscriptable-object # remove when pylint is upgraded to 2.6
|
||||
|
||||
load-plugins=pylint_django,pylint.extensions.bad_builtin
|
||||
|
||||
extension-pkg-whitelist=lxml
|
||||
extension-pkg-whitelist=lxml,xmlsec
|
||||
|
||||
# Allow constants to be shorter than normal (and lowercase, for settings.py)
|
||||
const-rgx=[a-zA-Z0-9_]{1,40}$
|
||||
|
||||
ignored-modules=django-otp
|
||||
generated-members=xmlsec.constants.*,xmlsec.tree.*,xmlsec.template.*
|
||||
ignore=migrations
|
||||
max-attributes=12
|
||||
|
||||
jobs=12
|
||||
max-branches=20
|
||||
|
18
Dockerfile
18
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM python:3.8-slim-buster as locker
|
||||
FROM python:3.9-slim-buster as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
@ -9,7 +9,7 @@ RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
|
||||
FROM python:3.8-slim-buster
|
||||
FROM python:3.9-slim-buster
|
||||
|
||||
WORKDIR /
|
||||
COPY --from=locker /app/requirements.txt /
|
||||
@ -20,17 +20,27 @@ RUN apt-get update && \
|
||||
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
||||
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-12 postgresql-client-11 build-essential && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-12 postgresql-client-11 build-essential libxmlsec1-dev pkg-config && \
|
||||
apt-get clean && \
|
||||
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 && \
|
||||
mkdir /backups && \
|
||||
chown passbook:passbook /backups
|
||||
|
||||
COPY ./passbook/ /passbook
|
||||
COPY ./manage.py /
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
|
||||
USER passbook
|
||||
STOPSIGNAL SIGINT
|
||||
|
||||
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]
|
||||
|
2
Makefile
2
Makefile
@ -12,7 +12,7 @@ lint-fix:
|
||||
|
||||
lint:
|
||||
pyright passbook e2e lifecycle
|
||||
bandit -r passbook e2e lifecycle
|
||||
bandit -r passbook e2e lifecycle -x node_modules
|
||||
pylint passbook e2e lifecycle
|
||||
prospector
|
||||
|
||||
|
4
Pipfile
4
Pipfile
@ -35,7 +35,6 @@ qrcode = "*"
|
||||
requests-oauthlib = "*"
|
||||
sentry-sdk = "*"
|
||||
service_identity = "*"
|
||||
signxml = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
@ -44,9 +43,10 @@ channels = "*"
|
||||
channels-redis = "*"
|
||||
kubernetes = "*"
|
||||
docker = "*"
|
||||
xmlsec = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
python_version = "3.9"
|
||||
|
||||
[dev-packages]
|
||||
autopep8 = "*"
|
||||
|
791
Pipfile.lock
generated
791
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
12
README.md
12
README.md
@ -1,4 +1,4 @@
|
||||
<img src="docs/images/logo.svg" height="50" alt="passbook logo"><img src="docs/images/brand_inverted.svg" height="50" alt="passbook">
|
||||
<img src="website/static/img/logo.svg" height="50" alt="passbook logo"><img src="website/static/img/brand_inverted.svg" height="50" alt="passbook">
|
||||
|
||||
[](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1)
|
||||

|
||||
@ -13,18 +13,18 @@ passbook is an open-source Identity Provider focused on flexibility and versatil
|
||||
|
||||
## Installation
|
||||
|
||||
For small/test setups it is recommended to use docker-compose, see the [documentation](https://passbook.beryju.org/installation/docker-compose/)
|
||||
For small/test setups it is recommended to use docker-compose, see the [documentation](https://passbook.beryju.org/website/docs/installation/docker-compose/)
|
||||
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org//installation/kubernetes/)
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org/website/docs/installation/kubernetes/)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Development
|
||||
|
||||
To develop on passbook, you need a system with Python 3.7+ (3.8 is recommended). passbook uses [pipenv](https://pipenv.pypa.io/en/latest/) for managing dependencies.
|
||||
To develop on passbook, you need a system with Python 3.8+ (3.9 is recommended). passbook uses [pipenv](https://pipenv.pypa.io/en/latest/) for managing dependencies.
|
||||
|
||||
To get started, run
|
||||
|
||||
|
@ -22,10 +22,11 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -37,10 +38,11 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -52,10 +54,11 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
pipenv install --dev prospector --skip-lock
|
||||
@ -68,10 +71,11 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -86,13 +90,14 @@ stages:
|
||||
version: '12.x'
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: npm install -g pyright@1.1.79
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -106,7 +111,7 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -116,6 +121,7 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -128,6 +134,9 @@ stages:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -139,6 +148,7 @@ stages:
|
||||
inputs:
|
||||
script: |
|
||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -162,7 +172,7 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -179,6 +189,7 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -204,7 +215,7 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -221,6 +232,7 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: DockerCompose@0
|
||||
@ -286,10 +298,11 @@ stages:
|
||||
path: "coverage-unittest/"
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
|
||||
|
@ -19,7 +19,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.2-stable}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.11-stable}
|
||||
command: server
|
||||
environment:
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
@ -40,7 +40,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
worker:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.2-stable}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.11-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.2-stable}
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.11-stable}
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
@ -68,7 +68,7 @@ services:
|
||||
traefik:
|
||||
image: traefik:2.3
|
||||
command:
|
||||
- "--accesslog=true"
|
||||
- "--log.format=json"
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
|
@ -1,26 +0,0 @@
|
||||
# Passbook User Object
|
||||
|
||||
The User object has the following attributes:
|
||||
|
||||
- `username`: User's username.
|
||||
- `email` User's email.
|
||||
- `name` User's display name.
|
||||
- `is_staff` Boolean field if user is staff.
|
||||
- `is_active` Boolean field if user is active.
|
||||
- `date_joined` Date user joined/was created.
|
||||
- `password_change_date` Date password was last changed.
|
||||
- `attributes` Dynamic attributes.
|
||||
- `pb_groups` This is a queryset of all the user's groups.
|
||||
|
||||
You can do additional filtering like `user.pb_groups.filter(name__startswith='test')`, see [here](https://docs.djangoproject.com/en/3.1/ref/models/querysets/#id4)
|
||||
|
||||
To get the name of all groups, you can do `[group.name for group in user.pb_groups.all()]`
|
||||
|
||||
## Examples
|
||||
|
||||
List all the User's group names:
|
||||
|
||||
```python
|
||||
for group in user.pb_groups.all():
|
||||
yield group.name
|
||||
```
|
@ -1,7 +0,0 @@
|
||||
# OTP Stage
|
||||
|
||||
This stage offers a generic Time-based One-time Password authentication step.
|
||||
|
||||
You can optionally enforce this step, which will force every user without OTP setup to configure it.
|
||||
|
||||
This stage uses a 6-digit Code with a 30 second time-drift. This is currently not changeable.
|
@ -1,8 +0,0 @@
|
||||
# User Delete Stage
|
||||
|
||||
!!! danger
|
||||
This stage deletes the `pending_user` without any confirmation. You have to make sure the user is aware of this.
|
||||
|
||||
This stage is intended for an unenrollment flow. It deletes the currently pending user.
|
||||
|
||||
The pending user is also removed from the current session.
|
@ -1,76 +0,0 @@
|
||||
# Kubernetes
|
||||
|
||||
For a mid to high-load installation, Kubernetes is recommended. passbook is installed using a helm-chart.
|
||||
|
||||
This installation automatically applies database migrations on startup. After the installation is done, you can use `pbadmin` as username and password.
|
||||
|
||||
```yaml
|
||||
###################################
|
||||
# Values directly affecting passbook
|
||||
###################################
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.12.2-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
||||
# Enable the Kubernetes integration which lets passbook deploy outposts into kubernetes
|
||||
kubernetesIntegration: true
|
||||
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
errorReporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
sendPii: false
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
logLevel: warning
|
||||
|
||||
# Enable Database Backups to S3
|
||||
# backup:
|
||||
# accessKey: access-key
|
||||
# secretKey: secret-key
|
||||
# bucket: s3-bucket
|
||||
# region: eu-central-1
|
||||
# host: s3-host
|
||||
|
||||
ingress:
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
|
||||
###################################
|
||||
# Values controlling dependencies
|
||||
###################################
|
||||
|
||||
install:
|
||||
postgresql: true
|
||||
redis: true
|
||||
|
||||
# These values influence the bundled postgresql and redis charts, but are also used by passbook to connect
|
||||
postgresql:
|
||||
postgresqlDatabase: passbook
|
||||
|
||||
redis:
|
||||
cluster:
|
||||
enabled: false
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
```
|
@ -1,75 +0,0 @@
|
||||
# Ansible Tower / AWX Integration
|
||||
|
||||
## What is Tower
|
||||
|
||||
From https://docs.ansible.com/ansible/2.5/reference_appendices/tower.html
|
||||
|
||||
!!! note ""
|
||||
Ansible Tower (formerly ‘AWX’) is a web-based solution that makes Ansible even more easy to use for IT teams of all kinds. It’s designed to be the hub for all of your automation tasks.
|
||||
|
||||
Tower allows you to control access to who can access what, even allowing sharing of SSH credentials without someone being able to transfer those credentials. Inventory can be graphically managed or synced with a wide variety of cloud sources. It logs all of your jobs, integrates well with LDAP, and has an amazing browsable REST API. Command line tools are available for easy integration with Jenkins as well. Provisioning callbacks provide great support for autoscaling topologies.
|
||||
|
||||
!!! note
|
||||
AWX is the open-source version of Tower. The term "AWX" will be used interchangeably throughout this document.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `awx.company` is the FQDN of the AWX/Tower install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML provider with the following parameters:
|
||||
|
||||
- ACS URL: `https://awx.company/sso/complete/saml/`
|
||||
- Audience: `awx`
|
||||
- Service Provider Binding: Post
|
||||
- Issuer: `https://awx.company/sso/metadata/saml/`
|
||||
|
||||
You can of course use a custom signing certificate, and adjust durations.
|
||||
|
||||
## AWX Configuration
|
||||
|
||||
Navigate to `https://awx.company/#/settings/auth` to configure SAML. Set the Field `SAML SERVICE PROVIDER ENTITY ID` to `awx`.
|
||||
|
||||
For the fields `SAML SERVICE PROVIDER PUBLIC CERTIFICATE` and `SAML SERVICE PROVIDER PRIVATE KEY`, you can either use custom certificates, or use the self-signed pair generated by passbook.
|
||||
|
||||
Provide metadata in the `SAML Service Provider Organization Info` field:
|
||||
|
||||
```json
|
||||
{
|
||||
"en-US": {
|
||||
"name": "passbook",
|
||||
"url": "https://passbook.company",
|
||||
"displayname": "passbook"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Provide metadata in the `SAML Service Provider Technical Contact` and `SAML Service Provider Technical Contact` fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"givenName": "Admin Name",
|
||||
"emailAddress": "admin@company"
|
||||
}
|
||||
```
|
||||
|
||||
In the `SAML Enabled Identity Providers` paste the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"passbook": {
|
||||
"attr_username": "urn:oid:2.16.840.1.113730.3.1.241",
|
||||
"attr_user_permanent_id": "urn:oid:0.9.2342.19200300.100.1.1",
|
||||
"x509cert": "MIIDEjCCAfqgAwIBAgIRAJZ9pOZ1g0xjiHtQAAejsMEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlcGFzc2Jvb2sgU2VsZi1zaWduZWQgU0FNTCBDZXJ0aWZpY2F0ZTAeFw0xOTEyMjYyMDEwNDFaFw0yMDEyMjYyMDEwNDFaMFkxLjAsBgNVBAMMJXBhc3Nib29rIFNlbGYtc2lnbmVkIFNBTUwgQ2VydGlmaWNhdGUxETAPBgNVBAoMCHBhc3Nib29rMRQwEgYDVQQLDAtTZWxmLXNpZ25lZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO/ktBYZkY9xAijF4acvzX6Q1K8KoIZeyde8fVgcWBz4L5FgDQ4/dni4k2YAcPdwteGL4nKVzetUzjbRCBUNuO6lqU4J4WNNX4Xg4Ir7XLRoAQeo+omTPBdpJ1p02HjtN5jT01umN3bK2yto1e37CJhK6WJiaXqRewPxh4lI4aqdj3BhFkJ3I3r2qxaWOAXQ6X7fg3w/ny7QP53//ouZo7hSLY3GIcRKgvdjjVM3OW5C3WLpOq5Dez5GWVJ17aeFCfGQ8bwFKde6qfYqyGcU9xHB36TtVHB9hSFP/tUFhkiSOxtsrYwCgCyXm4UTSpP+wiNyjKfFw7qGLBvA2hGTNw8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh9PeAqPRQk1/SSygIFADZBi08O/DPCshFwEHvJATIcTzcDD8UGAjXh+H5OlkDyX7KyrcaNvYaafCUo63A+WprdtdY5Ty6SBEwTYyiQyQfwM9BfK+imCoif1Ai7xAelD7p9lNazWq7JU+H/Ep7U7Q7LvpxAbK0JArt+IWTb2NcMb3OWE1r0gFbs44O1l6W9UbJTbyLMzbGbe5i+NHlgnwPwuhtRMh0NUYabGHKcHbhwyFhfGAQv2dAp5KF1E5gu6ZzCiFePzc0FrqXQyb2zpFYcJHXquiqaOeG7cZxRHYcjrl10Vxzki64XVA9BpdELgKSnupDGUEJsRUt3WVOmvZuA==",
|
||||
"url": "https://passbook.company/application/saml/awx/login/",
|
||||
"attr_last_name": "User.LastName",
|
||||
"entity_id": "https://awx.company/sso/metadata/saml/",
|
||||
"attr_email": "urn:oid:0.9.2342.19200300.100.1.3",
|
||||
"attr_first_name": "urn:oid:2.5.4.3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`x509cert` is the certificate configured in passbook. Remove the `--BEGIN CERTIFICATE--` and `--END CERTIFICATE--` headers, then enter the cert as one non-breaking string.
|
@ -1,83 +0,0 @@
|
||||
# VMware vCenter Integration
|
||||
|
||||
## What is vCenter
|
||||
|
||||
From https://en.wikipedia.org/wiki/VCenter
|
||||
|
||||
!!! note ""
|
||||
|
||||
vCenter Server is the centralized management utility for VMware, and is used to manage virtual machines, multiple ESXi hosts, and all dependent components from a single centralized location. VMware vMotion and svMotion require the use of vCenter and ESXi hosts.
|
||||
|
||||
!!! warning
|
||||
|
||||
This requires passbook 0.10.3 or newer.
|
||||
|
||||
!!! warning
|
||||
|
||||
This requires VMware vCenter 7.0.0 or newer.
|
||||
|
||||
!!! note
|
||||
|
||||
It seems that the vCenter still needs to be joined to the Active Directory Domain, otherwise group membership does not work correctly. We're working on a fix for this, for the meantime your vCenter should be part of your Domain.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `vcenter.company` is the FQDN of the vCenter server.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Since vCenter only allows OpenID-Connect in combination with Active Directory, it is recommended to have passbook sync with the same Active Directory.
|
||||
|
||||
### Step 1
|
||||
|
||||
Under *Property Mappings*, create a *Scope Mapping*. Give it a name like "OIDC-Scope-VMware-vCenter". Set the scope name to `openid` and the expression to the following
|
||||
|
||||
```python
|
||||
return {
|
||||
"domain": "<your active directory domain>",
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2
|
||||
|
||||
!!! note
|
||||
If your Active Directory Schema is the same as your Email address schema, skip to Step 3.
|
||||
|
||||
Under *Sources*, click *Edit* and ensure that "Autogenerated Active Directory Mapping: userPrincipalName -> attributes.upn" has been added to your source.
|
||||
|
||||
### Step 3
|
||||
|
||||
Under *Providers*, create an OAuth2/OpenID Provider with these settings:
|
||||
|
||||
- Client Type: Confidential
|
||||
- Response Type: code (ADFS Compatibility Mode, sends id_token as access_token)
|
||||
- JWT Algorithm: RS256
|
||||
- Redirect URI: `https://vcenter.company/ui/login/oauth2/authcode`
|
||||
- Post Logout Redirect URIs: `https://vcenter.company/ui/login`
|
||||
- Sub Mode: If your Email address Schema matches your UPN, select "Based on the User's Email...", otherwise select "Based on the User's UPN...".
|
||||
- Scopes: Select the Scope Mapping you've created in Step 1
|
||||
|
||||

|
||||
|
||||
### Step 4
|
||||
|
||||
Create an application which uses this provider. Optionally apply access restrictions to the application.
|
||||
|
||||
Set the Launch URL to `https://vcenter.company/ui/login/oauth2`. This will skip vCenter's User Prompt and directly log you in.
|
||||
|
||||
## vCenter Setup
|
||||
|
||||
Login as local Administrator account (most likely ends with vsphere.local). Using the Menu in the Navigation bar, navigate to *Administration -> Single Sing-on -> Configuration*.
|
||||
|
||||
Click on *Change Identity Provider* in the top-right corner.
|
||||
|
||||
In the wizard, select "Microsoft ADFS" and click Next.
|
||||
|
||||
Fill in the Client Identifier and Shared Secret from the Provider in passbook. For the OpenID Address, click on *View Setup URLs* in passbook, and copy the OpenID Configuration URL.
|
||||
|
||||
On the next page, fill in your Active Directory Connection Details. These should be similar to what you have set in passbook.
|
||||
|
||||

|
||||
|
||||
If your vCenter was already setup with LDAP beforehand, your Role assignments will continue to work.
|
@ -1,20 +0,0 @@
|
||||
# Outpost deployment in docker-compose
|
||||
|
||||
To deploy an outpost with docker-compose, use this snippet in your docker-compose file.
|
||||
|
||||
You can also run the outpost in a separate docker-compose project, you just have to ensure that the outpost container can reach your application container.
|
||||
|
||||
```yaml
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
passbook_proxy:
|
||||
image: beryju/passbook-proxy:0.10.0-stable
|
||||
ports:
|
||||
- 4180:4180
|
||||
- 4443:4443
|
||||
environment:
|
||||
PASSBOOK_HOST: https://your-passbook.tld
|
||||
PASSBOOK_INSECURE: 'false'
|
||||
PASSBOOK_TOKEN: token-generated-by-passbook
|
||||
```
|
@ -1,99 +0,0 @@
|
||||
# Outpost deployment on Kubernetes
|
||||
|
||||
Use the following manifest, replacing all values surrounded with `__`.
|
||||
|
||||
Afterwards, configure the proxy provider to connect to `<service name>.<namespace>.svc.cluster.local`, and update your Ingress to connect to the `passbook-outpost` service.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
name: passbook-outpost-api
|
||||
stringData:
|
||||
passbook_host: '__PASSBOOK_URL__'
|
||||
passbook_host_insecure: 'true'
|
||||
token: '__PASSBOOK_TOKEN__'
|
||||
type: Opaque
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
name: passbook-outpost
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 4180
|
||||
protocol: TCP
|
||||
targetPort: http
|
||||
- name: https
|
||||
port: 4443
|
||||
protocol: TCP
|
||||
targetPort: https
|
||||
selector:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
name: passbook-outpost
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: PASSBOOK_HOST
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: passbook_host
|
||||
name: passbook-outpost-api
|
||||
- name: PASSBOOK_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: token
|
||||
name: passbook-outpost-api
|
||||
- name: PASSBOOK_INSECURE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: passbook_host_insecure
|
||||
name: passbook-outpost-api
|
||||
image: beryju/passbook-proxy:0.10.0-stable
|
||||
name: proxy
|
||||
ports:
|
||||
- containerPort: 4180
|
||||
name: http
|
||||
protocol: TCP
|
||||
- containerPort: 4443
|
||||
name: https
|
||||
protocol: TCP
|
||||
```
|
@ -1,14 +0,0 @@
|
||||
# Outposts
|
||||
|
||||
An outpost is a single deployment of a passbook component, which can be deployed in a completely separate environment. Currently, only the Proxy Provider is supported as outpost.
|
||||
|
||||

|
||||
|
||||
Upon creation, a service account and a token is generated. The service account only has permissions to read the outpost and provider configuration. This token is used by the Outpost to connect to passbook.
|
||||
|
||||
To deploy an outpost, see: <a name="deploy">
|
||||
|
||||
- [Kubernetes](deploy-kubernetes.md)
|
||||
- [docker-compose](deploy-docker-compose.md)
|
||||
|
||||
In future versions, this snippet will be automatically generated. You will also be able to deploy an outpost directly into a kubernetes cluster.
|
@ -1,37 +0,0 @@
|
||||
# Expression Policies
|
||||
|
||||
!!! notice
|
||||
These variables are available in addition to the common variables/functions defined in [**Expressions**](../expressions/index.md)
|
||||
|
||||
The passing of the policy is determined by the return value of the code. Use `return True` to pass a policy and `return False` to fail it.
|
||||
|
||||
### Available Functions
|
||||
|
||||
#### `pb_message(message: str)`
|
||||
|
||||
Add a message, visible by the end user. This can be used to show the reason why they were denied.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
pb_message("Access denied")
|
||||
return False
|
||||
```
|
||||
|
||||
### Context variables
|
||||
|
||||
- `request`: A PolicyRequest object, which has the following properties:
|
||||
- `request.user`: The current user, against which the policy is applied. ([ref](../expressions/reference/user-object.md))
|
||||
- `request.http_request`: The Django HTTP Request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects))
|
||||
- `request.obj`: A Django Model instance. This is only set if the policy is ran against an object.
|
||||
- `request.context`: A dictionary with dynamic data. This depends on the origin of the execution.
|
||||
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external provider.
|
||||
- `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted. Can be [compared](../expressions/index.md#comparing-ip-addresses)
|
||||
|
||||
Additionally, when the policy is executed from a flow, every variable from the flow's current context is accessible under the `context` object.
|
||||
|
||||
This includes the following:
|
||||
|
||||
- `prompt_data`: Data which has been saved from a prompt stage or an external source.
|
||||
- `application`: The application the user is in the process of authorizing.
|
||||
- `pending_user`: The currently pending user
|
@ -1,12 +0,0 @@
|
||||
# Property Mapping Expressions
|
||||
|
||||
The property mapping should return a value that is expected by the Provider/Source. Supported types are documented in the individual Provider/Source. Returning `None` is always accepted and would simply skip the mapping for which `None` was returned.
|
||||
|
||||
!!! notice
|
||||
These variables are available in addition to the common variables/functions defined in [**Expressions**](../expressions/index.md)
|
||||
|
||||
### Context Variables
|
||||
|
||||
- `user`: The current user. This may be `None` if there is no contextual user. ([ref](../expressions/reference/user-object.md))
|
||||
- `request`: The current request. This may be `None` if there is no contextual request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects))
|
||||
- Other arbitrary arguments given by the provider, this is documented on the Provider/Source.
|
@ -1,31 +0,0 @@
|
||||
# OAuth2 Provider
|
||||
|
||||
This provider supports both generic OAuth2 as well as OpenID Connect
|
||||
|
||||
Scopes can be configured using Scope Mappings, a type of [Property Mappings](../property-mappings/index.md#scope-mapping).
|
||||
|
||||
Endpoint | URL
|
||||
---------|---
|
||||
Authorization | `/application/o/authorize/`
|
||||
Token | `/application/o/token/`
|
||||
User Info | `/application/o/userinfo/`
|
||||
End Session | `/application/o/end-session/`
|
||||
Introspect | `/application/o/end-session/`
|
||||
JWKS | `/application/o/<application slug>/jwks/`
|
||||
OpenID Configuration | `/application/o/<application slug>/.well-known/openid-configuration`
|
||||
|
||||
## GitHub Compatibility
|
||||
|
||||
This provider also exposes a GitHub-compatible endpoint. This endpoint can be used by applications, which support authenticating against GitHub Enterprise, but not generic OpenID Connect.
|
||||
|
||||
To use any of the GitHub Compatibility scopes, you have to use the GitHub Compatibility Endpoints.
|
||||
|
||||
|
||||
Endpoint | URL
|
||||
---------|---
|
||||
Authorization | `/login/oauth/authorize`
|
||||
Token | `/login/oauth/access_token`
|
||||
User Info | `/user`
|
||||
User Teams Info | `/user/teams`
|
||||
|
||||
To access the user's email address, a scope of `user:email` is required. To access their groups, `read:org` is required. Because these scopes are handled by a different endpoint, they are not customisable as a Scope Mapping.
|
@ -1,16 +0,0 @@
|
||||
# Proxy Provider
|
||||
|
||||
!!! info
|
||||
This provider is to be used in conjunction with [Outposts](../outposts/outposts.md)
|
||||
|
||||
This provider protects applications, which have no built-in support for OAuth2 or SAML. This is done by running a lightweight Reverse Proxy in front of the application, which authenticates the requests.
|
||||
|
||||
passbook Proxy is based on [oauth2_proxy](https://github.com/oauth2-proxy/oauth2-proxy), but has been integrated more tightly with passbook.
|
||||
|
||||
The Proxy these extra headers to the application:
|
||||
|
||||
Header Name | Value
|
||||
-------------|-------
|
||||
X-Auth-Request-User | The user's unique identifier
|
||||
X-Auth-Request-Email | The user's email address
|
||||
X-Auth-Request-Preferred-Username | The user's username
|
@ -1,12 +0,0 @@
|
||||
# SAML Provider
|
||||
|
||||
This provider allows you to integrate enterprise software using the SAML2 Protocol. It supports signed requests and uses [Property Mappings](../property-mappings/index.md#saml-property-mapping) to determine which fields are exposed and what values they return. This makes it possible to expose vendor-specific fields.
|
||||
Default fields are exposed through auto-generated Property Mappings, which are prefixed with "Autogenerated".
|
||||
|
||||
|
||||
Endpoint | URL
|
||||
---------|---
|
||||
SSO (Redirect binding) | `/application/saml/<application slug>/sso/binding/redirect/`
|
||||
SSO (POST binding) | `/application/saml/<application slug>/sso/binding/post/`
|
||||
IdP-initiated login | `/application/saml/<application slug>/sso/binding/init/`
|
||||
Metadata Download | `/application/saml/<application slug>/metadata/`
|
@ -1,2 +0,0 @@
|
||||
mkdocs
|
||||
mkdocs-material
|
@ -1 +0,0 @@
|
||||
3.7
|
@ -1,73 +0,0 @@
|
||||
# Upgrading to 0.10
|
||||
|
||||
This update brings a lot of big features, such as:
|
||||
|
||||
- New OAuth2/OpenID Provider
|
||||
|
||||
This new provider merges both OAuth2 and OpenID. It is based on the codebase of the old provider, which has been simplified and cleaned from the ground up. Support for Property Mappings has also been added. Because of this change, OpenID and OAuth2 Providers will have to be re-created.
|
||||
|
||||
- Proxy Provider
|
||||
|
||||
Due to this new OAuth2 Provider, the Application Gateway Provider, now simply called "Proxy Provider" has been revamped as well. The new passbook Proxy integrates more tightly with passbook via the new Outposts system. The new proxy also supports multiple applications per proxy instance, can configure TLS based on passbook Keypairs, and more.
|
||||
|
||||
See [Proxy](../providers/proxy.md)
|
||||
|
||||
- Outpost System
|
||||
|
||||
This is a new Object type, currently used only by the Proxy Provider. It manages the creation and permissions of service accounts, which are used by the outposts to communicate with passbook.
|
||||
|
||||
See [Outposts](../outposts/outposts.md)
|
||||
|
||||
- Flow Import/Export
|
||||
|
||||
Flows can now be imported and exported. This feature can be used as a backup system, or to share complex flows with other people. Example flows have also been added to the documentation to help you get going with passbook.
|
||||
|
||||
## Under the hood
|
||||
|
||||
- passbook now runs on Django 3.1 and Channels with complete ASGI enabled
|
||||
- uwsgi has been replaced with Gunicorn and uvicorn
|
||||
- Elastic APM has been replaced with Sentry Performance metrics
|
||||
- Flow title is now configurable separately from the name
|
||||
- All logging output is now json
|
||||
|
||||
## Upgrading
|
||||
|
||||
### docker-compose
|
||||
|
||||
The docker-compose file has been updated, please download the latest from `https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml`.
|
||||
By default, the new compose file uses a fixed version to prevent unintended updates.
|
||||
|
||||
Before updating the file, stop all containers. Then download the file, pull the new containers and start the database.
|
||||
|
||||
```
|
||||
docker-compose down
|
||||
docker-compose pull
|
||||
docker-compose up --no-start
|
||||
docker-compose start redis postgrseql
|
||||
docker-compose run --rm server migrate
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Helm
|
||||
|
||||
A few options have changed:
|
||||
|
||||
- `error_reporting` was changed from a simple boolean to a dictionary:
|
||||
|
||||
```yaml
|
||||
error_reporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
send_pii: false
|
||||
```
|
||||
|
||||
- The `apm` and `monitoring` blocks have been removed.
|
||||
- `serverReplicas` and `workerReplicas` have been added
|
||||
|
||||
### Upgrading
|
||||
|
||||
This upgrade only applies if you are upgrading from a running 0.9 instance. Passbook detects this on startup, and automatically executes this upgrade.
|
||||
|
||||
Because this upgrade brings the new OAuth2 Provider, the old providers will be lost in the process. Make sure to take note of the providers you want to bring over.
|
||||
|
||||
Another side-effect of this upgrade is the change of OAuth2 URLs, see [here](../providers/oauth2.md).
|
@ -1,20 +0,0 @@
|
||||
# Upgrading to 0.11
|
||||
|
||||
This update brings these headline features:
|
||||
|
||||
- Add Backup and Restore, currently only externally schedulable, documented [here](https://passbook.beryju.org/maintenance/backups/)
|
||||
- New Admin Dashboard with more metrics and Charts
|
||||
|
||||
Shows successful and failed logins from the last 24 hours, as well as the most used applications
|
||||
- Add search to all table views
|
||||
- Outpost now supports a Docker Controller, which installs the Outpost on the same host as passbook, updates and manages it
|
||||
- Add Token Identifier
|
||||
|
||||
Tokens now have an identifier which is used to reference to them, so the Primary key is not shown in URLs
|
||||
- `core/applications/list` API now shows applications the user has access to via policies
|
||||
|
||||
## Upgrading
|
||||
|
||||
This upgrade can be done as with minor upgrades, the only external change is the new docker-compose file, which enabled the Docker Integration for Outposts. To use this feature, please download the latest docker-compose from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml).
|
||||
|
||||
Afterwards, you can simply run `docker-compose up -d` and then the normal upgrade command of `docker-compose run --rm server migrate`.
|
@ -1,63 +0,0 @@
|
||||
# Upgrading to 0.12
|
||||
|
||||
This update brings these headline features:
|
||||
|
||||
- Rewrite Outpost state Logic, which now supports multiple concurrent Outpost instances.
|
||||
- Add Kubernetes Integration for Outposts, which deploys and maintains Outposts with High Availability in a Kubernetes Cluster
|
||||
- Add System Task Overview to see all background tasks, their status, the log output, and retry them
|
||||
- Alerts now disappear automatically
|
||||
- Audit Logs are now searchable
|
||||
- Users can now create their own Tokens to access the API
|
||||
- docker-compose deployment now uses traefik 2.3
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fix high CPU Usage of the proxy when Websocket connections fail
|
||||
|
||||
## Upgrading
|
||||
|
||||
### docker-compose
|
||||
|
||||
Docker-compose users should download the latest docker-compose file from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). This includes the new traefik 2.3.
|
||||
|
||||
Afterwards, you can simply run `docker-compose up -d` and then the normal upgrade command of `docker-compose run --rm server migrate`.
|
||||
|
||||
### Kubernetes
|
||||
|
||||
For Kubernetes users, there are some changes to the helm values.
|
||||
|
||||
The values change from
|
||||
|
||||
```yaml
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
error_reporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
send_pii: false
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
log_level: warning
|
||||
```
|
||||
|
||||
to
|
||||
|
||||
```yaml
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
errorReporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
sendPii: false
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
logLevel: warning
|
||||
```
|
||||
|
||||
in order to be consistent with the rest of the settings.
|
||||
|
||||
There is also a new setting called `kubernetesIntegration`, which controls the Kubernetes integration for passbook. When enabled (the default), a Service Account is created, which allows passbook to deploy and update Outposts.
|
@ -9,7 +9,7 @@ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
sudo npm install -g yarn
|
||||
# Setup python
|
||||
sudo apt install -y python3.8 python3-pip
|
||||
sudo apt install -y python3.9 python3-pip libxmlsec1-dev pkg-config
|
||||
# Setup docker
|
||||
sudo pip3 install pipenv
|
||||
|
||||
|
@ -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
|
||||
@ -23,7 +23,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "mailhog/mailhog:v1.0.1",
|
||||
"image": "docker.beryju.org/proxy/mailhog/mailhog:v1.0.1",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
@ -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"""
|
||||
|
@ -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/")
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -33,7 +33,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
"""Setup client grafana container which we test OAuth against"""
|
||||
return {
|
||||
"image": "grafana/grafana:7.1.0",
|
||||
"image": "docker.beryju.org/proxy/grafana/grafana:7.1.0",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
@ -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
|
||||
|
@ -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
|
||||
@ -47,7 +47,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "grafana/grafana:7.1.0",
|
||||
"image": "docker.beryju.org/proxy/grafana/grafana:7.1.0",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
@ -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)
|
||||
|
@ -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
|
||||
@ -53,7 +53,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
client: DockerClient = from_env()
|
||||
client.images.pull("beryju/oidc-test-client")
|
||||
container = client.containers.run(
|
||||
image="beryju/oidc-test-client",
|
||||
image="docker.beryju.org/proxy/beryju/oidc-test-client",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
@ -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)
|
||||
|
@ -11,14 +11,14 @@ 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
|
||||
from passbook.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
Outpost,
|
||||
OutpostConfig,
|
||||
OutpostDeploymentType,
|
||||
OutpostType,
|
||||
)
|
||||
from passbook.providers.proxy.models import ProxyProvider
|
||||
@ -36,7 +36,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "traefik/whoami:latest",
|
||||
"image": "docker.beryju.org/proxy/traefik/whoami:latest",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
@ -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(
|
||||
@ -75,7 +76,6 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name="proxy_outpost",
|
||||
type=OutpostType.PROXY,
|
||||
deployment_type=OutpostDeploymentType.CUSTOM,
|
||||
)
|
||||
outpost.providers.add(proxy)
|
||||
outpost.save()
|
||||
@ -110,6 +110,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()
|
||||
@ -126,10 +127,11 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||
proxy.save()
|
||||
# we need to create an application to actually access the proxy
|
||||
Application.objects.create(name="proxy", slug="proxy", provider=proxy)
|
||||
service_connection = DockerServiceConnection.objects.get(local=True)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name="proxy_outpost",
|
||||
type=OutpostType.PROXY,
|
||||
deployment_type=OutpostDeploymentType.DOCKER,
|
||||
service_connection=service_connection,
|
||||
_config=asdict(
|
||||
OutpostConfig(passbook_host=self.live_server_url, log_level="debug")
|
||||
),
|
||||
|
@ -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
|
||||
@ -38,7 +38,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
client: DockerClient = from_env()
|
||||
client.images.pull("beryju/oidc-test-client")
|
||||
container = client.containers.run(
|
||||
image="beryju/saml-test-sp",
|
||||
image="docker.beryju.org/proxy/beryju/saml-test-sp",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
@ -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
|
||||
|
@ -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()
|
||||
@ -255,7 +258,7 @@ class TestSourceOAuth1(SeleniumTestCase):
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "beryju/oauth1-test-server",
|
||||
"image": "docker.beryju.org/proxy/beryju/oauth1-test-server",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
@ -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()
|
||||
|
@ -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
|
||||
@ -75,7 +75,7 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "kristophjunge/test-saml-idp:1.15",
|
||||
"image": "docker.beryju.org/proxy/kristophjunge/test-saml-idp:1.15",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
@ -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
|
||||
|
42
e2e/utils.py
42
e2e/utils.py
@ -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 NoSuchElementException, 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,40 @@ 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, NoSuchElementException]
|
||||
|
||||
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()
|
||||
self._post_teardown() # noqa
|
||||
self.setUp()
|
||||
return wrapper(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return retry_actual
|
||||
|
@ -1,9 +1,11 @@
|
||||
apiVersion: v2
|
||||
appVersion: "0.12.2-stable"
|
||||
description: A Helm chart for passbook.
|
||||
description: passbook is an open-source Identity Provider focused on flexibility and versatility. You can use passbook in an existing environment to add support for new protocols. passbook is also a great solution for implementing signup/recovery/etc in your application, so you don't have to deal with it.
|
||||
name: passbook
|
||||
version: "0.12.2-stable"
|
||||
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
|
||||
home: https://passbook.beryju.org
|
||||
sources:
|
||||
- https://github.com/BeryJu/passbook
|
||||
version: "0.12.11-stable"
|
||||
icon: https://raw.githubusercontent.com/BeryJu/passbook/master/website/static/img/logo.svg
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 9.4.1
|
||||
|
28
helm/README.md
Normal file
28
helm/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# passbook Helm Chart
|
||||
|
||||
| Name | Default | Description |
|
||||
|-----------------------------------|-------------------------|-------------|
|
||||
| image.name | beryju/passbook | Image used to run the passbook server and worker |
|
||||
| image.name_static | beryju/passbook-static | Image used to run the passbook static server (CSS and JS Files) |
|
||||
| image.tag | 0.12.5-stable | Image tag |
|
||||
| serverReplicas | 1 | Replicas for the Server deployment |
|
||||
| workerReplicas | 1 | Replicas for the Worker deployment |
|
||||
| kubernetesIntegration | true | Enable/disable the Kubernetes integration for passbook. This will create a service account for passbook to create and update outposts in passbook |
|
||||
| config.secretKey | | Secret key used to sign session cookies, generate with `pwgen 50 1` for example. |
|
||||
| config.errorReporting.enabled | false | Enable/disable error reporting |
|
||||
| config.errorReporting.environment | customer | Environment sent with the error reporting |
|
||||
| config.errorReporting.sendPii | false | Whether to send Personally-identifiable data with the error reporting |
|
||||
| config.logLevel | warning | Log level of passbook |
|
||||
| backup.accessKey | | Optionally enable S3 Backup, Access Key |
|
||||
| backup.secretKey | | Optionally enable S3 Backup, Secret Key |
|
||||
| backup.bucket | | Optionally enable S3 Backup, Bucket |
|
||||
| backup.region | | Optionally enable S3 Backup, Region |
|
||||
| backup.host | | Optionally enable S3 Backup, to custom Endpoint like minio |
|
||||
| ingress.annotations | {} | Annotations for the ingress object |
|
||||
| ingress.hosts | [passbook.k8s.local] | Hosts which the ingress will match |
|
||||
| ingress.tls | [] | TLS Configuration, same as Ingress objects |
|
||||
| install.postgresql | true | Enables/disables the packaged PostgreSQL Chart
|
||||
| install.redis | true | Enables/disables the packaged Redis Chart
|
||||
| postgresql.postgresqlPassword | | Password used for PostgreSQL, generated automatically.
|
||||
|
||||
For more info, see https://passbook.beryju.org/ and https://passbook.beryju.org/docs/installation/kubernetes/
|
@ -3,7 +3,7 @@
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "passbook.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- default .Chart.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
@ -12,17 +12,13 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "passbook.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- $name := default .Chart.Name -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
|
@ -1,42 +0,0 @@
|
||||
{{- if .Values.backup }}
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: {{ include "passbook.fullname" . }}-backup
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
schedule: "0 0 * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
args: [server]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
prefix: PASSBOOK_
|
||||
env:
|
||||
- name: PASSBOOK_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ include "passbook.fullname" . }}-secret-key"
|
||||
key: "secret_key"
|
||||
- name: PASSBOOK_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-redis"
|
||||
key: "redis-password"
|
||||
- name: PASSBOOK_POSTGRESQL__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-postgresql"
|
||||
key: "postgresql-password"
|
||||
{{- end}}
|
@ -4,9 +4,7 @@
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.12.2-stable
|
||||
|
||||
nameOverride: ""
|
||||
tag: 0.12.11-stable
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
@ -38,7 +36,6 @@ ingress:
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
@ -62,7 +59,5 @@ redis:
|
||||
cluster:
|
||||
enabled: false
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Gunicorn config"""
|
||||
import os
|
||||
import warnings
|
||||
from multiprocessing import cpu_count
|
||||
from pathlib import Path
|
||||
|
||||
@ -13,6 +15,8 @@ worker_class = "uvicorn.workers.UvicornWorker"
|
||||
# Docker containers don't have /tmp as tmpfs
|
||||
worker_tmp_dir = "/dev/shm"
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
|
||||
|
||||
logconfig_dict = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
@ -49,3 +53,5 @@ if Path("/var/run/secrets/kubernetes.io").exists():
|
||||
else:
|
||||
worker = cpu_count() * 2 + 1
|
||||
threads = 4
|
||||
|
||||
warnings.simplefilter("once")
|
||||
|
95
mkdocs.yml
95
mkdocs.yml
@ -1,95 +0,0 @@
|
||||
site_name: passbook Docs
|
||||
site_url: https://passbook.beryju.org/
|
||||
copyright: "Copyright © 2019 - 2020 BeryJu.org"
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Terminology: terminology.md
|
||||
- Installation:
|
||||
- docker-compose: installation/docker-compose.md
|
||||
- Kubernetes: installation/kubernetes.md
|
||||
- Reverse Proxy: installation/reverse-proxy.md
|
||||
- Flows:
|
||||
Overview: flow/flows.md
|
||||
Examples: flow/examples/examples.md
|
||||
- Stages:
|
||||
- Captcha Stage: flow/stages/captcha/index.md
|
||||
- Dummy Stage: flow/stages/dummy/index.md
|
||||
- Email Stage: flow/stages/email/index.md
|
||||
- Identification Stage: flow/stages/identification/index.md
|
||||
- Invitation Stage: flow/stages/invitation/index.md
|
||||
- OTP Stage: flow/stages/otp/index.md
|
||||
- Password Stage: flow/stages/password/index.md
|
||||
- Prompt Stage: flow/stages/prompt/index.md
|
||||
- Prompt Stage Validation: flow/stages/prompt/validation.md
|
||||
- User Delete Stage: flow/stages/user_delete.md
|
||||
- User Login Stage: flow/stages/user_login.md
|
||||
- User Logout Stage: flow/stages/user_logout.md
|
||||
- User Write Stage: flow/stages/user_write.md
|
||||
- Sources: sources.md
|
||||
- Providers:
|
||||
- OAuth2: providers/oauth2.md
|
||||
- SAML: providers/saml.md
|
||||
- Proxy: providers/proxy.md
|
||||
- Outposts:
|
||||
- Overview: outposts/outposts.md
|
||||
- Upgrading: outposts/upgrading.md
|
||||
- Deploy on docker-compose: outposts/deploy-docker-compose.md
|
||||
- Deploy on Kubernetes: outposts/deploy-kubernetes.md
|
||||
- Expressions:
|
||||
- Overview: expressions/index.md
|
||||
- Reference:
|
||||
- User Object: expressions/reference/user-object.md
|
||||
- Property Mappings:
|
||||
- Overview: property-mappings/index.md
|
||||
- Expressions: property-mappings/expression.md
|
||||
- Policies:
|
||||
- Overview: policies/index.md
|
||||
- Expression: policies/expression.md
|
||||
- Integrations:
|
||||
- as Source:
|
||||
- Active Directory: integrations/sources/active-directory/index.md
|
||||
- as Provider:
|
||||
- Amazon Web Services: integrations/services/aws/index.md
|
||||
- GitLab: integrations/services/gitlab/index.md
|
||||
- Rancher: integrations/services/rancher/index.md
|
||||
- Harbor: integrations/services/harbor/index.md
|
||||
- Sentry: integrations/services/sentry/index.md
|
||||
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
|
||||
- VMware vCenter: integrations/services/vmware-vcenter/index.md
|
||||
- Ubuntu Landscape: integrations/services/ubuntu-landscape/index.md
|
||||
- Sonarr: integrations/services/sonarr/index.md
|
||||
- Tautulli: integrations/services/tautulli/index.md
|
||||
- Maintenance:
|
||||
- Backups: maintenance/backups/index.md
|
||||
- Upgrading:
|
||||
- to 0.9: upgrading/to-0.9.md
|
||||
- to 0.10: upgrading/to-0.10.md
|
||||
- to 0.11: upgrading/to-0.11.md
|
||||
- to 0.12: upgrading/to-0.12.md
|
||||
- Troubleshooting:
|
||||
- Access problems: troubleshooting/access.md
|
||||
|
||||
repo_name: "BeryJu/passbook"
|
||||
repo_url: https://github.com/BeryJu/passbook
|
||||
theme:
|
||||
name: material
|
||||
logo: images/logo.svg
|
||||
favicon: images/logo.svg
|
||||
palette:
|
||||
scheme: slate
|
||||
primary: white
|
||||
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
permalink: "¶"
|
||||
- admonition
|
||||
- codehilite
|
||||
- pymdownx.betterem:
|
||||
smart_enable: all
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.magiclink
|
||||
- attr_list
|
||||
|
||||
plugins:
|
||||
- search
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.12.2-stable"
|
||||
__version__ = "0.12.11-stable"
|
||||
|
@ -50,15 +50,23 @@ class TaskViewSet(ViewSet):
|
||||
task = TaskInfo.by_name(pk)
|
||||
if not task:
|
||||
raise Http404
|
||||
task_module = import_module(task.task_call_module)
|
||||
task_func = getattr(task_module, task.task_call_func)
|
||||
task_func.delay(*task.task_call_args, **task.task_call_kwargs)
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Successfully re-scheduled Task %(name)s!" % {"name": task.task_name}),
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"successful": True,
|
||||
}
|
||||
)
|
||||
try:
|
||||
task_module = import_module(task.task_call_module)
|
||||
task_func = getattr(task_module, task.task_call_func)
|
||||
task_func.delay(*task.task_call_args, **task.task_call_kwargs)
|
||||
messages.success(
|
||||
self.request,
|
||||
_(
|
||||
"Successfully re-scheduled Task %(name)s!"
|
||||
% {"name": task.task_name}
|
||||
),
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"successful": True,
|
||||
}
|
||||
)
|
||||
except ImportError:
|
||||
# if we get an import error, the module path has probably changed
|
||||
task.delete()
|
||||
return Response({"successful": False})
|
||||
|
@ -46,11 +46,28 @@
|
||||
{% trans 'Providers' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:outposts' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}">
|
||||
{% trans 'Outposts' %}
|
||||
<li class="pf-c-nav__item pf-m-expanded">
|
||||
<a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Outposts' %}
|
||||
<span class="pf-c-nav__toggle">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
<section class="pf-c-nav__subnav">
|
||||
<ul class="pf-c-nav__simple-list">
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:outposts' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}">
|
||||
{% trans 'Outposts' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:outpost-service-connections' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:outpost-service-connections' 'passbook_admin:outpost-service-connections-create' 'passbook_admin:outpost-service-connections-update' 'passbook_admin:outpost-service-connections-delete' %}">
|
||||
{% trans 'Service Connections' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:property-mappings' %}"
|
||||
|
@ -0,0 +1,135 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load passbook_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="pf-icon-integration"></i>
|
||||
{% trans 'Outpost Service-Connections' %}
|
||||
</h1>
|
||||
<p>{% trans "Outpost Service-Connections define how passbook connects to external platforms to manage and deploy Outposts." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:outpost-service-connection-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Local?' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Status' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for sc in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<span>{{ sc.name }}</span>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ sc|verbose_name }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ sc.local|yesno:"Yes,No" }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{% if sc.state.healthy %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ sc.state.version }}
|
||||
{% else %}
|
||||
<i class="fas fa-times pf-m-danger"></i> {% trans 'Unhealthy' %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-service-connection-update' pk=sc.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-service-connection-delete' pk=sc.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-map-marker pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Outpost Service Connections.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any outposts." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no service connections exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:outpost-service-connection-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
@ -21,7 +21,7 @@
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Identifier' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Description' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Last Status' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Last Run' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Status' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Messages' %}</th>
|
||||
<th role="cell"></th>
|
||||
|
@ -7,10 +7,11 @@ from passbook.admin.views import (
|
||||
flows,
|
||||
groups,
|
||||
outposts,
|
||||
outposts_service_connections,
|
||||
overview,
|
||||
policies,
|
||||
policies_bindings,
|
||||
property_mapping,
|
||||
property_mappings,
|
||||
providers,
|
||||
sources,
|
||||
stages,
|
||||
@ -225,22 +226,22 @@ urlpatterns = [
|
||||
# Property Mappings
|
||||
path(
|
||||
"property-mappings/",
|
||||
property_mapping.PropertyMappingListView.as_view(),
|
||||
property_mappings.PropertyMappingListView.as_view(),
|
||||
name="property-mappings",
|
||||
),
|
||||
path(
|
||||
"property-mappings/create/",
|
||||
property_mapping.PropertyMappingCreateView.as_view(),
|
||||
property_mappings.PropertyMappingCreateView.as_view(),
|
||||
name="property-mapping-create",
|
||||
),
|
||||
path(
|
||||
"property-mappings/<uuid:pk>/update/",
|
||||
property_mapping.PropertyMappingUpdateView.as_view(),
|
||||
property_mappings.PropertyMappingUpdateView.as_view(),
|
||||
name="property-mapping-update",
|
||||
),
|
||||
path(
|
||||
"property-mappings/<uuid:pk>/delete/",
|
||||
property_mapping.PropertyMappingDeleteView.as_view(),
|
||||
property_mappings.PropertyMappingDeleteView.as_view(),
|
||||
name="property-mapping-delete",
|
||||
),
|
||||
# Users
|
||||
@ -312,6 +313,27 @@ urlpatterns = [
|
||||
outposts.OutpostDeleteView.as_view(),
|
||||
name="outpost-delete",
|
||||
),
|
||||
# Outpost Service Connections
|
||||
path(
|
||||
"outposts/service_connections/",
|
||||
outposts_service_connections.OutpostServiceConnectionListView.as_view(),
|
||||
name="outpost-service-connections",
|
||||
),
|
||||
path(
|
||||
"outposts/service_connections/create/",
|
||||
outposts_service_connections.OutpostServiceConnectionCreateView.as_view(),
|
||||
name="outpost-service-connection-create",
|
||||
),
|
||||
path(
|
||||
"outposts/service_connections/<uuid:pk>/update/",
|
||||
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
|
||||
name="outpost-service-connection-update",
|
||||
),
|
||||
path(
|
||||
"outposts/service_connections/<uuid:pk>/delete/",
|
||||
outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(),
|
||||
name="outpost-service-connection-delete",
|
||||
),
|
||||
# Tasks
|
||||
path(
|
||||
"tasks/",
|
||||
|
@ -147,5 +147,5 @@ class FlowExportView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
flow: Flow = self.get_object()
|
||||
exporter = FlowExporter(flow)
|
||||
response = JsonResponse(exporter.export(), encoder=DataclassEncoder, safe=False)
|
||||
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.json"'
|
||||
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.pbflow"'
|
||||
return response
|
||||
|
83
passbook/admin/views/outposts_service_connections.py
Normal file
83
passbook/admin/views/outposts_service_connections.py
Normal file
@ -0,0 +1,83 @@
|
||||
"""passbook OutpostServiceConnection administration"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import (
|
||||
BackSuccessUrlMixin,
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
SearchListMixin,
|
||||
UserPaginateListMixin,
|
||||
)
|
||||
from passbook.outposts.models import OutpostServiceConnection
|
||||
|
||||
|
||||
class OutpostServiceConnectionListView(
|
||||
LoginRequiredMixin,
|
||||
PermissionListMixin,
|
||||
UserPaginateListMixin,
|
||||
SearchListMixin,
|
||||
InheritanceListView,
|
||||
):
|
||||
"""Show list of all outpost-service-connections"""
|
||||
|
||||
model = OutpostServiceConnection
|
||||
permission_required = "passbook_outposts.add_outpostserviceconnection"
|
||||
template_name = "administration/outpost_service_connection/list.html"
|
||||
ordering = "pk"
|
||||
search_fields = ["pk", "name"]
|
||||
|
||||
|
||||
class OutpostServiceConnectionCreateView(
|
||||
SuccessMessageMixin,
|
||||
BackSuccessUrlMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new OutpostServiceConnection"""
|
||||
|
||||
model = OutpostServiceConnection
|
||||
permission_required = "passbook_outposts.add_outpostserviceconnection"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:outpost-service-connections")
|
||||
success_message = _("Successfully created OutpostServiceConnection")
|
||||
|
||||
|
||||
class OutpostServiceConnectionUpdateView(
|
||||
SuccessMessageMixin,
|
||||
BackSuccessUrlMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update outpostserviceconnection"""
|
||||
|
||||
model = OutpostServiceConnection
|
||||
permission_required = "passbook_outposts.change_outpostserviceconnection"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:outpost-service-connections")
|
||||
success_message = _("Successfully updated OutpostServiceConnection")
|
||||
|
||||
|
||||
class OutpostServiceConnectionDeleteView(
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete outpostserviceconnection"""
|
||||
|
||||
model = OutpostServiceConnection
|
||||
permission_required = "passbook_outposts.delete_outpostserviceconnection"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:outpost-service-connections")
|
||||
success_message = _("Successfully deleted OutpostServiceConnection")
|
@ -32,8 +32,8 @@ class ProviderListView(
|
||||
model = Provider
|
||||
permission_required = "passbook_core.add_provider"
|
||||
template_name = "administration/provider/list.html"
|
||||
ordering = "id"
|
||||
search_fields = ["id", "name"]
|
||||
ordering = "pk"
|
||||
search_fields = ["pk", "name"]
|
||||
|
||||
|
||||
class ProviderCreateView(
|
||||
|
@ -25,10 +25,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
|
||||
try:
|
||||
auth_credentials = b64decode(auth_credentials.encode()).decode()
|
||||
except UnicodeDecodeError:
|
||||
# TODO: Remove this workaround
|
||||
# temporary fallback for 0.11 to 0.12 upgrade
|
||||
# 0.11 and below proxy sends authorization header not base64 encoded
|
||||
pass
|
||||
return None
|
||||
# Accept credentials with username and without
|
||||
if ":" in auth_credentials:
|
||||
_, password = auth_credentials.split(":")
|
||||
|
@ -19,7 +19,11 @@ from passbook.core.api.tokens import TokenViewSet
|
||||
from passbook.core.api.users import UserViewSet
|
||||
from passbook.crypto.api import CertificateKeyPairViewSet
|
||||
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
|
||||
from passbook.outposts.api import OutpostViewSet
|
||||
from passbook.outposts.api import (
|
||||
DockerServiceConnectionViewSet,
|
||||
KubernetesServiceConnectionViewSet,
|
||||
OutpostViewSet,
|
||||
)
|
||||
from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet
|
||||
from passbook.policies.dummy.api import DummyPolicyViewSet
|
||||
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||
@ -29,7 +33,7 @@ from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
|
||||
from passbook.policies.password.api import PasswordPolicyViewSet
|
||||
from passbook.policies.reputation.api import ReputationPolicyViewSet
|
||||
from passbook.providers.oauth2.api import OAuth2ProviderViewSet, ScopeMappingViewSet
|
||||
from passbook.providers.proxy.api import OutpostConfigViewSet, ProxyProviderViewSet
|
||||
from passbook.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
|
||||
from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
|
||||
from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
||||
from passbook.sources.oauth.api import OAuthSourceViewSet
|
||||
@ -66,7 +70,14 @@ router.register("core/users", UserViewSet)
|
||||
router.register("core/tokens", TokenViewSet)
|
||||
|
||||
router.register("outposts/outposts", OutpostViewSet)
|
||||
router.register("outposts/proxy", OutpostConfigViewSet)
|
||||
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
|
||||
router.register(
|
||||
"outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet
|
||||
)
|
||||
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
|
||||
|
||||
router.register("flows/instances", FlowViewSet)
|
||||
router.register("flows/bindings", FlowStageBindingViewSet)
|
||||
|
||||
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
|
||||
|
||||
@ -114,9 +125,6 @@ router.register("stages/user_login", UserLoginStageViewSet)
|
||||
router.register("stages/user_logout", UserLogoutStageViewSet)
|
||||
router.register("stages/user_write", UserWriteStageViewSet)
|
||||
|
||||
router.register("flows/instances", FlowViewSet)
|
||||
router.register("flows/bindings", FlowStageBindingViewSet)
|
||||
|
||||
router.register("stages/dummy", DummyStageViewSet)
|
||||
router.register("policies/dummy", DummyPolicyViewSet)
|
||||
|
||||
|
@ -1,4 +1,12 @@
|
||||
"""passbook core tasks"""
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
|
||||
from boto3.exceptions import Boto3Error
|
||||
from botocore.exceptions import BotoCoreError, ClientError
|
||||
from dbbackup.db.exceptions import CommandConnectorError
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.core import management
|
||||
from django.utils.timezone import now
|
||||
from structlog import get_logger
|
||||
|
||||
@ -24,3 +32,32 @@ def clean_expired_models(self: MonitoredTask):
|
||||
LOGGER.debug("Deleted expired models", model=cls, amount=amount)
|
||||
messages.append(f"Deleted {amount} expired {cls._meta.verbose_name_plural}")
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def backup_database(self: MonitoredTask): # pragma: no cover
|
||||
"""Database backup"""
|
||||
self.result_timeout_hours = 25
|
||||
try:
|
||||
start = datetime.now()
|
||||
out = StringIO()
|
||||
management.call_command("dbbackup", quiet=True, stdout=out)
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL,
|
||||
[
|
||||
f"Successfully finished database backup {naturaltime(start)}",
|
||||
out.getvalue(),
|
||||
],
|
||||
)
|
||||
)
|
||||
LOGGER.info("Successfully backed up database.")
|
||||
except (
|
||||
IOError,
|
||||
BotoCoreError,
|
||||
ClientError,
|
||||
Boto3Error,
|
||||
PermissionError,
|
||||
CommandConnectorError,
|
||||
) as exc:
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
|
@ -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 %}
|
||||
|
@ -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">
|
||||
|
@ -54,7 +54,7 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
||||
@property
|
||||
def private_key(self) -> Optional[RSAPrivateKey]:
|
||||
"""Get python cryptography PrivateKey instance"""
|
||||
if not self._private_key:
|
||||
if not self._private_key and self._private_key != "":
|
||||
self._private_key = load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])),
|
||||
password=None,
|
||||
|
@ -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):
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Flow and Stage forms"""
|
||||
|
||||
from django import forms
|
||||
from django.core.validators import FileExtensionValidator
|
||||
from django.forms import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@ -50,12 +51,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(),
|
||||
}
|
||||
@ -64,7 +63,9 @@ class FlowStageBindingForm(forms.ModelForm):
|
||||
class FlowImportForm(forms.Form):
|
||||
"""Form used for flow importing"""
|
||||
|
||||
flow = forms.FileField()
|
||||
flow = forms.FileField(
|
||||
validators=[FileExtensionValidator(allowed_extensions=["pbflow"])]
|
||||
)
|
||||
|
||||
def clean_flow(self):
|
||||
"""Check if the flow is valid and rewind the file to the start"""
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.1.2 on 2020-10-20 12:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0014_auto_20200925_2332"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="flowstagebinding",
|
||||
name="re_evaluate_policies",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Evaluate policies when the Stage is present to the user.",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="flowstagebinding",
|
||||
name="evaluate_on_plan",
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
help_text="Evaluate policies during the Flow planning process. Disable this for input-based policies.",
|
||||
),
|
||||
),
|
||||
]
|
@ -154,15 +154,19 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
|
||||
target = models.ForeignKey("Flow", on_delete=models.CASCADE)
|
||||
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()
|
||||
|
||||
|
@ -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,
|
||||
|
@ -12,7 +12,7 @@ class TestTransferDocs(TransactionTestCase):
|
||||
"""Empty class, test methods are added dynamically"""
|
||||
|
||||
|
||||
def generic_view_tester(file_name: str) -> Callable:
|
||||
def pbflow_tester(file_name: str) -> Callable:
|
||||
"""This is used instead of subTest for better visibility"""
|
||||
|
||||
def tester(self: TestTransferDocs):
|
||||
@ -24,8 +24,6 @@ def generic_view_tester(file_name: str) -> Callable:
|
||||
return tester
|
||||
|
||||
|
||||
for flow_file in glob("docs/flow/examples/*.json"):
|
||||
for flow_file in glob("website/static/flows/*.pbflow"):
|
||||
method_name = Path(flow_file).stem.replace("-", "_").replace(".", "_")
|
||||
setattr(
|
||||
TestTransferDocs, f"test_flow_{method_name}", generic_view_tester(flow_file)
|
||||
)
|
||||
setattr(TestTransferDocs, f"test_flow_{method_name}", pbflow_tester(flow_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()
|
||||
|
@ -22,6 +22,7 @@ error_reporting:
|
||||
send_pii: false
|
||||
|
||||
passbook:
|
||||
avatars: gravatar # gravatar or none
|
||||
branding:
|
||||
title: passbook
|
||||
title_show: true
|
||||
@ -29,6 +30,6 @@ passbook:
|
||||
# Optionally add links to the footer on the login page
|
||||
footer_links:
|
||||
- name: Documentation
|
||||
href: https://passbook.beryju.org/docs/
|
||||
- name: passbook Website
|
||||
href: https://passbook.beryju.org/
|
||||
# - name: test
|
||||
# href: https://test
|
||||
|
@ -27,7 +27,7 @@ class BaseEvaluator:
|
||||
|
||||
def __init__(self):
|
||||
# update passbook/policies/expression/templates/policy/expression/form.html
|
||||
# update docs/policies/expression/index.md
|
||||
# update website/docs/policies/expression.md
|
||||
self._globals = {
|
||||
"regex_match": BaseEvaluator.expr_filter_regex_match,
|
||||
"regex_replace": BaseEvaluator.expr_filter_regex_replace,
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""passbook sentry integration"""
|
||||
from aioredis.errors import ConnectionClosedError, ReplyError
|
||||
from billiard.exceptions import WorkerLostError
|
||||
from botocore.client import ClientError
|
||||
from celery.exceptions import CeleryError
|
||||
@ -8,7 +9,7 @@ from django.db import InternalError, OperationalError, ProgrammingError
|
||||
from django_redis.exceptions import ConnectionInterrupted
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from redis.exceptions import ConnectionError as RedisConnectionError
|
||||
from redis.exceptions import RedisError
|
||||
from redis.exceptions import RedisError, ResponseError
|
||||
from rest_framework.exceptions import APIException
|
||||
from structlog import get_logger
|
||||
from websockets.exceptions import WebSocketException
|
||||
@ -23,26 +24,37 @@ class SentryIgnoredException(Exception):
|
||||
def before_send(event, hint):
|
||||
"""Check if error is database error, and ignore if so"""
|
||||
ignored_classes = (
|
||||
# Inbuilt types
|
||||
KeyboardInterrupt,
|
||||
ConnectionResetError,
|
||||
OSError,
|
||||
# Django DB Errors
|
||||
OperationalError,
|
||||
InternalError,
|
||||
ProgrammingError,
|
||||
ConnectionInterrupted,
|
||||
APIException,
|
||||
ConnectionResetError,
|
||||
RedisConnectionError,
|
||||
WorkerLostError,
|
||||
DisallowedHost,
|
||||
ConnectionResetError,
|
||||
KeyboardInterrupt,
|
||||
ClientError,
|
||||
ValidationError,
|
||||
OSError,
|
||||
# Redis errors
|
||||
RedisConnectionError,
|
||||
ConnectionInterrupted,
|
||||
RedisError,
|
||||
SentryIgnoredException,
|
||||
CeleryError,
|
||||
LDAPException,
|
||||
ResponseError,
|
||||
ReplyError,
|
||||
ConnectionClosedError,
|
||||
# websocket errors
|
||||
ChannelFull,
|
||||
WebSocketException,
|
||||
# rest_framework error
|
||||
APIException,
|
||||
# celery errors
|
||||
WorkerLostError,
|
||||
CeleryError,
|
||||
# S3 errors
|
||||
ClientError,
|
||||
# custom baseclass
|
||||
SentryIgnoredException,
|
||||
# ldap errors
|
||||
LDAPException,
|
||||
)
|
||||
if "exc_info" in hint:
|
||||
_, exc_value, _ = hint["exc_info"]
|
||||
|
@ -62,13 +62,17 @@ class TaskInfo:
|
||||
"""Get TaskInfo Object by name"""
|
||||
return cache.get(f"task_{name}")
|
||||
|
||||
def save(self):
|
||||
def delete(self):
|
||||
"""Delete task info from cache"""
|
||||
return cache.delete(f"task_{self.task_name}")
|
||||
|
||||
def save(self, timeout_hours=6):
|
||||
"""Save task into cache"""
|
||||
key = f"task_{self.task_name}"
|
||||
if self.result.uid:
|
||||
key += f"_{self.result.uid}"
|
||||
self.task_name += f"_{self.result.uid}"
|
||||
cache.set(key, self, timeout=6 * 60 * 60)
|
||||
cache.set(key, self, timeout=timeout_hours * 60 * 60)
|
||||
|
||||
|
||||
class MonitoredTask(Task):
|
||||
@ -79,10 +83,18 @@ class MonitoredTask(Task):
|
||||
|
||||
_result: TaskResult
|
||||
|
||||
_uid: Optional[str]
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.save_on_success = True
|
||||
self._uid = None
|
||||
self._result = TaskResult(status=TaskResultStatus.ERROR, messages=[])
|
||||
self.result_timeout_hours = 6
|
||||
|
||||
def set_uid(self, uid: str):
|
||||
"""Set UID, so in the case of an unexpected error its saved correctly"""
|
||||
self._uid = uid
|
||||
|
||||
def set_status(self, result: TaskResult):
|
||||
"""Set result for current run, will overwrite previous result."""
|
||||
@ -92,6 +104,8 @@ class MonitoredTask(Task):
|
||||
def after_return(
|
||||
self, status, retval, task_id, args: List[Any], kwargs: Dict[str, Any], einfo
|
||||
):
|
||||
if not self._result.uid:
|
||||
self._result.uid = self._uid
|
||||
if self.save_on_success:
|
||||
TaskInfo(
|
||||
task_name=self.__name__,
|
||||
@ -102,11 +116,13 @@ class MonitoredTask(Task):
|
||||
task_call_func=self.__name__,
|
||||
task_call_args=args,
|
||||
task_call_kwargs=kwargs,
|
||||
).save()
|
||||
).save(self.result_timeout_hours)
|
||||
return super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
if not self._result.uid:
|
||||
self._result.uid = self._uid
|
||||
TaskInfo(
|
||||
task_name=self.__name__,
|
||||
task_description=self.__doc__,
|
||||
@ -116,7 +132,7 @@ class MonitoredTask(Task):
|
||||
task_call_func=self.__name__,
|
||||
task_call_args=args,
|
||||
task_call_kwargs=kwargs,
|
||||
).save()
|
||||
).save(self.result_timeout_hours)
|
||||
return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
|
@ -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
|
||||
|
@ -2,7 +2,11 @@
|
||||
from rest_framework.serializers import JSONField, ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.outposts.models import Outpost
|
||||
from passbook.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
Outpost,
|
||||
)
|
||||
|
||||
|
||||
class OutpostSerializer(ModelSerializer):
|
||||
@ -13,7 +17,7 @@ class OutpostSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
model = Outpost
|
||||
fields = ["pk", "name", "providers", "_config"]
|
||||
fields = ["pk", "name", "providers", "service_connection", "_config"]
|
||||
|
||||
|
||||
class OutpostViewSet(ModelViewSet):
|
||||
@ -21,3 +25,35 @@ class OutpostViewSet(ModelViewSet):
|
||||
|
||||
queryset = Outpost.objects.all()
|
||||
serializer_class = OutpostSerializer
|
||||
|
||||
|
||||
class DockerServiceConnectionSerializer(ModelSerializer):
|
||||
"""DockerServiceConnection Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = DockerServiceConnection
|
||||
fields = ["pk", "name", "local", "url", "tls"]
|
||||
|
||||
|
||||
class DockerServiceConnectionViewSet(ModelViewSet):
|
||||
"""DockerServiceConnection Viewset"""
|
||||
|
||||
queryset = DockerServiceConnection.objects.all()
|
||||
serializer_class = DockerServiceConnectionSerializer
|
||||
|
||||
|
||||
class KubernetesServiceConnectionSerializer(ModelSerializer):
|
||||
"""KubernetesServiceConnection Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = KubernetesServiceConnection
|
||||
fields = ["pk", "name", "local", "kubeconfig"]
|
||||
|
||||
|
||||
class KubernetesServiceConnectionViewSet(ModelViewSet):
|
||||
"""KubernetesServiceConnection Viewset"""
|
||||
|
||||
queryset = KubernetesServiceConnection.objects.all()
|
||||
serializer_class = KubernetesServiceConnectionSerializer
|
||||
|
@ -1,7 +1,20 @@
|
||||
"""passbook outposts app config"""
|
||||
from importlib import import_module
|
||||
from os import R_OK, access
|
||||
from os.path import expanduser
|
||||
from pathlib import Path
|
||||
from socket import gethostname
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import yaml
|
||||
from django.apps import AppConfig
|
||||
from django.db import ProgrammingError
|
||||
from docker.constants import DEFAULT_UNIX_SOCKET
|
||||
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
|
||||
from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
|
||||
from structlog import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class PassbookOutpostConfig(AppConfig):
|
||||
@ -14,3 +27,48 @@ class PassbookOutpostConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
import_module("passbook.outposts.signals")
|
||||
try:
|
||||
self.init_local_connection()
|
||||
except ProgrammingError:
|
||||
pass
|
||||
|
||||
def init_local_connection(self):
|
||||
"""Check if local kubernetes or docker connections should be created"""
|
||||
from passbook.outposts.models import (
|
||||
KubernetesServiceConnection,
|
||||
DockerServiceConnection,
|
||||
)
|
||||
|
||||
if Path(SERVICE_TOKEN_FILENAME).exists():
|
||||
LOGGER.debug("Detected in-cluster Kubernetes Config")
|
||||
if not KubernetesServiceConnection.objects.filter(local=True).exists():
|
||||
LOGGER.debug("Created Service Connection for in-cluster")
|
||||
KubernetesServiceConnection.objects.create(
|
||||
name="Local Kubernetes Cluster", local=True, kubeconfig={}
|
||||
)
|
||||
# For development, check for the existence of a kubeconfig file
|
||||
kubeconfig_path = expanduser(KUBE_CONFIG_DEFAULT_LOCATION)
|
||||
if Path(kubeconfig_path).exists():
|
||||
LOGGER.debug("Detected kubeconfig")
|
||||
kubeconfig_local_name = f"k8s-{gethostname()}"
|
||||
if not KubernetesServiceConnection.objects.filter(
|
||||
name=kubeconfig_local_name
|
||||
).exists():
|
||||
LOGGER.debug("Creating kubeconfig Service Connection")
|
||||
with open(kubeconfig_path, "r") as _kubeconfig:
|
||||
KubernetesServiceConnection.objects.create(
|
||||
name=kubeconfig_local_name,
|
||||
kubeconfig=yaml.safe_load(_kubeconfig),
|
||||
)
|
||||
unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path
|
||||
socket = Path(unix_socket_path)
|
||||
if socket.exists() and access(socket, R_OK):
|
||||
LOGGER.debug("Detected local docker socket")
|
||||
if not DockerServiceConnection.objects.filter(local=True).exists():
|
||||
LOGGER.debug("Created Service Connection for docker")
|
||||
DockerServiceConnection.objects.create(
|
||||
name="Local Docker connection",
|
||||
local=True,
|
||||
url=unix_socket_path,
|
||||
tls=True,
|
||||
)
|
||||
|
@ -5,11 +5,11 @@ from structlog import get_logger
|
||||
from structlog.testing import capture_logs
|
||||
|
||||
from passbook.lib.sentry import SentryIgnoredException
|
||||
from passbook.outposts.models import Outpost
|
||||
from passbook.outposts.models import Outpost, OutpostServiceConnection
|
||||
|
||||
|
||||
class ControllerException(SentryIgnoredException):
|
||||
"""Exception raise when anything fails during controller run"""
|
||||
"""Exception raised when anything fails during controller run"""
|
||||
|
||||
|
||||
class BaseController:
|
||||
@ -18,9 +18,11 @@ class BaseController:
|
||||
deployment_ports: Dict[str, int]
|
||||
|
||||
outpost: Outpost
|
||||
connection: OutpostServiceConnection
|
||||
|
||||
def __init__(self, outpost: Outpost):
|
||||
def __init__(self, outpost: Outpost, connection: OutpostServiceConnection):
|
||||
self.outpost = outpost
|
||||
self.connection = connection
|
||||
self.logger = get_logger()
|
||||
self.deployment_ports = {}
|
||||
|
||||
@ -33,7 +35,7 @@ class BaseController:
|
||||
"""Call .up() but capture all log output and return it."""
|
||||
with capture_logs() as logs:
|
||||
self.up()
|
||||
return [f"{x['controller']}: {x['event']}" for x in logs]
|
||||
return [x["event"] for x in logs]
|
||||
|
||||
def down(self):
|
||||
"""Handler to delete everything we've created"""
|
||||
|
@ -3,14 +3,18 @@ from time import sleep
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from django.conf import settings
|
||||
from docker import DockerClient, from_env
|
||||
from docker import DockerClient
|
||||
from docker.errors import DockerException, NotFound
|
||||
from docker.models.containers import Container
|
||||
from yaml import safe_dump
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.outposts.controllers.base import BaseController, ControllerException
|
||||
from passbook.outposts.models import Outpost
|
||||
from passbook.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
Outpost,
|
||||
ServiceConnectionInvalid,
|
||||
)
|
||||
|
||||
|
||||
class DockerController(BaseController):
|
||||
@ -19,12 +23,16 @@ class DockerController(BaseController):
|
||||
client: DockerClient
|
||||
|
||||
container: Container
|
||||
connection: DockerServiceConnection
|
||||
|
||||
image_base = "beryju/passbook"
|
||||
|
||||
def __init__(self, outpost: Outpost) -> None:
|
||||
super().__init__(outpost)
|
||||
self.client = from_env()
|
||||
def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None:
|
||||
super().__init__(outpost, connection)
|
||||
try:
|
||||
self.client = connection.client()
|
||||
except ServiceConnectionInvalid as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def _get_labels(self) -> Dict[str, str]:
|
||||
return {}
|
||||
|
@ -36,7 +36,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
|
||||
def __init__(self, controller: "KubernetesController") -> None:
|
||||
super().__init__(controller)
|
||||
self.api = AppsV1Api()
|
||||
self.api = AppsV1Api(controller.client)
|
||||
self.outpost = self.controller.outpost
|
||||
|
||||
@property
|
||||
|
@ -23,7 +23,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
||||
|
||||
def __init__(self, controller: "KubernetesController") -> None:
|
||||
super().__init__(controller)
|
||||
self.api = CoreV1Api()
|
||||
self.api = CoreV1Api(controller.client)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -7,6 +7,7 @@ from passbook.outposts.controllers.k8s.base import (
|
||||
KubernetesObjectReconciler,
|
||||
NeedsUpdate,
|
||||
)
|
||||
from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from passbook.outposts.controllers.kubernetes import KubernetesController
|
||||
@ -17,7 +18,7 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
||||
|
||||
def __init__(self, controller: "KubernetesController") -> None:
|
||||
super().__init__(controller)
|
||||
self.api = CoreV1Api()
|
||||
self.api = CoreV1Api(controller.client)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@ -36,9 +37,10 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
||||
ports = []
|
||||
for port_name, port in self.controller.deployment_ports.items():
|
||||
ports.append(V1ServicePort(name=port_name, port=port))
|
||||
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
|
||||
return V1Service(
|
||||
metadata=meta,
|
||||
spec=V1ServiceSpec(ports=ports, selector=meta.labels, type="ClusterIP"),
|
||||
spec=V1ServiceSpec(ports=ports, selector=selector_labels, type="ClusterIP"),
|
||||
)
|
||||
|
||||
def create(self, reference: V1Service):
|
||||
|
@ -3,8 +3,8 @@ from io import StringIO
|
||||
from typing import Dict, List, Type
|
||||
|
||||
from kubernetes.client import OpenApiException
|
||||
from kubernetes.config import load_incluster_config, load_kube_config
|
||||
from kubernetes.config.config_exception import ConfigException
|
||||
from kubernetes.client.api_client import ApiClient
|
||||
from structlog.testing import capture_logs
|
||||
from yaml import dump_all
|
||||
|
||||
from passbook.outposts.controllers.base import BaseController, ControllerException
|
||||
@ -12,7 +12,7 @@ from passbook.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||
from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
from passbook.outposts.controllers.k8s.secret import SecretReconciler
|
||||
from passbook.outposts.controllers.k8s.service import ServiceReconciler
|
||||
from passbook.outposts.models import Outpost
|
||||
from passbook.outposts.models import KubernetesServiceConnection, Outpost
|
||||
|
||||
|
||||
class KubernetesController(BaseController):
|
||||
@ -21,12 +21,14 @@ class KubernetesController(BaseController):
|
||||
reconcilers: Dict[str, Type[KubernetesObjectReconciler]]
|
||||
reconcile_order: List[str]
|
||||
|
||||
def __init__(self, outpost: Outpost) -> None:
|
||||
super().__init__(outpost)
|
||||
try:
|
||||
load_incluster_config()
|
||||
except ConfigException:
|
||||
load_kube_config()
|
||||
client: ApiClient
|
||||
connection: KubernetesServiceConnection
|
||||
|
||||
def __init__(
|
||||
self, outpost: Outpost, connection: KubernetesServiceConnection
|
||||
) -> None:
|
||||
super().__init__(outpost, connection)
|
||||
self.client = connection.client()
|
||||
self.reconcilers = {
|
||||
"secret": SecretReconciler,
|
||||
"deployment": DeploymentReconciler,
|
||||
@ -43,6 +45,18 @@ class KubernetesController(BaseController):
|
||||
except OpenApiException as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def up_with_logs(self) -> List[str]:
|
||||
try:
|
||||
all_logs = []
|
||||
for reconcile_key in self.reconcile_order:
|
||||
with capture_logs() as logs:
|
||||
reconciler = self.reconcilers[reconcile_key](self)
|
||||
reconciler.up()
|
||||
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
||||
return all_logs
|
||||
except OpenApiException as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def down(self):
|
||||
try:
|
||||
for reconcile_key in self.reconcile_order:
|
||||
|
@ -4,7 +4,12 @@ from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.admin.fields import CodeMirrorWidget, YAMLField
|
||||
from passbook.outposts.models import Outpost
|
||||
from passbook.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
Outpost,
|
||||
OutpostServiceConnection,
|
||||
)
|
||||
from passbook.providers.proxy.models import ProxyProvider
|
||||
|
||||
|
||||
@ -14,6 +19,9 @@ class OutpostForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["providers"].queryset = ProxyProvider.objects.all()
|
||||
self.fields[
|
||||
"service_connection"
|
||||
].queryset = OutpostServiceConnection.objects.select_subclasses()
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -21,7 +29,7 @@ class OutpostForm(forms.ModelForm):
|
||||
fields = [
|
||||
"name",
|
||||
"type",
|
||||
"deployment_type",
|
||||
"service_connection",
|
||||
"providers",
|
||||
"_config",
|
||||
]
|
||||
@ -33,3 +41,40 @@ class OutpostForm(forms.ModelForm):
|
||||
"_config": YAMLField,
|
||||
}
|
||||
labels = {"_config": _("Configuration")}
|
||||
|
||||
|
||||
class DockerServiceConnectionForm(forms.ModelForm):
|
||||
"""Docker service-connection form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = DockerServiceConnection
|
||||
fields = ["name", "local", "url", "tls"]
|
||||
widgets = {
|
||||
"name": forms.TextInput,
|
||||
"url": forms.TextInput,
|
||||
}
|
||||
labels = {
|
||||
"url": _("URL"),
|
||||
"tls": _("TLS"),
|
||||
}
|
||||
|
||||
|
||||
class KubernetesServiceConnectionForm(forms.ModelForm):
|
||||
"""Kubernetes service-connection form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = KubernetesServiceConnection
|
||||
fields = [
|
||||
"name",
|
||||
"local",
|
||||
"kubeconfig",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput,
|
||||
"kubeconfig": CodeMirrorWidget,
|
||||
}
|
||||
field_classes = {
|
||||
"kubeconfig": YAMLField,
|
||||
}
|
||||
|
@ -6,13 +6,20 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def fix_missing_token_identifier(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
User = apps.get_model("passbook_core", "User")
|
||||
Token = apps.get_model("passbook_core", "Token")
|
||||
from passbook.outposts.models import Outpost
|
||||
|
||||
for outpost in Outpost.objects.using(schema_editor.connection.alias).all():
|
||||
token = outpost.token
|
||||
if token.identifier != outpost.token_identifier:
|
||||
token.identifier = outpost.token_identifier
|
||||
token.save()
|
||||
for outpost in (
|
||||
Outpost.objects.using(schema_editor.connection.alias).all().only("pk")
|
||||
):
|
||||
user_identifier = outpost.user_identifier
|
||||
user = User.objects.get(username=user_identifier)
|
||||
tokens = Token.objects.filter(user=user)
|
||||
for token in tokens:
|
||||
if token.identifier != outpost.token_identifier:
|
||||
token.identifier = outpost.token_identifier
|
||||
token.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
172
passbook/outposts/migrations/0010_service_connection.py
Normal file
172
passbook/outposts/migrations/0010_service_connection.py
Normal file
@ -0,0 +1,172 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-04 09:11
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.apps.registry import Apps
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import passbook.lib.models
|
||||
|
||||
|
||||
def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
Outpost = apps.get_model("passbook_outposts", "Outpost")
|
||||
DockerServiceConnection = apps.get_model(
|
||||
"passbook_outposts", "DockerServiceConnection"
|
||||
)
|
||||
KubernetesServiceConnection = apps.get_model(
|
||||
"passbook_outposts", "KubernetesServiceConnection"
|
||||
)
|
||||
from passbook.outposts.apps import PassbookOutpostConfig
|
||||
|
||||
# Ensure that local connection have been created
|
||||
PassbookOutpostConfig.init_local_connection(None)
|
||||
|
||||
docker = DockerServiceConnection.objects.filter(local=True).first()
|
||||
k8s = KubernetesServiceConnection.objects.filter(local=True).first()
|
||||
|
||||
try:
|
||||
for outpost in (
|
||||
Outpost.objects.using(db_alias).all().exclude(deployment_type="custom")
|
||||
):
|
||||
if outpost.deployment_type == "kubernetes":
|
||||
outpost.service_connection = k8s
|
||||
elif outpost.deployment_type == "docker":
|
||||
outpost.service_connection = docker
|
||||
outpost.save()
|
||||
except FieldError:
|
||||
# This is triggered during e2e tests when this function is called on an already-upgraded
|
||||
# schema
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_outposts", "0009_fix_missing_token_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="OutpostServiceConnection",
|
||||
fields=[
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("name", models.TextField()),
|
||||
(
|
||||
"local",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="If enabled, use the local connection. Required Docker socket/Kubernetes Integration",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="DockerServiceConnection",
|
||||
fields=[
|
||||
(
|
||||
"outpostserviceconnection_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="passbook_outposts.outpostserviceconnection",
|
||||
),
|
||||
),
|
||||
("url", models.TextField()),
|
||||
("tls", models.BooleanField()),
|
||||
],
|
||||
bases=("passbook_outposts.outpostserviceconnection",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="KubernetesServiceConnection",
|
||||
fields=[
|
||||
(
|
||||
"outpostserviceconnection_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="passbook_outposts.outpostserviceconnection",
|
||||
),
|
||||
),
|
||||
("kubeconfig", models.JSONField()),
|
||||
],
|
||||
bases=("passbook_outposts.outpostserviceconnection",),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="outpost",
|
||||
name="service_connection",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Select Service-Connection passbook should use to manage this outpost. Leave empty if passbook should not handle the deployment.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="passbook_outposts.outpostserviceconnection",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_to_service_connection),
|
||||
migrations.RemoveField(
|
||||
model_name="outpost",
|
||||
name="deployment_type",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="dockerserviceconnection",
|
||||
options={
|
||||
"verbose_name": "Docker Service-Connection",
|
||||
"verbose_name_plural": "Docker Service-Connections",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="kubernetesserviceconnection",
|
||||
options={
|
||||
"verbose_name": "Kubernetes Service-Connection",
|
||||
"verbose_name_plural": "Kubernetes Service-Connections",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="outpost",
|
||||
name="service_connection",
|
||||
field=passbook.lib.models.InheritanceForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Select Service-Connection passbook should use to manage this outpost. Leave empty if passbook should not handle the deployment.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="passbook_outposts.outpostserviceconnection",
|
||||
),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="outpostserviceconnection",
|
||||
options={
|
||||
"verbose_name": "Outpost Service-Connection",
|
||||
"verbose_name_plural": "Outpost Service-Connections",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="kubernetesserviceconnection",
|
||||
name="kubeconfig",
|
||||
field=models.JSONField(
|
||||
default=None,
|
||||
help_text="Paste your kubeconfig here. passbook will automatically use the currently selected context.",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@ -1,28 +1,46 @@
|
||||
"""Outpost models"""
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Dict, Iterable, List, Optional, Union
|
||||
from typing import Dict, Iterable, List, Optional, Type, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from dacite import from_dict
|
||||
from django.core.cache import cache
|
||||
from django.db import models, transaction
|
||||
from django.db.models.base import Model
|
||||
from django.forms.models import ModelForm
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from docker.client import DockerClient
|
||||
from docker.errors import DockerException
|
||||
from guardian.models import UserObjectPermission
|
||||
from guardian.shortcuts import assign_perm
|
||||
from kubernetes.client import VersionApi, VersionInfo
|
||||
from kubernetes.client.api_client import ApiClient
|
||||
from kubernetes.client.configuration import Configuration
|
||||
from kubernetes.client.exceptions import OpenApiException
|
||||
from kubernetes.config.config_exception import ConfigException
|
||||
from kubernetes.config.incluster_config import load_incluster_config
|
||||
from kubernetes.config.kube_config import load_kube_config_from_dict
|
||||
from model_utils.managers import InheritanceManager
|
||||
from packaging.version import LegacyVersion, Version, parse
|
||||
from urllib3.exceptions import HTTPError
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.core.models import Provider, Token, TokenIntents, User
|
||||
from passbook.lib.config import CONFIG
|
||||
from passbook.lib.models import InheritanceForeignKey
|
||||
from passbook.lib.sentry import SentryIgnoredException
|
||||
from passbook.lib.utils.template import render_to_string
|
||||
|
||||
OUR_VERSION = parse(__version__)
|
||||
OUTPOST_HELLO_INTERVAL = 10
|
||||
|
||||
|
||||
class ServiceConnectionInvalid(SentryIgnoredException):
|
||||
""""Exception raised when a Service Connection has invalid parameters"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutpostConfig:
|
||||
"""Configuration an outpost uses to configure it self"""
|
||||
@ -60,19 +78,158 @@ class OutpostType(models.TextChoices):
|
||||
PROXY = "proxy"
|
||||
|
||||
|
||||
class OutpostDeploymentType(models.TextChoices):
|
||||
"""Deployment types that are managed through passbook"""
|
||||
|
||||
KUBERNETES = "kubernetes"
|
||||
DOCKER = "docker"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
def default_outpost_config():
|
||||
"""Get default outpost config"""
|
||||
return asdict(OutpostConfig(passbook_host=""))
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutpostServiceConnectionState:
|
||||
"""State of an Outpost Service Connection"""
|
||||
|
||||
version: str
|
||||
healthy: bool
|
||||
|
||||
|
||||
class OutpostServiceConnection(models.Model):
|
||||
"""Connection details for an Outpost Controller, like Docker or Kubernetes"""
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
||||
name = models.TextField()
|
||||
|
||||
local = models.BooleanField(
|
||||
default=False,
|
||||
unique=True,
|
||||
help_text=_(
|
||||
(
|
||||
"If enabled, use the local connection. Required Docker "
|
||||
"socket/Kubernetes Integration"
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
def state(self) -> OutpostServiceConnectionState:
|
||||
"""Get state of service connection"""
|
||||
state_key = f"outpost_service_connection_{self.pk.hex}"
|
||||
state = cache.get(state_key, None)
|
||||
if not state:
|
||||
state = self._get_state()
|
||||
cache.set(state_key, state, timeout=0)
|
||||
return state
|
||||
|
||||
def _get_state(self) -> OutpostServiceConnectionState:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
"""Return Form class used to edit this object"""
|
||||
raise NotImplementedError
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Outpost Service-Connection")
|
||||
verbose_name_plural = _("Outpost Service-Connections")
|
||||
|
||||
|
||||
class DockerServiceConnection(OutpostServiceConnection):
|
||||
"""Service Connection to a Docker endpoint"""
|
||||
|
||||
url = models.TextField()
|
||||
tls = models.BooleanField()
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.outposts.forms import DockerServiceConnectionForm
|
||||
|
||||
return DockerServiceConnectionForm
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Docker Service-Connection {self.name}"
|
||||
|
||||
def client(self) -> DockerClient:
|
||||
"""Get DockerClient"""
|
||||
try:
|
||||
client = None
|
||||
if self.local:
|
||||
client = DockerClient.from_env()
|
||||
else:
|
||||
client = DockerClient(
|
||||
base_url=self.url,
|
||||
tls=self.tls,
|
||||
)
|
||||
client.containers.list()
|
||||
except DockerException as exc:
|
||||
raise ServiceConnectionInvalid from exc
|
||||
return client
|
||||
|
||||
def _get_state(self) -> OutpostServiceConnectionState:
|
||||
try:
|
||||
client = self.client()
|
||||
return OutpostServiceConnectionState(
|
||||
version=client.info()["ServerVersion"], healthy=True
|
||||
)
|
||||
except ServiceConnectionInvalid:
|
||||
return OutpostServiceConnectionState(version="", healthy=False)
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Docker Service-Connection")
|
||||
verbose_name_plural = _("Docker Service-Connections")
|
||||
|
||||
|
||||
class KubernetesServiceConnection(OutpostServiceConnection):
|
||||
"""Service Connection to a Kubernetes cluster"""
|
||||
|
||||
kubeconfig = models.JSONField(
|
||||
help_text=_(
|
||||
(
|
||||
"Paste your kubeconfig here. passbook will automatically use "
|
||||
"the currently selected context."
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.outposts.forms import KubernetesServiceConnectionForm
|
||||
|
||||
return KubernetesServiceConnectionForm
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Kubernetes Service-Connection {self.name}"
|
||||
|
||||
def _get_state(self) -> OutpostServiceConnectionState:
|
||||
try:
|
||||
client = self.client()
|
||||
api_instance = VersionApi(client)
|
||||
version: VersionInfo = api_instance.get_code()
|
||||
return OutpostServiceConnectionState(
|
||||
version=version.git_version, healthy=True
|
||||
)
|
||||
except (OpenApiException, HTTPError):
|
||||
return OutpostServiceConnectionState(version="", healthy=False)
|
||||
|
||||
def client(self) -> ApiClient:
|
||||
"""Get Kubernetes client configured from kubeconfig"""
|
||||
config = Configuration()
|
||||
try:
|
||||
if self.local:
|
||||
load_incluster_config(client_configuration=config)
|
||||
else:
|
||||
load_kube_config_from_dict(self.kubeconfig, client_configuration=config)
|
||||
return ApiClient(config)
|
||||
except ConfigException as exc:
|
||||
raise ServiceConnectionInvalid from exc
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Kubernetes Service-Connection")
|
||||
verbose_name_plural = _("Kubernetes Service-Connections")
|
||||
|
||||
|
||||
class Outpost(models.Model):
|
||||
"""Outpost instance which manages a service user and token"""
|
||||
|
||||
@ -80,13 +237,20 @@ class Outpost(models.Model):
|
||||
name = models.TextField()
|
||||
|
||||
type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY)
|
||||
deployment_type = models.TextField(
|
||||
choices=OutpostDeploymentType.choices,
|
||||
default=OutpostDeploymentType.CUSTOM,
|
||||
service_connection = InheritanceForeignKey(
|
||||
OutpostServiceConnection,
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"Select between passbook-managed deployment types or a custom deployment."
|
||||
(
|
||||
"Select Service-Connection passbook should use to manage this outpost. "
|
||||
"Leave empty if passbook should not handle the deployment."
|
||||
)
|
||||
),
|
||||
on_delete=models.SET_DEFAULT,
|
||||
)
|
||||
|
||||
_config = models.JSONField(default=default_outpost_config)
|
||||
|
||||
providers = models.ManyToManyField(Provider)
|
||||
@ -111,12 +275,17 @@ class Outpost(models.Model):
|
||||
"""Get outpost's health status"""
|
||||
return OutpostState.for_outpost(self)
|
||||
|
||||
@property
|
||||
def user_identifier(self):
|
||||
"""Username for service user"""
|
||||
return f"pb-outpost-{self.uuid.hex}"
|
||||
|
||||
@property
|
||||
def user(self) -> User:
|
||||
"""Get/create user with access to all required objects"""
|
||||
users = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}")
|
||||
users = User.objects.filter(username=self.user_identifier)
|
||||
if not users.exists():
|
||||
user: User = User.objects.create(username=f"pb-outpost-{self.uuid.hex}")
|
||||
user: User = User.objects.create(username=self.user_identifier)
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
else:
|
||||
|
@ -7,4 +7,9 @@ CELERY_BEAT_SCHEDULE = {
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
},
|
||||
"outposts_service_connection_check": {
|
||||
"task": "passbook.outposts.tasks.outpost_service_connection_monitor",
|
||||
"schedule": crontab(minute=0, hour="*"),
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
},
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ from typing import Any
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.core.cache import cache
|
||||
from django.db.models.base import Model
|
||||
from django.utils.text import slugify
|
||||
from structlog import get_logger
|
||||
@ -11,9 +12,11 @@ from passbook.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||
from passbook.lib.utils.reflection import path_to_class
|
||||
from passbook.outposts.controllers.base import ControllerException
|
||||
from passbook.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
Outpost,
|
||||
OutpostDeploymentType,
|
||||
OutpostModel,
|
||||
OutpostServiceConnection,
|
||||
OutpostState,
|
||||
OutpostType,
|
||||
)
|
||||
@ -27,33 +30,48 @@ LOGGER = get_logger()
|
||||
@CELERY_APP.task()
|
||||
def outpost_controller_all():
|
||||
"""Launch Controller for all Outposts which support it"""
|
||||
for outpost in Outpost.objects.exclude(
|
||||
deployment_type=OutpostDeploymentType.CUSTOM
|
||||
):
|
||||
for outpost in Outpost.objects.exclude(service_connection=None):
|
||||
outpost_controller.delay(outpost.pk.hex)
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def outpost_service_connection_state(state_pk: Any):
|
||||
"""Update cached state of a service connection"""
|
||||
connection: OutpostServiceConnection = (
|
||||
OutpostServiceConnection.objects.filter(pk=state_pk).select_subclasses().first()
|
||||
)
|
||||
cache.delete(f"outpost_service_connection_{connection.pk.hex}")
|
||||
_ = connection.state
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def outpost_service_connection_monitor(self: MonitoredTask):
|
||||
"""Regularly check the state of Outpost Service Connections"""
|
||||
for connection in OutpostServiceConnection.objects.select_subclasses():
|
||||
cache.delete(f"outpost_service_connection_{connection.pk.hex}")
|
||||
_ = connection.state
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def outpost_controller(self: MonitoredTask, outpost_pk: str):
|
||||
"""Launch controller deployment of Outpost"""
|
||||
"""Create/update/monitor the deployment of an Outpost"""
|
||||
logs = []
|
||||
outpost: Outpost = Outpost.objects.get(pk=outpost_pk)
|
||||
self.set_uid(slugify(outpost.name))
|
||||
try:
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
if outpost.deployment_type == OutpostDeploymentType.KUBERNETES:
|
||||
logs = ProxyKubernetesController(outpost).up_with_logs()
|
||||
if outpost.deployment_type == OutpostDeploymentType.DOCKER:
|
||||
logs = ProxyDockerController(outpost).up_with_logs()
|
||||
service_connection = outpost.service_connection
|
||||
if isinstance(service_connection, DockerServiceConnection):
|
||||
logs = ProxyDockerController(outpost, service_connection).up_with_logs()
|
||||
if isinstance(service_connection, KubernetesServiceConnection):
|
||||
logs = ProxyKubernetesController(
|
||||
outpost, service_connection
|
||||
).up_with_logs()
|
||||
except ControllerException as exc:
|
||||
self.set_status(
|
||||
TaskResult(TaskResultStatus.ERROR, uid=slugify(outpost.name)).with_error(
|
||||
exc
|
||||
)
|
||||
)
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
else:
|
||||
self.set_status(
|
||||
TaskResult(TaskResultStatus.SUCCESSFUL, logs, uid=slugify(outpost.name))
|
||||
)
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, logs))
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
@ -61,10 +79,11 @@ def outpost_pre_delete(outpost_pk: str):
|
||||
"""Delete outpost objects before deleting the DB Object"""
|
||||
outpost = Outpost.objects.get(pk=outpost_pk)
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
if outpost.deployment_type == OutpostDeploymentType.KUBERNETES:
|
||||
ProxyKubernetesController(outpost).down()
|
||||
if outpost.deployment_type == OutpostDeploymentType.DOCKER:
|
||||
ProxyDockerController(outpost).down()
|
||||
service_connection = outpost.service_connection
|
||||
if isinstance(service_connection, DockerServiceConnection):
|
||||
ProxyDockerController(outpost, service_connection).down()
|
||||
if isinstance(service_connection, KubernetesServiceConnection):
|
||||
ProxyKubernetesController(outpost, service_connection).down()
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
@ -94,6 +113,10 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
||||
outpost_send_update(instance)
|
||||
return
|
||||
|
||||
if isinstance(instance, OutpostServiceConnection):
|
||||
LOGGER.debug("triggering ServiceConnection state update", instance=instance)
|
||||
outpost_service_connection_state.delay(instance.pk)
|
||||
|
||||
for field in instance._meta.get_fields():
|
||||
# Each field is checked if it has a `related_model` attribute (when ForeginKeys or M2Ms)
|
||||
# are used, and if it has a value
|
||||
@ -128,6 +151,9 @@ def outpost_send_update(model_instace: Model):
|
||||
|
||||
def _outpost_single_update(outpost: Outpost, layer=None):
|
||||
"""Update outpost instances connected to a single outpost"""
|
||||
# Ensure token again, because this function is called when anything related to an
|
||||
# OutpostModel is saved, so we can be sure permissions are right
|
||||
_ = outpost.token
|
||||
if not layer: # pragma: no cover
|
||||
layer = get_channel_layer()
|
||||
for state in OutpostState.for_outpost(outpost):
|
||||
|
@ -12,7 +12,7 @@
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Outpost Deployment Info' %}</h1>
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body" id="modal-description">
|
||||
<p><a href="https://passbook.beryju.org/outposts/outposts/#deploy">{% trans 'View deployment documentation' %}</a></p>
|
||||
<p><a href="https://passbook.beryju.org/docs/outposts/outposts/#deploy">{% trans 'View deployment documentation' %}</a></p>
|
||||
<form class="pf-c-form">
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||
@ -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>
|
||||
|
@ -11,7 +11,7 @@ from passbook.flows.models import Flow
|
||||
from passbook.outposts.controllers.k8s.base import NeedsUpdate
|
||||
from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
from passbook.outposts.controllers.kubernetes import KubernetesController
|
||||
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
|
||||
from passbook.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
|
||||
from passbook.providers.proxy.models import ProxyProvider
|
||||
|
||||
|
||||
@ -29,7 +29,6 @@ class OutpostTests(TestCase):
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name="test",
|
||||
type=OutpostType.PROXY,
|
||||
deployment_type=OutpostDeploymentType.CUSTOM,
|
||||
)
|
||||
|
||||
# Before we add a provider, the user should only have access to the outpost
|
||||
@ -79,17 +78,18 @@ class OutpostKubernetesTests(TestCase):
|
||||
external_host="http://localhost",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
)
|
||||
self.service_connection = KubernetesServiceConnection.objects.first()
|
||||
self.outpost: Outpost = Outpost.objects.create(
|
||||
name="test",
|
||||
type=OutpostType.PROXY,
|
||||
deployment_type=OutpostDeploymentType.KUBERNETES,
|
||||
service_connection=self.service_connection,
|
||||
)
|
||||
self.outpost.providers.add(self.provider)
|
||||
self.outpost.save()
|
||||
|
||||
def test_deployment_reconciler(self):
|
||||
"""test that deployment requires update"""
|
||||
controller = KubernetesController(self.outpost)
|
||||
controller = KubernetesController(self.outpost, self.service_connection)
|
||||
deployment_reconciler = DeploymentReconciler(controller)
|
||||
|
||||
self.assertIsNotNone(deployment_reconciler.retrieve())
|
||||
|
@ -12,7 +12,12 @@ from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.outposts.controllers.docker import DockerController
|
||||
from passbook.outposts.models import Outpost, OutpostType
|
||||
from passbook.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
Outpost,
|
||||
OutpostType,
|
||||
)
|
||||
from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -35,7 +40,7 @@ class DockerComposeView(LoginRequiredMixin, View):
|
||||
)
|
||||
manifest = ""
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
controller = DockerController(outpost)
|
||||
controller = DockerController(outpost, DockerServiceConnection())
|
||||
manifest = controller.get_static_deployment()
|
||||
|
||||
return HttpResponse(manifest, content_type="text/vnd.yaml")
|
||||
@ -53,7 +58,9 @@ class KubernetesManifestView(LoginRequiredMixin, View):
|
||||
)
|
||||
manifest = ""
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
controller = ProxyKubernetesController(outpost)
|
||||
controller = ProxyKubernetesController(
|
||||
outpost, KubernetesServiceConnection()
|
||||
)
|
||||
manifest = controller.get_static_deployment()
|
||||
|
||||
return HttpResponse(manifest, content_type="text/vnd.yaml")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user