Compare commits
94 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| 895e7d7393 | |||
| 3beca0574d | |||
| 990f5f0a43 | |||
| 97ce143efe | |||
| cbbe174fd8 | |||
| da3c640343 | |||
| 4b39c71de0 | |||
| 818f417fd8 | |||
| f1ccef7f6a | |||
| 6187436518 | |||
| 9559ee7cb9 | |||
| 72e9c4e6fa | |||
| 97b8a025b3 | |||
| ea9687c30b | |||
| 0a5e14a352 | |||
| 0325847c22 | |||
| 491dcc1159 | |||
| 6292049c74 | |||
| 1e97af772f | |||
| 5c622cd4d2 | |||
| c4de808c4e | |||
| 8c604d225b | |||
| c7daadfb18 | |||
| 683968c96e | |||
| c94added99 | |||
| 61c00e5b39 | |||
| 566ebae065 | |||
| 9b62a6403b | |||
| 8c465b2026 | |||
| 6b7da71aa8 | |||
| e95bbfab9a | |||
| e401575894 | |||
| 6428801270 | |||
| 3e13c13619 | |||
| 92f79eb30e | |||
| e7472de4bf | |||
| 494950ac65 | |||
| 4d51295db2 | |||
| 3bbded3555 | |||
| b3262e2a82 | |||
| 40614a65fc | |||
| 3cf558d594 | |||
| 812cc0d2f1 | |||
| e21ed92848 | |||
| 5184c4b7ef | |||
| 2c07859b68 | |||
| ae6304c05e | |||
| 501683e3cb | |||
| cc8afa8706 | |||
| 17a9e02bc0 | |||
| 6a669992a8 | |||
| 7ea5c22b6c | |||
| b11d6a5891 | |||
| 49830367a7 | |||
| e69ca5a229 | |||
| a57d21f5e8 | |||
| c7026407c6 | |||
| 69eecd6b60 | |||
| 810f10edfe | |||
| 1c57128f11 | |||
| 82eade3eb1 | |||
| 56a9dcc88d | |||
| fe70d80189 | |||
| e97e22c58a | |||
| bb4e39aab6 | |||
| a8744f443c | |||
| 7fe9b8f0b4 | |||
| 696aa7e5f6 | |||
| e1d82aee1d | |||
| 151374f565 | |||
| bebeff9f7f | |||
| 8b99afa34d | |||
| b317852e8a | |||
| 24ae35c35a | |||
| 8e6bb48227 | |||
| 7a4e8af1ae | |||
| 0161205c82 | |||
| ca0ba85023 | |||
| c2ebaa7f64 | |||
| 23cccebb96 | |||
| 3f5d30e6fe | |||
| ca735349f9 | |||
| 25ce8c6dc7 | |||
| 081ac0bcdb | |||
| 8a07b349ee | |||
| b3468bc265 | |||
| 4edfad869f | |||
| 404f5d7912 | |||
| 8bea99a953 | |||
| 0b0ba33dce | |||
| e3627b2cd9 | |||
| 37fac3ae00 | |||
| 17a90adf3e | |||
| 7c3590f8ef |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.10.0-rc3
|
current_version = 0.10.3-stable
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||||
@ -15,10 +15,10 @@ values =
|
|||||||
beta
|
beta
|
||||||
stable
|
stable
|
||||||
|
|
||||||
[bumpversion:file:README.md]
|
|
||||||
|
|
||||||
[bumpversion:file:docs/installation/docker-compose.md]
|
[bumpversion:file:docs/installation/docker-compose.md]
|
||||||
|
|
||||||
|
[bumpversion:file:docs/installation/kubernetes.md]
|
||||||
|
|
||||||
[bumpversion:file:docker-compose.yml]
|
[bumpversion:file:docker-compose.yml]
|
||||||
|
|
||||||
[bumpversion:file:helm/values.yaml]
|
[bumpversion:file:helm/values.yaml]
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
[run]
|
[run]
|
||||||
source = passbook
|
source = passbook
|
||||||
|
relative_files = true
|
||||||
omit =
|
omit =
|
||||||
*/asgi.py
|
*/asgi.py
|
||||||
manage.py
|
manage.py
|
||||||
|
|||||||
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
@ -1,6 +1,8 @@
|
|||||||
name: passbook-release
|
name: passbook-on-release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release
|
release:
|
||||||
|
types: [published, created]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Build
|
# Build
|
||||||
@ -16,17 +18,26 @@ jobs:
|
|||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
run: docker build
|
run: docker build
|
||||||
--no-cache
|
--no-cache
|
||||||
-t beryju/passbook:0.10.0-rc3
|
-t beryju/passbook:0.10.3-stable
|
||||||
-t beryju/passbook:latest
|
-t beryju/passbook:latest
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook:0.10.0-rc3
|
run: docker push beryju/passbook:0.10.3-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook:latest
|
run: docker push beryju/passbook:latest
|
||||||
build-proxy:
|
build-proxy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: "^1.15"
|
||||||
|
- name: prepare go api client
|
||||||
|
run: |
|
||||||
|
cd proxy
|
||||||
|
go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
||||||
|
swagger generate client -f ../swagger.yaml -A passbook -t pkg/
|
||||||
|
go build -v .
|
||||||
- name: Docker Login Registry
|
- name: Docker Login Registry
|
||||||
env:
|
env:
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
@ -37,11 +48,11 @@ jobs:
|
|||||||
cd proxy
|
cd proxy
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t beryju/passbook-proxy:0.10.0-rc3 \
|
-t beryju/passbook-proxy:0.10.3-stable \
|
||||||
-t beryju/passbook-proxy:latest \
|
-t beryju/passbook-proxy:latest \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook-proxy:0.10.0-rc3
|
run: docker push beryju/passbook-proxy:0.10.3-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook-proxy:latest
|
run: docker push beryju/passbook-proxy:latest
|
||||||
build-static:
|
build-static:
|
||||||
@ -66,11 +77,11 @@ jobs:
|
|||||||
run: docker build
|
run: docker build
|
||||||
--no-cache
|
--no-cache
|
||||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||||
-t beryju/passbook-static:0.10.0-rc3
|
-t beryju/passbook-static:0.10.3-stable
|
||||||
-t beryju/passbook-static:latest
|
-t beryju/passbook-static:latest
|
||||||
-f static.Dockerfile .
|
-f static.Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook-static:0.10.0-rc3
|
run: docker push beryju/passbook-static:0.10.3-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook-static:latest
|
run: docker push beryju/passbook-static:latest
|
||||||
test-release:
|
test-release:
|
||||||
@ -82,10 +93,13 @@ jobs:
|
|||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run test suite in final docker images
|
- name: Run test suite in final docker images
|
||||||
run: |
|
run: |
|
||||||
|
sudo apt-get install -y pwgen
|
||||||
|
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||||
|
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||||
docker-compose pull -q
|
docker-compose pull -q
|
||||||
docker-compose up --no-start
|
docker-compose up --no-start
|
||||||
docker-compose start postgresql redis
|
docker-compose start postgresql redis
|
||||||
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
|
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test passbook"
|
||||||
sentry-release:
|
sentry-release:
|
||||||
needs:
|
needs:
|
||||||
- test-release
|
- test-release
|
||||||
@ -100,5 +114,5 @@ jobs:
|
|||||||
SENTRY_PROJECT: passbook
|
SENTRY_PROJECT: passbook
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
tagName: 0.10.0-rc3
|
tagName: 0.10.3-stable
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
|
|||||||
15
.github/workflows/tag.yml
vendored
15
.github/workflows/tag.yml
vendored
@ -1,10 +1,10 @@
|
|||||||
|
name: passbook-on-tag
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'version/*'
|
- 'version/*'
|
||||||
|
|
||||||
name: passbook-version-tag
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Create Release from Tag
|
name: Create Release from Tag
|
||||||
@ -13,7 +13,10 @@ jobs:
|
|||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
- name: Pre-release test
|
- name: Pre-release test
|
||||||
run: |
|
run: |
|
||||||
export PASSBOOK_TAG=latest
|
sudo apt-get install -y pwgen
|
||||||
|
echo "PASSBOOK_TAG=latest" >> .env
|
||||||
|
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||||
|
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||||
docker-compose pull -q
|
docker-compose pull -q
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
@ -21,7 +24,7 @@ jobs:
|
|||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
docker-compose up --no-start
|
docker-compose up --no-start
|
||||||
docker-compose start postgresql redis
|
docker-compose start postgresql redis
|
||||||
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
|
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test passbook"
|
||||||
- name: Install Helm
|
- name: Install Helm
|
||||||
run: |
|
run: |
|
||||||
apt update && apt install -y curl
|
apt update && apt install -y curl
|
||||||
@ -31,7 +34,7 @@ jobs:
|
|||||||
helm dependency update helm/
|
helm dependency update helm/
|
||||||
helm package helm/
|
helm package helm/
|
||||||
mv passbook-*.tgz passbook-chart.tgz
|
mv passbook-*.tgz passbook-chart.tgz
|
||||||
- name: Extract verison number
|
- name: Extract version number
|
||||||
id: get_version
|
id: get_version
|
||||||
uses: actions/github-script@0.2.0
|
uses: actions/github-script@0.2.0
|
||||||
with:
|
with:
|
||||||
@ -46,7 +49,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref }}
|
tag_name: ${{ github.ref }}
|
||||||
release_name: Release ${{ steps.get_version.outputs.result }}
|
release_name: Release ${{ steps.get_version.outputs.result }}
|
||||||
draft: false
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
- name: Upload packaged Helm Chart
|
- name: Upload packaged Helm Chart
|
||||||
id: upload-release-asset
|
id: upload-release-asset
|
||||||
|
|||||||
8
Makefile
8
Makefile
@ -1,7 +1,7 @@
|
|||||||
all: lint-fix lint coverage gen
|
all: lint-fix lint coverage gen
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
coverage run --concurrency=multiprocessing manage.py test passbook --failfast
|
coverage run --concurrency=multiprocessing manage.py test --failfast -v 3
|
||||||
coverage combine
|
coverage combine
|
||||||
coverage html
|
coverage html
|
||||||
coverage report
|
coverage report
|
||||||
@ -18,3 +18,9 @@ lint:
|
|||||||
|
|
||||||
gen: coverage
|
gen: coverage
|
||||||
./manage.py generate_swagger -o swagger.yaml -f yaml
|
./manage.py generate_swagger -o swagger.yaml -f yaml
|
||||||
|
|
||||||
|
local-stack:
|
||||||
|
export PASSBOOK_TAG=testing
|
||||||
|
docker build -t beryju/passbook:testng .
|
||||||
|
docker-compose up -d
|
||||||
|
docker-compose run --rm server migrate
|
||||||
|
|||||||
3
Pipfile
3
Pipfile
@ -59,5 +59,6 @@ docker = "*"
|
|||||||
pylint = "*"
|
pylint = "*"
|
||||||
pylint-django = "*"
|
pylint-django = "*"
|
||||||
selenium = "*"
|
selenium = "*"
|
||||||
unittest-xml-reporting = "*"
|
|
||||||
prospector = "*"
|
prospector = "*"
|
||||||
|
pytest = "*"
|
||||||
|
pytest-django = "*"
|
||||||
|
|||||||
244
Pipfile.lock
generated
244
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "a798bbd0b97857cac136c1743b8d6ad8bf8c3d95e2760c71d324bb2a7f47f678"
|
"sha256": "80570636236962f4b934a884817292de9f7bb48520aa964afc2959b0f795fb57"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -74,18 +74,17 @@
|
|||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2ab73b0c400ab8c7df84bee7564ef8a0813021da28dd7a05fcbffb77a8ae9de9",
|
"sha256:25c716b7c01d4664027afc6a6418a06459e311a610c7fd39a030a1ced1b72ce4"
|
||||||
"sha256:bb2222fa02fcd09b39e581e532d4f013ea850742d8cd46e9c10a21028b6d2ef5"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.14.56"
|
"version": "==1.14.63"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:37cc3f1013c00dc0f061582198d6b785dadf147bd99307d41c5c0e47debca65c",
|
"sha256:40f13f6c9c29c307a9dc5982739e537ddce55b29787b90c3447b507e3283bcd6",
|
||||||
"sha256:acd2df778a5e12b2a16ac040ce6e91a6c6f2d7ac67bd4f966472ce5c68b5b62d"
|
"sha256:aa88eafc6295132f4bc606f1df32b3248e0fa611724c0a216aceda767948ac75"
|
||||||
],
|
],
|
||||||
"version": "==1.17.58"
|
"version": "==1.17.63"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -111,36 +110,44 @@
|
|||||||
},
|
},
|
||||||
"cffi": {
|
"cffi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e",
|
"sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d",
|
||||||
"sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c",
|
"sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b",
|
||||||
"sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e",
|
"sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4",
|
||||||
"sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1",
|
"sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f",
|
||||||
"sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4",
|
"sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3",
|
||||||
"sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2",
|
"sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579",
|
||||||
"sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c",
|
"sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537",
|
||||||
"sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0",
|
"sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e",
|
||||||
"sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798",
|
"sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05",
|
||||||
"sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1",
|
"sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171",
|
||||||
"sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4",
|
"sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca",
|
||||||
"sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731",
|
"sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522",
|
||||||
"sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4",
|
"sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c",
|
||||||
"sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c",
|
"sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc",
|
||||||
"sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487",
|
"sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d",
|
||||||
"sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e",
|
"sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808",
|
||||||
"sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f",
|
"sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828",
|
||||||
"sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123",
|
"sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869",
|
||||||
"sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c",
|
"sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d",
|
||||||
"sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b",
|
"sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9",
|
||||||
"sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650",
|
"sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0",
|
||||||
"sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad",
|
"sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc",
|
||||||
"sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75",
|
"sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15",
|
||||||
"sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82",
|
"sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c",
|
||||||
"sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7",
|
"sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a",
|
||||||
"sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15",
|
"sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3",
|
||||||
"sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa",
|
"sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1",
|
||||||
"sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"
|
"sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768",
|
||||||
|
"sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d",
|
||||||
|
"sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b",
|
||||||
|
"sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e",
|
||||||
|
"sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d",
|
||||||
|
"sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730",
|
||||||
|
"sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394",
|
||||||
|
"sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1",
|
||||||
|
"sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591"
|
||||||
],
|
],
|
||||||
"version": "==1.14.2"
|
"version": "==1.14.3"
|
||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -327,11 +334,11 @@
|
|||||||
},
|
},
|
||||||
"django-storages": {
|
"django-storages": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1e37da57678e6cf1e9914f84099a305323e4e1f261afe54fdb703cae7aa6fbc3",
|
"sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c",
|
||||||
"sha256:36ed8dab33d761954498189592ce005920095fcbc02dab4184eb51393c370991"
|
"sha256:652275ab7844538c462b62810276c0244866f345878256a9e0e86f5b1283ae18"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.10"
|
"version": "==1.10.1"
|
||||||
},
|
},
|
||||||
"djangorestframework": {
|
"djangorestframework": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -387,10 +394,10 @@
|
|||||||
},
|
},
|
||||||
"google-auth": {
|
"google-auth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:bcbd9f970e7144fe933908aa286d7a12c44b7deb6d78a76871f0377a29d09789",
|
"sha256:7084c50c03f7a8a5696ef4500e65df0c525a0f6909f3c70b9ee65900a230c755",
|
||||||
"sha256:f4d5093f13b1b1c0a434ab1dc851cd26a983f86a4d75c95239974e33ed406a87"
|
"sha256:dcf86c5adc3a8a7659be190b12bb8912ae019cfd9ee2a571ea881e289fafbe39"
|
||||||
],
|
],
|
||||||
"version": "==1.21.1"
|
"version": "==1.21.2"
|
||||||
},
|
},
|
||||||
"gunicorn": {
|
"gunicorn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -835,9 +842,9 @@
|
|||||||
},
|
},
|
||||||
"pyrsistent": {
|
"pyrsistent": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:27515d2d5db0629c7dadf6fbe76973eb56f098c1b01d36de42eb69220d2c19e4"
|
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
||||||
],
|
],
|
||||||
"version": "==0.17.2"
|
"version": "==0.17.3"
|
||||||
},
|
},
|
||||||
"python-dateutil": {
|
"python-dateutil": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -952,11 +959,11 @@
|
|||||||
},
|
},
|
||||||
"sentry-sdk": {
|
"sentry-sdk": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:97bff68e57402ad39674e6fe2545df0d5eea41c3d51e280c170761705c8c20ff",
|
"sha256:1a086486ff9da15791f294f6e9915eb3747d161ef64dee2d038a4d0b4a369b24",
|
||||||
"sha256:a16caf9ce892623081cbb9a95f6c1f892778bb123909b0ed7afdfb52ce7a58a1"
|
"sha256:45486deb031cea6bbb25a540d7adb4dd48cd8a1cc31e6a5ce9fb4f792a572e9a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.17.4"
|
"version": "==0.17.6"
|
||||||
},
|
},
|
||||||
"service-identity": {
|
"service-identity": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1269,43 +1276,43 @@
|
|||||||
},
|
},
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb",
|
"sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
|
||||||
"sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3",
|
"sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
|
||||||
"sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716",
|
"sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
|
||||||
"sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034",
|
"sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
|
||||||
"sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3",
|
"sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
|
||||||
"sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8",
|
"sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
|
||||||
"sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0",
|
"sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
|
||||||
"sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f",
|
"sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
|
||||||
"sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4",
|
"sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
|
||||||
"sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962",
|
"sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
|
||||||
"sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d",
|
"sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
|
||||||
"sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b",
|
"sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
|
||||||
"sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4",
|
"sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
|
||||||
"sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3",
|
"sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
|
||||||
"sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258",
|
"sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
|
||||||
"sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59",
|
"sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
|
||||||
"sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01",
|
"sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
|
||||||
"sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd",
|
"sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
|
||||||
"sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b",
|
"sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
|
||||||
"sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d",
|
"sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
|
||||||
"sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89",
|
"sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
|
||||||
"sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd",
|
"sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
|
||||||
"sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b",
|
"sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
|
||||||
"sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d",
|
"sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
|
||||||
"sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46",
|
"sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
|
||||||
"sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546",
|
"sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
|
||||||
"sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082",
|
"sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
|
||||||
"sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b",
|
"sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
|
||||||
"sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4",
|
"sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
|
||||||
"sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8",
|
"sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
|
||||||
"sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811",
|
"sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
|
||||||
"sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd",
|
"sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
|
||||||
"sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651",
|
"sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
|
||||||
"sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"
|
"sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.2.1"
|
"version": "==5.3"
|
||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1373,6 +1380,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.10"
|
"version": "==2.10"
|
||||||
},
|
},
|
||||||
|
"iniconfig": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
||||||
|
"sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"
|
||||||
|
],
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
"isort": {
|
"isort": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||||
@ -1413,6 +1427,21 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.6.1"
|
"version": "==0.6.1"
|
||||||
},
|
},
|
||||||
|
"more-itertools": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20",
|
||||||
|
"sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"
|
||||||
|
],
|
||||||
|
"version": "==8.5.0"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
|
||||||
|
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==20.4"
|
||||||
|
},
|
||||||
"pathspec": {
|
"pathspec": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
|
"sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
|
||||||
@ -1434,6 +1463,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.10.0"
|
"version": "==0.10.0"
|
||||||
},
|
},
|
||||||
|
"pluggy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||||
|
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||||
|
],
|
||||||
|
"version": "==0.13.1"
|
||||||
|
},
|
||||||
"prospector": {
|
"prospector": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:43e5e187c027336b0e4c4aa6a82d66d3b923b5ec5b51968126132e32f9d14a2f"
|
"sha256:43e5e187c027336b0e4c4aa6a82d66d3b923b5ec5b51968126132e32f9d14a2f"
|
||||||
@ -1441,6 +1477,13 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.0"
|
"version": "==1.3.0"
|
||||||
},
|
},
|
||||||
|
"py": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
|
||||||
|
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
|
||||||
|
],
|
||||||
|
"version": "==1.9.0"
|
||||||
|
},
|
||||||
"pycodestyle": {
|
"pycodestyle": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
|
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
|
||||||
@ -1497,6 +1540,29 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.6"
|
"version": "==0.6"
|
||||||
},
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||||
|
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||||
|
],
|
||||||
|
"version": "==2.4.7"
|
||||||
|
},
|
||||||
|
"pytest": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40",
|
||||||
|
"sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==6.0.2"
|
||||||
|
},
|
||||||
|
"pytest-django": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4de6dbd077ed8606616958f77655fed0d5e3ee45159475671c7fa67596c6dba6",
|
||||||
|
"sha256:c33e3d3da14d8409b125d825d4e74da17bb252191bf6fc3da6856e27a8b73ea4"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.10.0"
|
||||||
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
|
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
|
||||||
@ -1604,10 +1670,10 @@
|
|||||||
},
|
},
|
||||||
"stevedore": {
|
"stevedore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a34086819e2c7a7f86d5635363632829dab8014e5fd7be2454c7cba84ac7514e",
|
"sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62",
|
||||||
"sha256:ddc09a744dc224c84ec8e8efcb70595042d21c97c76df60daee64c9ad53bc7ee"
|
"sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"
|
||||||
],
|
],
|
||||||
"version": "==3.2.1"
|
"version": "==3.2.2"
|
||||||
},
|
},
|
||||||
"toml": {
|
"toml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1642,14 +1708,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.4.1"
|
"version": "==1.4.1"
|
||||||
},
|
},
|
||||||
"unittest-xml-reporting": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:7bf515ea8cb244255a25100cd29db611a73f8d3d0aaf672ed3266307e14cc1ca",
|
|
||||||
"sha256:984cebba69e889401bfe3adb9088ca376b3a1f923f0590d005126c1bffd1a695"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==3.0.4"
|
|
||||||
},
|
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"extras": [
|
"extras": [
|
||||||
"secure"
|
"secure"
|
||||||
|
|||||||
17
README.md
17
README.md
@ -1,4 +1,4 @@
|
|||||||
<img src="passbook/static/static/passbook/logo.svg" height="50" alt="passbook logo"><img src="passbook/static/static/passbook/brand_inverted.svg" height="50" alt="passbook">
|
<img src="docs/images/logo.svg" height="50" alt="passbook logo"><img src="docs/images/brand_inverted.svg" height="50" alt="passbook">
|
||||||
|
|
||||||
[](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1)
|
[](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1)
|
||||||

|

|
||||||
@ -13,20 +13,7 @@ passbook is an open-source Identity Provider focused on flexibility and versatil
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For small/test setups it is recommended to use docker-compose.
|
For small/test setups it is recommended to use docker-compose, see the [documentation](https://passbook.beryju.org/installation/docker-compose/)
|
||||||
|
|
||||||
```
|
|
||||||
wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
|
||||||
# Optionally enable Error-reporting
|
|
||||||
# export PASSBOOK_ERROR_REPORTING=true
|
|
||||||
# Optionally deploy a different version
|
|
||||||
# export PASSBOOK_TAG=0.10.0-rc3
|
|
||||||
# If this is a productive installation, set a different PostgreSQL Password
|
|
||||||
# export PG_PASS=$(pwgen 40 1)
|
|
||||||
docker-compose pull
|
|
||||||
docker-compose up -d
|
|
||||||
docker-compose run --rm server migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
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//installation/kubernetes/)
|
||||||
|
|
||||||
|
|||||||
@ -139,7 +139,7 @@ stages:
|
|||||||
displayName: Run full test suite
|
displayName: Run full test suite
|
||||||
inputs:
|
inputs:
|
||||||
script: |
|
script: |
|
||||||
pipenv run coverage run ./manage.py test passbook
|
pipenv run coverage run ./manage.py test passbook -v 3
|
||||||
mkdir output-unittest
|
mkdir output-unittest
|
||||||
mv unittest.xml output-unittest/unittest.xml
|
mv unittest.xml output-unittest/unittest.xml
|
||||||
mv .coverage output-unittest/coverage
|
mv .coverage output-unittest/coverage
|
||||||
@ -150,7 +150,7 @@ stages:
|
|||||||
publishLocation: 'pipeline'
|
publishLocation: 'pipeline'
|
||||||
- job: coverage_e2e
|
- job: coverage_e2e
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
name: coventry
|
||||||
steps:
|
steps:
|
||||||
- task: UsePythonVersion@0
|
- task: UsePythonVersion@0
|
||||||
inputs:
|
inputs:
|
||||||
@ -181,7 +181,14 @@ stages:
|
|||||||
- task: CmdLine@2
|
- task: CmdLine@2
|
||||||
displayName: Run full test suite
|
displayName: Run full test suite
|
||||||
inputs:
|
inputs:
|
||||||
script: pipenv run coverage run ./manage.py test e2e
|
script: |
|
||||||
|
pipenv run coverage run ./manage.py test e2e -v 3
|
||||||
|
- task: CmdLine@2
|
||||||
|
condition: always()
|
||||||
|
displayName: Cleanup
|
||||||
|
inputs:
|
||||||
|
script: |
|
||||||
|
docker stop $(docker ps -aq)
|
||||||
- task: CmdLine@2
|
- task: CmdLine@2
|
||||||
displayName: Prepare unittests and coverage for upload
|
displayName: Prepare unittests and coverage for upload
|
||||||
inputs:
|
inputs:
|
||||||
@ -225,11 +232,9 @@ stages:
|
|||||||
script: |
|
script: |
|
||||||
sudo pip install -U wheel pipenv
|
sudo pip install -U wheel pipenv
|
||||||
pipenv install --dev
|
pipenv install --dev
|
||||||
find .
|
|
||||||
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
|
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
|
||||||
pipenv run coverage xml
|
pipenv run coverage xml
|
||||||
pipenv run coverage html
|
pipenv run coverage html
|
||||||
find .
|
|
||||||
- task: PublishCodeCoverageResults@1
|
- task: PublishCodeCoverageResults@1
|
||||||
inputs:
|
inputs:
|
||||||
codeCoverageTool: 'Cobertura'
|
codeCoverageTool: 'Cobertura'
|
||||||
|
|||||||
@ -14,6 +14,8 @@ services:
|
|||||||
- POSTGRES_DB=passbook
|
- POSTGRES_DB=passbook
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=false
|
- traefik.enable=false
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis
|
||||||
networks:
|
networks:
|
||||||
@ -21,13 +23,12 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- traefik.enable=false
|
- traefik.enable=false
|
||||||
server:
|
server:
|
||||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-rc3}
|
image: beryju/passbook:${PASSBOOK_TAG:-0.10.3-stable}
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
PASSBOOK_REDIS__HOST: redis
|
PASSBOOK_REDIS__HOST: redis
|
||||||
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
|
|
||||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
|
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
PASSBOOK_LOG_LEVEL: debug
|
PASSBOOK_LOG_LEVEL: debug
|
||||||
ports:
|
ports:
|
||||||
- 8000
|
- 8000
|
||||||
@ -37,8 +38,10 @@ services:
|
|||||||
- traefik.port=8000
|
- traefik.port=8000
|
||||||
- traefik.docker.network=internal
|
- traefik.docker.network=internal
|
||||||
- traefik.frontend.rule=PathPrefix:/
|
- traefik.frontend.rule=PathPrefix:/
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
worker:
|
worker:
|
||||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-rc3}
|
image: beryju/passbook:${PASSBOOK_TAG:-0.10.3-stable}
|
||||||
command: worker
|
command: worker
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
@ -46,12 +49,13 @@ services:
|
|||||||
- traefik.enable=false
|
- traefik.enable=false
|
||||||
environment:
|
environment:
|
||||||
PASSBOOK_REDIS__HOST: redis
|
PASSBOOK_REDIS__HOST: redis
|
||||||
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
|
|
||||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
|
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
PASSBOOK_LOG_LEVEL: debug
|
PASSBOOK_LOG_LEVEL: debug
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
static:
|
static:
|
||||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.0-rc3}
|
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.3-stable}
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@ -11,14 +11,21 @@ This installation method is for test-setups and small-scale productive setups.
|
|||||||
|
|
||||||
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). Place it in a directory of your choice.
|
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). Place it in a directory of your choice.
|
||||||
|
|
||||||
|
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING=true >> .env`
|
||||||
|
|
||||||
|
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.3-stable >> .env`
|
||||||
|
|
||||||
|
If this is a fresh passbook install run the following commands to generate a password:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt-get install -y pwgen
|
||||||
|
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||||
|
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Afterwards, run these commands to finish
|
||||||
|
|
||||||
```
|
```
|
||||||
wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
|
||||||
# Optionally enable Error-reporting
|
|
||||||
# export PASSBOOK_ERROR_REPORTING=true
|
|
||||||
# Optionally deploy a different version
|
|
||||||
# export PASSBOOK_TAG=0.10.0-rc3
|
|
||||||
# If this is a productive installation, set a different PostgreSQL Password
|
|
||||||
# export PG_PASS=$(pwgen 40 1)
|
|
||||||
docker-compose pull
|
docker-compose pull
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
docker-compose run --rm server migrate
|
docker-compose run --rm server migrate
|
||||||
|
|||||||
@ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th
|
|||||||
image:
|
image:
|
||||||
name: beryju/passbook
|
name: beryju/passbook
|
||||||
name_static: beryju/passbook-static
|
name_static: beryju/passbook-static
|
||||||
tag: 0.9.0-stable
|
tag: 0.10.3-stable
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
|
|||||||
77
docs/integrations/services/vmware-vcenter/index.md
Normal file
77
docs/integrations/services/vmware-vcenter/index.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
BIN
docs/integrations/services/vmware-vcenter/passbook_setup.png
Normal file
BIN
docs/integrations/services/vmware-vcenter/passbook_setup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
BIN
docs/integrations/services/vmware-vcenter/vcenter_post_setup.png
Normal file
BIN
docs/integrations/services/vmware-vcenter/vcenter_post_setup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
20
docs/outposts/deploy-docker-compose.md
Normal file
20
docs/outposts/deploy-docker-compose.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# 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: 'true'
|
||||||
|
PASSBOOK_TOKEN: token-generated-by-passbook
|
||||||
|
```
|
||||||
99
docs/outposts/deploy-kubernetes.md
Normal file
99
docs/outposts/deploy-kubernetes.md
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
@ -6,21 +6,9 @@ An outpost is a single deployment of a passbook component, which can be deployed
|
|||||||
|
|
||||||
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.
|
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, you can for example use this docker-compose snippet:
|
To deploy an outpost, see: <a name="deploy">
|
||||||
|
|
||||||
```yaml
|
- [Kubernetes](deploy-kubernetes.md)
|
||||||
version: 3.5
|
- [docker-compose](deploy-docker-compose.md)
|
||||||
|
|
||||||
services:
|
In future versions, this snippet will be automatically generated. You will also be able to deploy an outpost directly into a kubernetes cluster.
|
||||||
passbook_proxy:
|
|
||||||
image: beryju/passbook-proxy:0.10.0-stable
|
|
||||||
ports:
|
|
||||||
- 4180:4180
|
|
||||||
- 4443:4443
|
|
||||||
environment:
|
|
||||||
PASSBOOK_HOST: https://your-passbook.tld
|
|
||||||
PASSBOOK_INSECURE: 'true'
|
|
||||||
PASSBOOK_TOKEN: token-generated-by-passbook
|
|
||||||
```
|
|
||||||
|
|
||||||
In future versions, this snippet will be automatically generated. You will also be able to deploy an outpost directly into a kubernetes cluster.w
|
|
||||||
|
|||||||
11
docs/troubleshooting/access.md
Normal file
11
docs/troubleshooting/access.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Troubleshooting access problems
|
||||||
|
|
||||||
|
## I get an access denied error when trying to access an application.
|
||||||
|
|
||||||
|
If your user is a superuser, or has the attribute `passbook_user_debug` set to true:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Afterwards, try to access the application again. You will now see a message explaining which policy denied you access:
|
||||||
|
|
||||||
|

|
||||||
BIN
docs/troubleshooting/access_denied_message.png
Normal file
BIN
docs/troubleshooting/access_denied_message.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/troubleshooting/passbook_user_debug.png
Normal file
BIN
docs/troubleshooting/passbook_user_debug.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@ -2,7 +2,7 @@ version: '3.7'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
chrome:
|
chrome:
|
||||||
image: selenium/standalone-chrome-debug:3.141.59-20200525
|
image: selenium/standalone-chrome-debug:3.141.59-20200719
|
||||||
volumes:
|
volumes:
|
||||||
- /dev/shm:/dev/shm
|
- /dev/shm:/dev/shm
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
"""Test Enroll flow"""
|
"""Test Enroll flow"""
|
||||||
from time import sleep
|
from sys import platform
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from docker import DockerClient, from_env
|
|
||||||
from docker.models.containers import Container
|
|
||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
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
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
@ -18,41 +17,23 @@ from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
|||||||
from passbook.stages.user_login.models import UserLoginStage
|
from passbook.stages.user_login.models import UserLoginStage
|
||||||
from passbook.stages.user_write.models import UserWriteStage
|
from passbook.stages.user_write.models import UserWriteStage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestFlowsEnroll(SeleniumTestCase):
|
class TestFlowsEnroll(SeleniumTestCase):
|
||||||
"""Test Enroll flow"""
|
"""Test Enroll flow"""
|
||||||
|
|
||||||
def setUp(self):
|
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||||
self.container = self.setup_client()
|
return {
|
||||||
super().setUp()
|
"image": "mailhog/mailhog:v1.0.1",
|
||||||
|
"detach": True,
|
||||||
def setup_client(self) -> Container:
|
"network_mode": "host",
|
||||||
"""Setup test IdP container"""
|
"auto_remove": True,
|
||||||
client: DockerClient = from_env()
|
"healthcheck": Healthcheck(
|
||||||
container = client.containers.run(
|
|
||||||
image="mailhog/mailhog:v1.0.1",
|
|
||||||
detach=True,
|
|
||||||
network_mode="host",
|
|
||||||
auto_remove=True,
|
|
||||||
healthcheck=Healthcheck(
|
|
||||||
test=["CMD", "wget", "--spider", "http://localhost:8025"],
|
test=["CMD", "wget", "--spider", "http://localhost:8025"],
|
||||||
interval=5 * 100 * 1000000,
|
interval=5 * 100 * 1000000,
|
||||||
start_period=1 * 100 * 1000000,
|
start_period=1 * 100 * 1000000,
|
||||||
),
|
),
|
||||||
)
|
}
|
||||||
while True:
|
|
||||||
container.reload()
|
|
||||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
|
||||||
if status == "healthy":
|
|
||||||
return container
|
|
||||||
LOGGER.info("Container failed healthcheck")
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.container.kill()
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
def test_enroll_2_step(self):
|
def test_enroll_2_step(self):
|
||||||
"""Test 2-step enroll flow"""
|
"""Test 2-step enroll flow"""
|
||||||
@ -220,21 +201,25 @@ class TestFlowsEnroll(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_name").send_keys("some name")
|
self.driver.find_element(By.ID, "id_name").send_keys("some name")
|
||||||
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
|
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
|
||||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||||
sleep(3)
|
# Wait for the success message so we know the email is sent
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-form > p"))
|
||||||
|
)
|
||||||
|
|
||||||
# Open Mailhog
|
# Open Mailhog
|
||||||
self.driver.get("http://localhost:8025")
|
self.driver.get("http://localhost:8025")
|
||||||
|
|
||||||
# Click on first message
|
# Click on first message
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CLASS_NAME, "msglist-message"))
|
||||||
|
)
|
||||||
self.driver.find_element(By.CLASS_NAME, "msglist-message").click()
|
self.driver.find_element(By.CLASS_NAME, "msglist-message").click()
|
||||||
sleep(3)
|
|
||||||
self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane"))
|
self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane"))
|
||||||
self.driver.find_element(By.ID, "confirm").click()
|
self.driver.find_element(By.ID, "confirm").click()
|
||||||
self.driver.close()
|
self.driver.close()
|
||||||
self.driver.switch_to.window(self.driver.window_handles[0])
|
self.driver.switch_to.window(self.driver.window_handles[0])
|
||||||
|
|
||||||
# We're now logged in
|
# We're now logged in
|
||||||
sleep(3)
|
|
||||||
self.wait.until(
|
self.wait.until(
|
||||||
ec.presence_of_element_located(
|
ec.presence_of_element_located(
|
||||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
"""test default login flow"""
|
"""test default login flow"""
|
||||||
|
from sys import platform
|
||||||
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestFlowsLogin(SeleniumTestCase):
|
class TestFlowsLogin(SeleniumTestCase):
|
||||||
"""test default login flow"""
|
"""test default login flow"""
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
"""test stage setup flows (password change)"""
|
"""test stage setup flows (password change)"""
|
||||||
import string
|
from sys import platform
|
||||||
from random import SystemRandom
|
from unittest.case import skipUnless
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
@ -9,9 +8,11 @@ from selenium.webdriver.common.keys import Keys
|
|||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase
|
||||||
from passbook.core.models import User
|
from passbook.core.models import User
|
||||||
from passbook.flows.models import Flow, FlowDesignation
|
from passbook.flows.models import Flow, FlowDesignation
|
||||||
|
from passbook.providers.oauth2.generators import generate_client_secret
|
||||||
from passbook.stages.password.models import PasswordStage
|
from passbook.stages.password.models import PasswordStage
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestFlowsStageSetup(SeleniumTestCase):
|
class TestFlowsStageSetup(SeleniumTestCase):
|
||||||
"""test stage setup flows"""
|
"""test stage setup flows"""
|
||||||
|
|
||||||
@ -27,10 +28,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
|
|||||||
stage.change_flow = flow
|
stage.change_flow = flow
|
||||||
stage.save()
|
stage.save()
|
||||||
|
|
||||||
new_password = "".join(
|
new_password = generate_client_secret()
|
||||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
|
||||||
for _ in range(8)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.driver.get(
|
self.driver.get(
|
||||||
f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F"
|
f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F"
|
||||||
@ -48,7 +46,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password)
|
self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password)
|
||||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||||
|
|
||||||
sleep(2)
|
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||||
# Because USER() is cached, we need to get the user manually here
|
# Because USER() is cached, we need to get the user manually here
|
||||||
user = User.objects.get(username=USER().username)
|
user = User.objects.get(username=USER().username)
|
||||||
self.assertTrue(user.check_password(new_password))
|
self.assertTrue(user.check_password(new_password))
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
"""test OAuth Provider flow"""
|
"""test OAuth Provider flow"""
|
||||||
from time import sleep
|
from sys import platform
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
from docker import DockerClient, from_env
|
|
||||||
from docker.models.containers import Container
|
|
||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
from structlog import get_logger
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
@ -19,32 +19,29 @@ from passbook.providers.oauth2.generators import (
|
|||||||
)
|
)
|
||||||
from passbook.providers.oauth2.models import ClientTypes, OAuth2Provider, ResponseTypes
|
from passbook.providers.oauth2.models import ClientTypes, OAuth2Provider, ResponseTypes
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestProviderOAuth2Github(SeleniumTestCase):
|
class TestProviderOAuth2Github(SeleniumTestCase):
|
||||||
"""test OAuth Provider flow"""
|
"""test OAuth Provider flow"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client_id = generate_client_id()
|
self.client_id = generate_client_id()
|
||||||
self.client_secret = generate_client_secret()
|
self.client_secret = generate_client_secret()
|
||||||
self.container = self.setup_client()
|
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
def setup_client(self) -> Container:
|
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||||
"""Setup client grafana container which we test OAuth against"""
|
"""Setup client grafana container which we test OAuth against"""
|
||||||
client: DockerClient = from_env()
|
return {
|
||||||
container = client.containers.run(
|
"image": "grafana/grafana:7.1.0",
|
||||||
image="grafana/grafana:7.1.0",
|
"detach": True,
|
||||||
detach=True,
|
"network_mode": "host",
|
||||||
network_mode="host",
|
"auto_remove": True,
|
||||||
auto_remove=True,
|
"healthcheck": Healthcheck(
|
||||||
healthcheck=Healthcheck(
|
|
||||||
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
||||||
interval=5 * 100 * 1000000,
|
interval=5 * 100 * 1000000,
|
||||||
start_period=1 * 100 * 1000000,
|
start_period=1 * 100 * 1000000,
|
||||||
),
|
),
|
||||||
environment={
|
"environment": {
|
||||||
"GF_AUTH_GITHUB_ENABLED": "true",
|
"GF_AUTH_GITHUB_ENABLED": "true",
|
||||||
"GF_AUTH_GITHUB_ALLOW_SIGN_UP": "true",
|
"GF_AUTH_GITHUB_ALLOW_SIGN_UP": "true",
|
||||||
"GF_AUTH_GITHUB_CLIENT_ID": self.client_id,
|
"GF_AUTH_GITHUB_CLIENT_ID": self.client_id,
|
||||||
@ -61,22 +58,10 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
),
|
),
|
||||||
"GF_LOG_LEVEL": "debug",
|
"GF_LOG_LEVEL": "debug",
|
||||||
},
|
},
|
||||||
)
|
}
|
||||||
while True:
|
|
||||||
container.reload()
|
|
||||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
|
||||||
if status == "healthy":
|
|
||||||
return container
|
|
||||||
LOGGER.info("Container failed healthcheck")
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.container.kill()
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
def test_authorization_consent_implied(self):
|
def test_authorization_consent_implied(self):
|
||||||
"""test OAuth Provider flow (default authorization flow with implied consent)"""
|
"""test OAuth Provider flow (default authorization flow with implied consent)"""
|
||||||
sleep(1)
|
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
authorization_flow = Flow.objects.get(
|
authorization_flow = Flow.objects.get(
|
||||||
slug="default-provider-authorization-implicit-consent"
|
slug="default-provider-authorization-implicit-consent"
|
||||||
@ -129,7 +114,6 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
|
|
||||||
def test_authorization_consent_explicit(self):
|
def test_authorization_consent_explicit(self):
|
||||||
"""test OAuth Provider flow (default authorization flow with explicit consent)"""
|
"""test OAuth Provider flow (default authorization flow with explicit consent)"""
|
||||||
sleep(1)
|
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
authorization_flow = Flow.objects.get(
|
authorization_flow = Flow.objects.get(
|
||||||
slug="default-provider-authorization-explicit-consent"
|
slug="default-provider-authorization-explicit-consent"
|
||||||
@ -167,8 +151,13 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]"
|
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]"
|
||||||
).text,
|
).text,
|
||||||
)
|
)
|
||||||
sleep(1)
|
self.driver.find_element(
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
By.CSS_SELECTOR,
|
||||||
|
(
|
||||||
|
"form[action='/flows/b/default-provider-authorization-explicit-consent/'] "
|
||||||
|
"[type=submit]"
|
||||||
|
),
|
||||||
|
).click()
|
||||||
|
|
||||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||||
@ -197,7 +186,6 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
|
|
||||||
def test_denied(self):
|
def test_denied(self):
|
||||||
"""test OAuth Provider flow (default authorization flow, denied)"""
|
"""test OAuth Provider flow (default authorization flow, denied)"""
|
||||||
sleep(1)
|
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
authorization_flow = Flow.objects.get(
|
authorization_flow = Flow.objects.get(
|
||||||
slug="default-provider-authorization-explicit-consent"
|
slug="default-provider-authorization-explicit-consent"
|
||||||
@ -227,7 +215,10 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||||
"Permission denied",
|
"Permission denied",
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
"""test OAuth2 OpenID Provider flow"""
|
"""test OAuth2 OpenID Provider flow"""
|
||||||
|
from sys import platform
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
from docker import DockerClient, from_env
|
|
||||||
from docker.models.containers import Container
|
|
||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
@ -34,29 +35,27 @@ from passbook.providers.oauth2.models import (
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestProviderOAuth2OIDC(SeleniumTestCase):
|
class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||||
"""test OAuth with OpenID Provider flow"""
|
"""test OAuth with OpenID Provider flow"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client_id = generate_client_id()
|
self.client_id = generate_client_id()
|
||||||
self.client_secret = generate_client_secret()
|
self.client_secret = generate_client_secret()
|
||||||
self.container = self.setup_client()
|
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
def setup_client(self) -> Container:
|
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||||
"""Setup client grafana container which we test OIDC against"""
|
return {
|
||||||
client: DockerClient = from_env()
|
"image": "grafana/grafana:7.1.0",
|
||||||
container = client.containers.run(
|
"detach": True,
|
||||||
image="grafana/grafana:7.1.0",
|
"network_mode": "host",
|
||||||
detach=True,
|
"auto_remove": True,
|
||||||
network_mode="host",
|
"healthcheck": Healthcheck(
|
||||||
auto_remove=True,
|
|
||||||
healthcheck=Healthcheck(
|
|
||||||
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
||||||
interval=5 * 100 * 1000000,
|
interval=5 * 100 * 1000000,
|
||||||
start_period=1 * 100 * 1000000,
|
start_period=1 * 100 * 1000000,
|
||||||
),
|
),
|
||||||
environment={
|
"environment": {
|
||||||
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
|
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
|
||||||
"GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id,
|
"GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id,
|
||||||
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
|
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
|
||||||
@ -72,18 +71,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
),
|
),
|
||||||
"GF_LOG_LEVEL": "debug",
|
"GF_LOG_LEVEL": "debug",
|
||||||
},
|
},
|
||||||
)
|
}
|
||||||
while True:
|
|
||||||
container.reload()
|
|
||||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
|
||||||
if status == "healthy":
|
|
||||||
return container
|
|
||||||
LOGGER.info("Container failed healthcheck")
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.container.kill()
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
def test_redirect_uri_error(self):
|
def test_redirect_uri_error(self):
|
||||||
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
||||||
@ -297,7 +285,10 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||||
"Permission denied",
|
"Permission denied",
|
||||||
|
|||||||
96
e2e/test_provider_proxy.py
Normal file
96
e2e/test_provider_proxy.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
"""Proxy and Outpost e2e tests"""
|
||||||
|
from sys import platform
|
||||||
|
from time import sleep
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
|
from docker.client import DockerClient, from_env
|
||||||
|
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 passbook.core.models import Application
|
||||||
|
from passbook.flows.models import Flow
|
||||||
|
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
|
||||||
|
from passbook.providers.proxy.models import ProxyProvider
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
class TestProviderProxy(SeleniumTestCase):
|
||||||
|
"""Proxy and Outpost e2e tests"""
|
||||||
|
|
||||||
|
proxy_container: Container
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
super().tearDown()
|
||||||
|
self.proxy_container.kill()
|
||||||
|
|
||||||
|
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||||
|
return {
|
||||||
|
"image": "traefik/whoami:latest",
|
||||||
|
"detach": True,
|
||||||
|
"network_mode": "host",
|
||||||
|
"auto_remove": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def start_proxy(self, outpost: Outpost) -> Container:
|
||||||
|
"""Start proxy container based on outpost created"""
|
||||||
|
client: DockerClient = from_env()
|
||||||
|
container = client.containers.run(
|
||||||
|
image="beryju/passbook-proxy:latest",
|
||||||
|
detach=True,
|
||||||
|
network_mode="host",
|
||||||
|
auto_remove=True,
|
||||||
|
environment={
|
||||||
|
"PASSBOOK_HOST": self.live_server_url,
|
||||||
|
"PASSBOOK_TOKEN": outpost.token.token_uuid.hex,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return container
|
||||||
|
|
||||||
|
def test_proxy_simple(self):
|
||||||
|
"""Test simple outpost setup with single provider"""
|
||||||
|
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||||
|
name="proxy_provider",
|
||||||
|
authorization_flow=Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-implicit-consent"
|
||||||
|
),
|
||||||
|
internal_host="http://localhost:80",
|
||||||
|
external_host="http://localhost:4180",
|
||||||
|
)
|
||||||
|
# Ensure OAuth2 Params are set
|
||||||
|
proxy.set_oauth_defaults()
|
||||||
|
proxy.save()
|
||||||
|
# we need to create an application to actually access the proxy
|
||||||
|
Application.objects.create(name="proxy", slug="proxy", provider=proxy)
|
||||||
|
outpost: Outpost = Outpost.objects.create(
|
||||||
|
name="proxy_outpost",
|
||||||
|
type=OutpostType.PROXY,
|
||||||
|
deployment_type=OutpostDeploymentType.CUSTOM,
|
||||||
|
)
|
||||||
|
outpost.providers.add(proxy)
|
||||||
|
outpost.save()
|
||||||
|
|
||||||
|
self.proxy_container = self.start_proxy(outpost)
|
||||||
|
|
||||||
|
# Wait until outpost healthcheck succeeds
|
||||||
|
healthcheck_retries = 0
|
||||||
|
while healthcheck_retries < 50:
|
||||||
|
if outpost.health:
|
||||||
|
break
|
||||||
|
healthcheck_retries += 1
|
||||||
|
sleep(0.5)
|
||||||
|
|
||||||
|
self.driver.get("http://localhost:4180")
|
||||||
|
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||||
|
self.assertIn("X-Forwarded-Preferred-Username: pbadmin", full_body_text)
|
||||||
@ -1,11 +1,14 @@
|
|||||||
"""test SAML Provider flow"""
|
"""test SAML Provider flow"""
|
||||||
|
from sys import platform
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
from docker import DockerClient, from_env
|
from docker import DockerClient, from_env
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase
|
||||||
@ -23,6 +26,7 @@ from passbook.providers.saml.models import (
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestProviderSAML(SeleniumTestCase):
|
class TestProviderSAML(SeleniumTestCase):
|
||||||
"""test SAML Provider flow"""
|
"""test SAML Provider flow"""
|
||||||
|
|
||||||
@ -60,10 +64,6 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
LOGGER.info("Container failed healthcheck")
|
LOGGER.info("Container failed healthcheck")
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.container.kill()
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
def test_sp_initiated_implicit(self):
|
def test_sp_initiated_implicit(self):
|
||||||
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
@ -207,7 +207,10 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||||
"Permission denied",
|
"Permission denied",
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
"""test OAuth Source"""
|
"""test OAuth Source"""
|
||||||
from os.path import abspath
|
from os.path import abspath
|
||||||
|
from sys import platform
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
from docker import DockerClient, from_env
|
from django.test import override_settings
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
@ -21,6 +24,7 @@ CONFIG_PATH = "/tmp/dex.yml"
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestSourceOAuth(SeleniumTestCase):
|
class TestSourceOAuth(SeleniumTestCase):
|
||||||
"""test OAuth Source flow"""
|
"""test OAuth Source flow"""
|
||||||
|
|
||||||
@ -28,7 +32,7 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client_secret = generate_client_secret()
|
self.client_secret = generate_client_secret()
|
||||||
self.container = self.setup_client()
|
self.prepare_dex_config()
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
def prepare_dex_config(self):
|
def prepare_dex_config(self):
|
||||||
@ -66,34 +70,23 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
with open(CONFIG_PATH, "w+") as _file:
|
with open(CONFIG_PATH, "w+") as _file:
|
||||||
safe_dump(config, _file)
|
safe_dump(config, _file)
|
||||||
|
|
||||||
def setup_client(self) -> Container:
|
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||||
"""Setup test Dex container"""
|
return {
|
||||||
self.prepare_dex_config()
|
"image": "quay.io/dexidp/dex:v2.24.0",
|
||||||
client: DockerClient = from_env()
|
"detach": True,
|
||||||
container = client.containers.run(
|
"network_mode": "host",
|
||||||
image="quay.io/dexidp/dex:v2.24.0",
|
"auto_remove": True,
|
||||||
detach=True,
|
"command": "serve /config.yml",
|
||||||
network_mode="host",
|
"healthcheck": Healthcheck(
|
||||||
auto_remove=True,
|
|
||||||
command="serve /config.yml",
|
|
||||||
healthcheck=Healthcheck(
|
|
||||||
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
|
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
|
||||||
interval=5 * 100 * 1000000,
|
interval=5 * 100 * 1000000,
|
||||||
start_period=1 * 100 * 1000000,
|
start_period=1 * 100 * 1000000,
|
||||||
),
|
),
|
||||||
volumes={abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}},
|
"volumes": {abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}},
|
||||||
)
|
}
|
||||||
while True:
|
|
||||||
container.reload()
|
|
||||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
|
||||||
if status == "healthy":
|
|
||||||
return container
|
|
||||||
LOGGER.info("Container failed healthcheck")
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
def create_objects(self):
|
def create_objects(self):
|
||||||
"""Create required objects"""
|
"""Create required objects"""
|
||||||
sleep(1)
|
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||||
@ -111,10 +104,6 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
consumer_secret=self.client_secret,
|
consumer_secret=self.client_secret,
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.container.kill()
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
def test_oauth_enroll(self):
|
def test_oauth_enroll(self):
|
||||||
"""test OAuth Source With With OIDC"""
|
"""test OAuth Source With With OIDC"""
|
||||||
self.create_objects()
|
self.create_objects()
|
||||||
@ -141,6 +130,7 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||||
|
|
||||||
|
self.wait.until(ec.presence_of_element_located((By.NAME, "username")))
|
||||||
# At this point we've been redirected back
|
# At this point we've been redirected back
|
||||||
# and we're asked for the username
|
# and we're asked for the username
|
||||||
self.driver.find_element(By.NAME, "username").click()
|
self.driver.find_element(By.NAME, "username").click()
|
||||||
@ -167,6 +157,42 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
"admin@example.com",
|
"admin@example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@override_settings(SESSION_COOKIE_SAMESITE="strict")
|
||||||
|
def test_oauth_samesite_strict(self):
|
||||||
|
"""test OAuth Source With SameSite set to strict
|
||||||
|
(=will fail because session is not carried over)"""
|
||||||
|
self.create_objects()
|
||||||
|
self.driver.get(self.live_server_url)
|
||||||
|
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located(
|
||||||
|
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.find_element(
|
||||||
|
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||||
|
).click()
|
||||||
|
|
||||||
|
# Now we should be at the IDP, wait for the login field
|
||||||
|
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||||
|
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||||
|
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||||
|
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
|
# Wait until we're logged in
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
|
||||||
|
)
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||||
|
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-alert__title"))
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-alert__title").text,
|
||||||
|
"Authentication Failed.",
|
||||||
|
)
|
||||||
|
|
||||||
def test_oauth_enroll_auth(self):
|
def test_oauth_enroll_auth(self):
|
||||||
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
|
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
|
||||||
self.test_oauth_enroll()
|
self.test_oauth_enroll()
|
||||||
@ -178,10 +204,11 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
sleep(1)
|
||||||
self.driver.find_element(
|
self.driver.find_element(
|
||||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||||
).click()
|
).click()
|
||||||
|
sleep(1)
|
||||||
# Now we should be at the IDP, wait for the login field
|
# Now we should be at the IDP, wait for the login field
|
||||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||||
@ -1,8 +1,9 @@
|
|||||||
"""test SAML Source"""
|
"""test SAML Source"""
|
||||||
|
from sys import platform
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
from docker import DockerClient, from_env
|
|
||||||
from docker.models.containers import Container
|
|
||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
@ -68,48 +69,31 @@ Sm75WXsflOxuTn08LbgGc4s=
|
|||||||
-----END PRIVATE KEY-----"""
|
-----END PRIVATE KEY-----"""
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestSourceSAML(SeleniumTestCase):
|
class TestSourceSAML(SeleniumTestCase):
|
||||||
"""test SAML Source flow"""
|
"""test SAML Source flow"""
|
||||||
|
|
||||||
def setUp(self):
|
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||||
self.container = self.setup_client()
|
return {
|
||||||
super().setUp()
|
"image": "kristophjunge/test-saml-idp:1.15",
|
||||||
|
"detach": True,
|
||||||
def setup_client(self) -> Container:
|
"network_mode": "host",
|
||||||
"""Setup test IdP container"""
|
"auto_remove": True,
|
||||||
client: DockerClient = from_env()
|
"healthcheck": Healthcheck(
|
||||||
container = client.containers.run(
|
|
||||||
image="kristophjunge/test-saml-idp:1.15",
|
|
||||||
detach=True,
|
|
||||||
network_mode="host",
|
|
||||||
auto_remove=True,
|
|
||||||
healthcheck=Healthcheck(
|
|
||||||
test=["CMD", "curl", "http://localhost:8080"],
|
test=["CMD", "curl", "http://localhost:8080"],
|
||||||
interval=5 * 100 * 1000000,
|
interval=5 * 100 * 1000000,
|
||||||
start_period=1 * 100 * 1000000,
|
start_period=1 * 100 * 1000000,
|
||||||
),
|
),
|
||||||
environment={
|
"environment": {
|
||||||
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
|
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
|
||||||
"SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": (
|
"SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": (
|
||||||
f"{self.live_server_url}/source/saml/saml-idp-test/acs/"
|
f"{self.live_server_url}/source/saml/saml-idp-test/acs/"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
}
|
||||||
while True:
|
|
||||||
container.reload()
|
|
||||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
|
||||||
if status == "healthy":
|
|
||||||
return container
|
|
||||||
LOGGER.info("Container failed healthcheck")
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.container.kill()
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
def test_idp_redirect(self):
|
def test_idp_redirect(self):
|
||||||
"""test SAML Source With redirect binding"""
|
"""test SAML Source With redirect binding"""
|
||||||
sleep(1)
|
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||||
@ -161,7 +145,6 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
|
|
||||||
def test_idp_post(self):
|
def test_idp_post(self):
|
||||||
"""test SAML Source With post binding"""
|
"""test SAML Source With post binding"""
|
||||||
sleep(1)
|
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||||
@ -215,7 +198,6 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
|
|
||||||
def test_idp_post_auto(self):
|
def test_idp_post_auto(self):
|
||||||
"""test SAML Source With post binding (auto redirect)"""
|
"""test SAML Source With post binding (auto redirect)"""
|
||||||
sleep(1)
|
|
||||||
# Bootstrap all needed objects
|
# Bootstrap all needed objects
|
||||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||||
|
|||||||
33
e2e/utils.py
33
e2e/utils.py
@ -4,13 +4,16 @@ from glob import glob
|
|||||||
from importlib.util import module_from_spec, spec_from_file_location
|
from importlib.util import module_from_spec, spec_from_file_location
|
||||||
from inspect import getmembers, isfunction
|
from inspect import getmembers, isfunction
|
||||||
from os import environ, makedirs
|
from os import environ, makedirs
|
||||||
from time import time
|
from time import sleep, time
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
|
from docker import DockerClient, from_env
|
||||||
|
from docker.models.containers import Container
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||||
from selenium.webdriver.remote.webdriver import WebDriver
|
from selenium.webdriver.remote.webdriver import WebDriver
|
||||||
@ -30,15 +33,37 @@ def USER() -> User: # noqa
|
|||||||
class SeleniumTestCase(StaticLiveServerTestCase):
|
class SeleniumTestCase(StaticLiveServerTestCase):
|
||||||
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
|
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
|
||||||
|
|
||||||
|
container: Optional[Container] = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
makedirs("selenium_screenshots/", exist_ok=True)
|
makedirs("selenium_screenshots/", exist_ok=True)
|
||||||
self.driver = self._get_driver()
|
self.driver = self._get_driver()
|
||||||
self.driver.maximize_window()
|
self.driver.maximize_window()
|
||||||
self.driver.implicitly_wait(30)
|
self.driver.implicitly_wait(10)
|
||||||
self.wait = WebDriverWait(self.driver, 50)
|
self.wait = WebDriverWait(self.driver, 30)
|
||||||
self.apply_default_data()
|
self.apply_default_data()
|
||||||
self.logger = get_logger()
|
self.logger = get_logger()
|
||||||
|
if specs := self.get_container_specs():
|
||||||
|
self.container = self._start_container(specs)
|
||||||
|
|
||||||
|
def _start_container(self, specs: Dict[str, Any]) -> Container:
|
||||||
|
client: DockerClient = from_env()
|
||||||
|
container = client.containers.run(**specs)
|
||||||
|
if "healthcheck" not in specs:
|
||||||
|
return container
|
||||||
|
while True:
|
||||||
|
container.reload()
|
||||||
|
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||||
|
if status == "healthy":
|
||||||
|
return container
|
||||||
|
self.logger.info("Container failed healthcheck")
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Optionally get container specs which will launched on setup, wait for the container to
|
||||||
|
be healthy, and deleted again on tearDown"""
|
||||||
|
return None
|
||||||
|
|
||||||
def _get_driver(self) -> WebDriver:
|
def _get_driver(self) -> WebDriver:
|
||||||
return webdriver.Remote(
|
return webdriver.Remote(
|
||||||
@ -57,6 +82,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
|||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
line["message"], source=line["source"], level=line["level"]
|
line["message"], source=line["source"], level=line["level"]
|
||||||
)
|
)
|
||||||
|
if self.container:
|
||||||
|
self.container.kill()
|
||||||
self.driver.quit()
|
self.driver.quit()
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: "0.10.0-rc3"
|
appVersion: "0.10.3-stable"
|
||||||
description: A Helm chart for passbook.
|
description: A Helm chart for passbook.
|
||||||
name: passbook
|
name: passbook
|
||||||
version: "0.10.0-rc3"
|
version: "0.10.3-stable"
|
||||||
icon: https://github.com/BeryJu/passbook/blob/master/passbook/static/static/passbook/logo.svg
|
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: postgresql
|
- name: postgresql
|
||||||
version: 9.3.2
|
version: 9.4.1
|
||||||
repository: https://charts.bitnami.com/bitnami
|
repository: https://charts.bitnami.com/bitnami
|
||||||
condition: install.postgresql
|
condition: install.postgresql
|
||||||
- name: redis
|
- name: redis
|
||||||
version: 10.7.16
|
version: 10.9.0
|
||||||
repository: https://charts.bitnami.com/bitnami
|
repository: https://charts.bitnami.com/bitnami
|
||||||
condition: install.redis
|
condition: install.redis
|
||||||
|
|||||||
@ -9,7 +9,7 @@ metadata:
|
|||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
k8s.passbook.beryju.org/component: web
|
k8s.passbook.beryju.org/component: web
|
||||||
spec:
|
spec:
|
||||||
replicas: {{ serverReplicas }}
|
replicas: {{ .Values.serverReplicas }}
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||||
@ -22,10 +22,29 @@ spec:
|
|||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
k8s.passbook.beryju.org/component: web
|
k8s.passbook.beryju.org/component: web
|
||||||
spec:
|
spec:
|
||||||
|
affinity:
|
||||||
|
podAntiAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- weight: 1
|
||||||
|
podAffinityTerm:
|
||||||
|
labelSelector:
|
||||||
|
matchExpressions:
|
||||||
|
- key: app.kubernetes.io/name
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- {{ include "passbook.name" . }}
|
||||||
|
- key: app.kubernetes.io/instance
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- {{ .Release.Name }}
|
||||||
|
- key: k8s.passbook.beryju.org/component
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- web
|
||||||
|
topologyKey: "kubernetes.io/hostname"
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: passbook-database-migrations
|
- name: passbook-database-migrations
|
||||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||||
imagePullPolicy: Always
|
|
||||||
args: [migrate]
|
args: [migrate]
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
@ -50,7 +69,6 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}
|
- name: {{ .Chart.Name }}
|
||||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||||
imagePullPolicy: Always
|
|
||||||
args: [server]
|
args: [server]
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
@ -93,7 +111,7 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 100m
|
cpu: 100m
|
||||||
memory: 200M
|
memory: 300M
|
||||||
limits:
|
limits:
|
||||||
cpu: 300m
|
cpu: 300m
|
||||||
memory: 350M
|
memory: 500M
|
||||||
|
|||||||
@ -9,7 +9,7 @@ metadata:
|
|||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
k8s.passbook.beryju.org/component: worker
|
k8s.passbook.beryju.org/component: worker
|
||||||
spec:
|
spec:
|
||||||
replicas: {{ workerReplicas }}
|
replicas: {{ .Values.workerReplicas }}
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||||
@ -22,6 +22,26 @@ spec:
|
|||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
k8s.passbook.beryju.org/component: worker
|
k8s.passbook.beryju.org/component: worker
|
||||||
spec:
|
spec:
|
||||||
|
affinity:
|
||||||
|
podAntiAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- weight: 1
|
||||||
|
podAffinityTerm:
|
||||||
|
labelSelector:
|
||||||
|
matchExpressions:
|
||||||
|
- key: app.kubernetes.io/name
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- {{ include "passbook.name" . }}
|
||||||
|
- key: app.kubernetes.io/instance
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- {{ .Release.Name }}
|
||||||
|
- key: k8s.passbook.beryju.org/component
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- worker
|
||||||
|
topologyKey: "kubernetes.io/hostname"
|
||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}
|
- name: {{ .Chart.Name }}
|
||||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||||
@ -50,7 +70,7 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 150m
|
cpu: 150m
|
||||||
memory: 300M
|
memory: 400M
|
||||||
limits:
|
limits:
|
||||||
cpu: 300m
|
cpu: 300m
|
||||||
memory: 500M
|
memory: 600M
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
image:
|
image:
|
||||||
name: beryju/passbook
|
name: beryju/passbook
|
||||||
name_static: beryju/passbook-static
|
name_static: beryju/passbook-static
|
||||||
tag: 0.10.0-rc3
|
tag: 0.10.3-stable
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap",
|
|||||||
if [[ "$1" == "server" ]]; then
|
if [[ "$1" == "server" ]]; then
|
||||||
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
|
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
|
||||||
elif [[ "$1" == "worker" ]]; then
|
elif [[ "$1" == "worker" ]]; then
|
||||||
celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule
|
celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule -Q passbook,passbook_scheduled
|
||||||
elif [[ "$1" == "migrate" ]]; then
|
elif [[ "$1" == "migrate" ]]; then
|
||||||
# Run system migrations first, run normal migrations after
|
# Run system migrations first, run normal migrations after
|
||||||
python -m lifecycle.migrate
|
python -m lifecycle.migrate
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
"""Gunicorn config"""
|
"""Gunicorn config"""
|
||||||
|
from multiprocessing import cpu_count
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
bind = "0.0.0.0:8000"
|
bind = "0.0.0.0:8000"
|
||||||
workers = 2
|
|
||||||
threads = 4
|
|
||||||
|
|
||||||
user = "passbook"
|
user = "passbook"
|
||||||
group = "passbook"
|
group = "passbook"
|
||||||
@ -40,3 +41,11 @@ logconfig_dict = {
|
|||||||
"gunicorn": {"handlers": ["console"], "level": "INFO", "propagate": False},
|
"gunicorn": {"handlers": ["console"], "level": "INFO", "propagate": False},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# if we're running in kubernetes, use fixed workers because we can scale with more pods
|
||||||
|
# otherwise (assume docker-compose), use as much as we can
|
||||||
|
if Path("/var/run/secrets/kubernetes.io").exists():
|
||||||
|
workers = 2
|
||||||
|
else:
|
||||||
|
worker = cpu_count() * 2 + 1
|
||||||
|
threads = 4
|
||||||
|
|||||||
@ -30,7 +30,10 @@ nav:
|
|||||||
- OAuth2: providers/oauth2.md
|
- OAuth2: providers/oauth2.md
|
||||||
- SAML: providers/saml.md
|
- SAML: providers/saml.md
|
||||||
- Proxy: providers/proxy.md
|
- Proxy: providers/proxy.md
|
||||||
- Outposts: outposts/outposts.md
|
- Outposts:
|
||||||
|
- Overview: outposts/outposts.md
|
||||||
|
- Deploy on docker-compose: outposts/deploy-docker-compose.md
|
||||||
|
- Deploy on Kubernetes: outposts/deploy-kubernetes.md
|
||||||
- Expressions:
|
- Expressions:
|
||||||
- Overview: expressions/index.md
|
- Overview: expressions/index.md
|
||||||
- Reference:
|
- Reference:
|
||||||
@ -49,9 +52,12 @@ nav:
|
|||||||
- Harbor: integrations/services/harbor/index.md
|
- Harbor: integrations/services/harbor/index.md
|
||||||
- Sentry: integrations/services/sentry/index.md
|
- Sentry: integrations/services/sentry/index.md
|
||||||
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
|
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
|
||||||
|
- VMware vCenter: integrations/services/vmware-vcenter/index.md
|
||||||
- Upgrading:
|
- Upgrading:
|
||||||
- to 0.9: upgrading/to-0.9.md
|
- to 0.9: upgrading/to-0.9.md
|
||||||
- to 0.10: upgrading/to-0.10.md
|
- to 0.10: upgrading/to-0.10.md
|
||||||
|
- Troubleshooting:
|
||||||
|
- Access problems: troubleshooting/access.md
|
||||||
|
|
||||||
repo_name: "BeryJu/passbook"
|
repo_name: "BeryJu/passbook"
|
||||||
repo_url: https://github.com/BeryJu/passbook
|
repo_url: https://github.com/BeryJu/passbook
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
"""passbook"""
|
"""passbook"""
|
||||||
__version__ = "0.10.0-rc3"
|
__version__ = "0.10.3-stable"
|
||||||
|
|||||||
@ -15,11 +15,8 @@ class CodeMirrorWidget(forms.Textarea):
|
|||||||
self.mode = mode
|
self.mode = mode
|
||||||
|
|
||||||
def render(self, *args, **kwargs):
|
def render(self, *args, **kwargs):
|
||||||
if "attrs" not in kwargs:
|
attrs = kwargs.setdefault("attrs", {})
|
||||||
kwargs["attrs"] = {}
|
attrs.setdefault("class", "")
|
||||||
attrs = kwargs["attrs"]
|
|
||||||
if "class" not in attrs:
|
|
||||||
attrs["class"] = ""
|
|
||||||
attrs["class"] += " codemirror"
|
attrs["class"] += " codemirror"
|
||||||
attrs["data-cm-mode"] = self.mode
|
attrs["data-cm-mode"] = self.mode
|
||||||
return super().render(*args, **kwargs)
|
return super().render(*args, **kwargs)
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class UserForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
fields = ["username", "name", "email", "is_staff", "is_active", "attributes"]
|
fields = ["username", "name", "email", "is_active", "attributes"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput,
|
"name": forms.TextInput,
|
||||||
"attributes": CodeMirrorWidget,
|
"attributes": CodeMirrorWidget,
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td role="cell">
|
<td role="cell">
|
||||||
<span>
|
<span>
|
||||||
{{ group.user_set.all|length }}
|
{{ group.users.all|length }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@ -69,6 +69,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-update' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-update' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-delete' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-delete' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||||
|
<a href="https://passbook.beryju.org/outposts/outposts/#deploy">{% trans 'Deploy' %}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@ -55,7 +55,7 @@
|
|||||||
<th role="columnheader">
|
<th role="columnheader">
|
||||||
<div>
|
<div>
|
||||||
<div>{{ policy.name }}</div>
|
<div>{{ policy.name }}</div>
|
||||||
{% if not policy.bindings.exists %}
|
{% if not policy.bindings.exists and not policy.promptstage_set.exists %}
|
||||||
<i class="pf-icon pf-icon-warning-triangle"></i>
|
<i class="pf-icon pf-icon-warning-triangle"></i>
|
||||||
<small>{% trans 'Warning: Policy is not assigned.' %}</small>
|
<small>{% trans 'Warning: Policy is not assigned.' %}</small>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from django.test import Client, TestCase
|
|||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
|
|
||||||
from passbook.admin.urls import urlpatterns
|
from passbook.admin.urls import urlpatterns
|
||||||
from passbook.core.models import User
|
from passbook.core.models import Group, User
|
||||||
from passbook.lib.utils.reflection import get_apps
|
from passbook.lib.utils.reflection import get_apps
|
||||||
|
|
||||||
|
|
||||||
@ -16,7 +16,9 @@ class TestAdmin(TestCase):
|
|||||||
"""Generic admin tests"""
|
"""Generic admin tests"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = User.objects.create_superuser(username="test")
|
self.user = User.objects.create_user(username="test")
|
||||||
|
self.user.pb_groups.add(Group.objects.filter(is_superuser=True).first())
|
||||||
|
self.user.save()
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
|||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
|||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from django.contrib.auth.mixins import (
|
|||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import DetailView, FormView, ListView, UpdateView
|
from django.views.generic import DetailView, FormView, ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
|||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
|||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
"""passbook administration overview"""
|
"""passbook administration overview"""
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.shortcuts import redirect, reverse
|
from django.shortcuts import redirect, reverse
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from packaging.version import Version, parse
|
from packaging.version import LegacyVersion, Version, parse
|
||||||
from requests import RequestException, get
|
from requests import RequestException, get
|
||||||
|
|
||||||
from passbook import __version__
|
from passbook import __version__
|
||||||
@ -16,7 +18,7 @@ from passbook.stages.invitation.models import Invitation
|
|||||||
VERSION_CACHE_KEY = "passbook_latest_version"
|
VERSION_CACHE_KEY = "passbook_latest_version"
|
||||||
|
|
||||||
|
|
||||||
def latest_version() -> Version:
|
def latest_version() -> Union[LegacyVersion, Version]:
|
||||||
"""Get latest release from GitHub, cached"""
|
"""Get latest release from GitHub, cached"""
|
||||||
if not cache.get(VERSION_CACHE_KEY):
|
if not cache.get(VERSION_CACHE_KEY):
|
||||||
try:
|
try:
|
||||||
@ -45,7 +47,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs["application_count"] = len(Application.objects.all())
|
kwargs["application_count"] = len(Application.objects.all())
|
||||||
kwargs["policy_count"] = len(Policy.objects.all())
|
kwargs["policy_count"] = len(Policy.objects.all())
|
||||||
kwargs["user_count"] = len(User.objects.all())
|
kwargs["user_count"] = len(User.objects.all()) - 1 # Remove anonymous user
|
||||||
kwargs["provider_count"] = len(Provider.objects.all())
|
kwargs["provider_count"] = len(Provider.objects.all())
|
||||||
kwargs["source_count"] = len(Source.objects.all())
|
kwargs["source_count"] = len(Source.objects.all())
|
||||||
kwargs["stage_count"] = len(Stage.objects.all())
|
kwargs["stage_count"] = len(Stage.objects.all())
|
||||||
@ -58,7 +60,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
|||||||
application=None
|
application=None
|
||||||
)
|
)
|
||||||
kwargs["policies_without_binding"] = len(
|
kwargs["policies_without_binding"] = len(
|
||||||
Policy.objects.filter(bindings__isnull=True)
|
Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
|
||||||
)
|
)
|
||||||
kwargs["cached_policies"] = len(cache.keys("policy_*"))
|
kwargs["cached_policies"] = len(cache.keys("policy_*"))
|
||||||
kwargs["cached_flows"] = len(cache.keys("flow_*"))
|
kwargs["cached_flows"] = len(cache.keys("flow_*"))
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from django.contrib.messages.views import SuccessMessageMixin
|
|||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from django.views.generic.detail import DetailView
|
from django.views.generic.detail import DetailView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from django.contrib.auth.mixins import (
|
|||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
|||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import (
|
from passbook.admin.views.utils import (
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
|||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import (
|
from passbook.admin.views.utils import (
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
|||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import (
|
from passbook.admin.views.utils import (
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
|||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import (
|
from passbook.admin.views.utils import (
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
|||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from django.contrib.auth.mixins import (
|
|||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
|||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"""passbook Token administration"""
|
"""passbook Token administration"""
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from django.http import HttpRequest, HttpResponse
|
|||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import DetailView, ListView, UpdateView
|
from django.views.generic import DetailView, ListView, UpdateView
|
||||||
from guardian.mixins import (
|
from guardian.mixins import (
|
||||||
PermissionListMixin,
|
PermissionListMixin,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"""api v2 urls"""
|
"""api v2 urls"""
|
||||||
from django.conf.urls import url
|
from django.urls import path, re_path
|
||||||
from django.urls import path
|
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg.views import get_schema_view
|
from drf_yasg.views import get_schema_view
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
@ -119,7 +118,7 @@ SchemaView = get_schema_view(
|
|||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(
|
re_path(
|
||||||
r"^swagger(?P<format>\.json|\.yaml)$",
|
r"^swagger(?P<format>\.json|\.yaml)$",
|
||||||
SchemaView.without_ui(cache_timeout=0),
|
SchemaView.without_ui(cache_timeout=0),
|
||||||
name="schema-json",
|
name="schema-json",
|
||||||
|
|||||||
@ -11,7 +11,7 @@ class GroupSerializer(ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
fields = ["pk", "name", "parent", "user_set", "attributes"]
|
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
|
||||||
|
|
||||||
|
|
||||||
class GroupViewSet(ModelViewSet):
|
class GroupViewSet(ModelViewSet):
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import BooleanField, ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from passbook.core.models import User
|
from passbook.core.models import User
|
||||||
@ -8,10 +8,12 @@ from passbook.core.models import User
|
|||||||
class UserSerializer(ModelSerializer):
|
class UserSerializer(ModelSerializer):
|
||||||
"""User Serializer"""
|
"""User Serializer"""
|
||||||
|
|
||||||
|
is_superuser = BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
fields = ["pk", "username", "name", "email"]
|
fields = ["pk", "username", "name", "is_superuser", "email"]
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ModelViewSet):
|
class UserViewSet(ModelViewSet):
|
||||||
|
|||||||
@ -29,7 +29,16 @@ class ApplicationForm(forms.ModelForm):
|
|||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"name": forms.TextInput(),
|
||||||
"meta_launch_url": forms.TextInput(),
|
"meta_launch_url": forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
"placeholder": _(
|
||||||
|
(
|
||||||
|
"If left empty, passbook will try to extract the launch URL "
|
||||||
|
"based on the selected provider."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
"meta_icon_url": forms.TextInput(),
|
"meta_icon_url": forms.TextInput(),
|
||||||
"meta_publisher": forms.TextInput(),
|
"meta_publisher": forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,21 +18,19 @@ class GroupForm(forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
self.initial["members"] = self.instance.user_set.values_list(
|
self.initial["members"] = self.instance.users.values_list("pk", flat=True)
|
||||||
"pk", flat=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
instance = super().save(*args, **kwargs)
|
instance = super().save(*args, **kwargs)
|
||||||
if instance.pk:
|
if instance.pk:
|
||||||
instance.user_set.clear()
|
instance.users.clear()
|
||||||
instance.user_set.add(*self.cleaned_data["members"])
|
instance.users.add(*self.cleaned_data["members"])
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
fields = ["name", "parent", "members", "attributes"]
|
fields = ["name", "is_superuser", "parent", "members", "attributes"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"name": forms.TextInput(),
|
||||||
"attributes": CodeMirrorWidget,
|
"attributes": CodeMirrorWidget,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 3.0.6 on 2020-05-23 16:40
|
# Generated by Django 3.0.6 on 2020-05-23 16:40
|
||||||
|
|
||||||
from django.apps.registry import Apps
|
from django.apps.registry import Apps
|
||||||
from django.db import migrations
|
from django.db import migrations, models
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
|
||||||
@ -15,8 +15,6 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
username="pbadmin", email="root@localhost", name="passbook Default Admin"
|
username="pbadmin", email="root@localhost", name="passbook Default Admin"
|
||||||
)
|
)
|
||||||
pbadmin.set_password("pbadmin") # noqa # nosec
|
pbadmin.set_password("pbadmin") # noqa # nosec
|
||||||
pbadmin.is_superuser = True
|
|
||||||
pbadmin.is_staff = True
|
|
||||||
pbadmin.save()
|
pbadmin.save()
|
||||||
|
|
||||||
|
|
||||||
@ -27,5 +25,15 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.RemoveField(model_name="user", name="is_superuser",),
|
||||||
|
migrations.RemoveField(model_name="user", name="is_staff",),
|
||||||
migrations.RunPython(create_default_user),
|
migrations.RunPython(create_default_user),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="is_superuser",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user", name="is_staff", field=models.BooleanField(default=False)
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
49
passbook/core/migrations/0009_group_is_superuser.py
Normal file
49
passbook/core/migrations/0009_group_is_superuser.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 3.1.1 on 2020-09-15 19:53
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
import passbook.core.models
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_admin_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
Group = apps.get_model("passbook_core", "Group")
|
||||||
|
User = apps.get_model("passbook_core", "User")
|
||||||
|
|
||||||
|
# Creates a default admin group
|
||||||
|
group, _ = Group.objects.using(db_alias).get_or_create(
|
||||||
|
is_superuser=True, defaults={"name": "passbook Admins",}
|
||||||
|
)
|
||||||
|
group.users.set(User.objects.filter(username="pbadmin"))
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_core", "0008_auto_20200824_1532"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(model_name="user", name="is_superuser",),
|
||||||
|
migrations.RemoveField(model_name="user", name="is_staff",),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="pb_groups",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
related_name="users", to="passbook_core.Group"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="group",
|
||||||
|
name="is_superuser",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False, help_text="Users added to this group will be superusers."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(create_default_admin_group),
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name="user", managers=[("objects", passbook.core.models.UserManager()),],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -4,6 +4,7 @@ from typing import Any, Optional, Type
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, QuerySet
|
from django.db.models import Q, QuerySet
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
@ -22,6 +23,7 @@ from passbook.lib.models import CreatedUpdatedModel
|
|||||||
from passbook.policies.models import PolicyBindingModel
|
from passbook.policies.models import PolicyBindingModel
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
PASSBOOK_USER_DEBUG = "passbook_user_debug"
|
||||||
|
|
||||||
|
|
||||||
def default_token_duration():
|
def default_token_duration():
|
||||||
@ -33,7 +35,12 @@ class Group(models.Model):
|
|||||||
"""Custom Group model which supports a basic hierarchy"""
|
"""Custom Group model which supports a basic hierarchy"""
|
||||||
|
|
||||||
group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=80)
|
name = models.CharField(_("name"), max_length=80)
|
||||||
|
is_superuser = models.BooleanField(
|
||||||
|
default=False, help_text=_("Users added to this group will be superusers.")
|
||||||
|
)
|
||||||
|
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
"Group",
|
"Group",
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -51,6 +58,14 @@ class Group(models.Model):
|
|||||||
unique_together = (("name", "parent",),)
|
unique_together = (("name", "parent",),)
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager(DjangoUserManager):
|
||||||
|
"""Custom user manager that doesn't assign is_superuser and is_staff"""
|
||||||
|
|
||||||
|
def create_user(self, username, email=None, password=None, **extra_fields):
|
||||||
|
"""Custom user manager that doesn't assign is_superuser and is_staff"""
|
||||||
|
return self._create_user(username, email, password, **extra_fields)
|
||||||
|
|
||||||
|
|
||||||
class User(GuardianUserMixin, AbstractUser):
|
class User(GuardianUserMixin, AbstractUser):
|
||||||
"""Custom User model to allow easier adding o f user-based settings"""
|
"""Custom User model to allow easier adding o f user-based settings"""
|
||||||
|
|
||||||
@ -58,11 +73,23 @@ class User(GuardianUserMixin, AbstractUser):
|
|||||||
name = models.TextField(help_text=_("User's display name."))
|
name = models.TextField(help_text=_("User's display name."))
|
||||||
|
|
||||||
sources = models.ManyToManyField("Source", through="UserSourceConnection")
|
sources = models.ManyToManyField("Source", through="UserSourceConnection")
|
||||||
pb_groups = models.ManyToManyField("Group")
|
pb_groups = models.ManyToManyField("Group", related_name="users")
|
||||||
password_change_date = models.DateTimeField(auto_now_add=True)
|
password_change_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
attributes = models.JSONField(default=dict, blank=True)
|
attributes = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
objects = UserManager()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_superuser(self) -> bool:
|
||||||
|
"""Get supseruser status based on membership in a group with superuser status"""
|
||||||
|
return self.pb_groups.filter(is_superuser=True).exists()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_staff(self) -> bool:
|
||||||
|
"""superuser == staff user"""
|
||||||
|
return self.is_superuser
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
if self.pk:
|
if self.pk:
|
||||||
password_changed.send(sender=self, user=self, password=password)
|
password_changed.send(sender=self, user=self, password=password)
|
||||||
@ -92,6 +119,12 @@ class Provider(models.Model):
|
|||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def launch_url(self) -> Optional[str]:
|
||||||
|
"""URL to this provider and initiate authorization for the user.
|
||||||
|
Can return None for providers that are not URL-based"""
|
||||||
|
return None
|
||||||
|
|
||||||
def form(self) -> Type[ModelForm]:
|
def form(self) -> Type[ModelForm]:
|
||||||
"""Return Form class used to edit this object"""
|
"""Return Form class used to edit this object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -119,6 +152,14 @@ class Application(PolicyBindingModel):
|
|||||||
meta_description = models.TextField(default="", blank=True)
|
meta_description = models.TextField(default="", blank=True)
|
||||||
meta_publisher = models.TextField(default="", blank=True)
|
meta_publisher = models.TextField(default="", blank=True)
|
||||||
|
|
||||||
|
def get_launch_url(self) -> Optional[str]:
|
||||||
|
"""Get launch URL if set, otherwise attempt to get launch URL based on provider."""
|
||||||
|
if self.meta_launch_url:
|
||||||
|
return self.meta_launch_url
|
||||||
|
if self.provider:
|
||||||
|
return self.provider.launch_url
|
||||||
|
return None
|
||||||
|
|
||||||
def get_provider(self) -> Optional[Provider]:
|
def get_provider(self) -> Optional[Provider]:
|
||||||
"""Get casted provider instance"""
|
"""Get casted provider instance"""
|
||||||
if not self.provider:
|
if not self.provider:
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
"""passbook core tasks"""
|
"""passbook core tasks"""
|
||||||
|
from django.utils.timezone import now
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import ExpiringModel
|
from passbook.core.models import ExpiringModel
|
||||||
@ -12,5 +13,10 @@ def clean_expired_models():
|
|||||||
"""Remove expired objects"""
|
"""Remove expired objects"""
|
||||||
for cls in ExpiringModel.__subclasses__():
|
for cls in ExpiringModel.__subclasses__():
|
||||||
cls: ExpiringModel
|
cls: ExpiringModel
|
||||||
amount, _ = cls.filter_not_expired().delete()
|
amount, _ = (
|
||||||
|
cls.objects.all()
|
||||||
|
.exclude(expiring=False)
|
||||||
|
.exclude(expiring=True, expires__gt=now())
|
||||||
|
.delete()
|
||||||
|
)
|
||||||
LOGGER.debug("Deleted expired models", model=cls, amount=amount)
|
LOGGER.debug("Deleted expired models", model=cls, amount=amount)
|
||||||
|
|||||||
@ -20,8 +20,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<a class="pf-c-page__header-brand-link">
|
<a class="pf-c-page__header-brand-link">
|
||||||
<img class="pf-c-brand" src="{% static 'passbook/logo.png' %}" alt="" />
|
<div class="pf-c-brand pb-brand">
|
||||||
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" alt="passbook" />
|
<img src="{{ config.passbook.branding.logo }}" alt="passbook icon">
|
||||||
|
{% if config.passbook.branding.title_show %}
|
||||||
|
<small><small>{{ config.passbook.branding.title }}</small></small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-page__header-nav">
|
<div class="pf-c-page__header-nav">
|
||||||
|
|||||||
@ -6,15 +6,17 @@
|
|||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff2' %}" as="font" type="font/woff2" crossorigin>
|
||||||
|
<link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff' %}" as="font" type="font/woff" crossorigin>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
<title>{% block title %}{% trans title|default:"passbook" %}{% endblock %}</title>
|
<title>{% block title %}{% trans title|default:config.passbook.branding.title %}{% endblock %}</title>
|
||||||
<link rel="icon" type="image/png" href="{% static 'passbook/logo.png' %}">
|
<link rel="icon" type="image/png" href="{% static 'passbook/logo.png' %}">
|
||||||
<link rel="shortcut icon" type="image/png" href="{% static 'passbook/logo.png' %}">
|
<link rel="shortcut icon" type="image/png" href="{% static 'passbook/logo.png' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'node_modules/@patternfly/patternfly/patternfly.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'node_modules/@patternfly/patternfly/patternfly.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'node_modules/@patternfly/patternfly/patternfly-addons.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'node_modules/@patternfly/patternfly/patternfly-addons.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'passbook/pf.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'passbook/passbook.css' %}">
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
@ -35,6 +37,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<script src="{% static 'passbook/pf.js' %}"></script>
|
<script src="{% static 'passbook/passbook.js' %}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -22,8 +22,12 @@
|
|||||||
<div class="pf-c-login">
|
<div class="pf-c-login">
|
||||||
<div class="pf-c-login__container">
|
<div class="pf-c-login__container">
|
||||||
<header class="pf-c-login__header">
|
<header class="pf-c-login__header">
|
||||||
<img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;" alt="passbook icon" />
|
<div class="pf-c-brand pb-brand">
|
||||||
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;" alt="passbook branding" />
|
<img src="{{ config.passbook.branding.logo }}" alt="passbook icon" />
|
||||||
|
{% if config.passbook.branding.title_show %}
|
||||||
|
<p>{{ config.passbook.branding.title }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% block main_container %}
|
{% block main_container %}
|
||||||
<main class="pf-c-login__main">
|
<main class="pf-c-login__main">
|
||||||
@ -47,6 +51,13 @@
|
|||||||
<a href="{{ link.href }}">{{ link.name }}</a>
|
<a href="{{ link.href }}">{{ link.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if config.passbook.branding.title != "passbook" %}
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/beryju/passbook">
|
||||||
|
{% trans 'Powered by passbook' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
{% extends 'login/base_full.html' %}
|
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load passbook_utils %}
|
|
||||||
|
|
||||||
{% block card_title %}
|
|
||||||
{% trans 'Permission denied' %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
{% trans 'Permission denied' %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block card %}
|
|
||||||
<form method="POST" class="pf-c-form">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% include 'partials/form.html' %}
|
|
||||||
<div class="pf-c-form__group">
|
|
||||||
<p>
|
|
||||||
<i class="pf-icon pf-icon-error-circle-o"></i>
|
|
||||||
{% trans 'Access denied' %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% if 'back' in request.GET %}
|
|
||||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@ -24,7 +24,7 @@
|
|||||||
{% if applications %}
|
{% if applications %}
|
||||||
<div class="pf-l-gallery pf-m-gutter">
|
<div class="pf-l-gallery pf-m-gutter">
|
||||||
{% for app in applications %}
|
{% for app in applications %}
|
||||||
<a href="{{ app.meta_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
<a href="{{ app.get_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||||
<div class="pf-c-card__header">
|
<div class="pf-c-card__header">
|
||||||
{% if not app.meta_icon_url %}
|
{% if not app.meta_icon_url %}
|
||||||
<i class="pf-icon pf-icon-arrow"></i>
|
<i class="pf-icon pf-icon-arrow"></i>
|
||||||
|
|||||||
@ -13,7 +13,7 @@ class TestOverviewViews(TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.create_superuser(
|
self.user = User.objects.create_user(
|
||||||
username="unittest user",
|
username="unittest user",
|
||||||
email="unittest@example.com",
|
email="unittest@example.com",
|
||||||
password="".join(
|
password="".join(
|
||||||
|
|||||||
@ -13,7 +13,7 @@ class TestUserViews(TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.create_superuser(
|
self.user = User.objects.create_user(
|
||||||
username="unittest user",
|
username="unittest user",
|
||||||
email="unittest@example.com",
|
email="unittest@example.com",
|
||||||
password="".join(
|
password="".join(
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
"""passbook util view tests"""
|
|
||||||
import string
|
|
||||||
from random import SystemRandom
|
|
||||||
|
|
||||||
from django.test import RequestFactory, TestCase
|
|
||||||
|
|
||||||
from passbook.core.models import User
|
|
||||||
from passbook.core.views.utils import PermissionDeniedView
|
|
||||||
|
|
||||||
|
|
||||||
class TestUtilViews(TestCase):
|
|
||||||
"""Test Utility Views"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.user = User.objects.create_superuser(
|
|
||||||
username="unittest user",
|
|
||||||
email="unittest@example.com",
|
|
||||||
password="".join(
|
|
||||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
|
||||||
for _ in range(8)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
|
|
||||||
def test_permission_denied_view(self):
|
|
||||||
"""Test PermissionDeniedView"""
|
|
||||||
request = self.factory.get("something")
|
|
||||||
request.user = self.user
|
|
||||||
response = PermissionDeniedView.as_view()(request)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
"""passbook core utils view"""
|
|
||||||
from django.utils.translation import ugettext as _
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionDeniedView(TemplateView):
|
|
||||||
"""Generic Permission denied view"""
|
|
||||||
|
|
||||||
template_name = "login/denied.html"
|
|
||||||
title = _("Permission denied.")
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
kwargs["title"] = self.title
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
|
|
||||||
class FlowNonApplicableException(BaseException):
|
class FlowNonApplicableException(BaseException):
|
||||||
"""Exception raised when a Flow does not apply to a user."""
|
"""Flow does not apply to current user (denied by policy)."""
|
||||||
|
|
||||||
|
|
||||||
class EmptyFlowException(BaseException):
|
class EmptyFlowException(BaseException):
|
||||||
"""Exception raised when a Flow Plan is empty"""
|
"""Flow has no stages."""
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from django.core.management.base import BaseCommand, no_translations
|
|||||||
from passbook.flows.transfer.importer import FlowImporter
|
from passbook.flows.transfer.importer import FlowImporter
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand): # pragma: no cover
|
||||||
"""Apply flow from commandline"""
|
"""Apply flow from commandline"""
|
||||||
|
|
||||||
@no_translations
|
@no_translations
|
||||||
|
|||||||
@ -52,7 +52,7 @@ def create_default_source_enrollment_flow(
|
|||||||
|
|
||||||
# PromptStage to ask user for their username
|
# PromptStage to ask user for their username
|
||||||
prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
|
prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
|
||||||
name="default-source-enrollment-username-prompt",
|
name="Welcome to passbook! Please select a username.",
|
||||||
)
|
)
|
||||||
prompt, _ = Prompt.objects.using(db_alias).update_or_create(
|
prompt, _ = Prompt.objects.using(db_alias).update_or_create(
|
||||||
field_key="username",
|
field_key="username",
|
||||||
|
|||||||
@ -115,11 +115,12 @@ const updateFormAction = (form) => {
|
|||||||
for (let index = 0; index < form.elements.length; index++) {
|
for (let index = 0; index < form.elements.length; index++) {
|
||||||
const element = form.elements[index];
|
const element = form.elements[index];
|
||||||
if (element.value === form.action) {
|
if (element.value === form.action) {
|
||||||
console.log("Found Form action URL in form elements, not changing form action.");
|
console.log("pb-flow: Found Form action URL in form elements, not changing form action.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
form.action = flowBodyUrl;
|
form.action = flowBodyUrl;
|
||||||
|
console.log(`pb-flow: updated form.action ${flowBodyUrl}`);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const checkAutosubmit = (form) => {
|
const checkAutosubmit = (form) => {
|
||||||
@ -129,11 +130,11 @@ const checkAutosubmit = (form) => {
|
|||||||
};
|
};
|
||||||
const setFormSubmitHandlers = () => {
|
const setFormSubmitHandlers = () => {
|
||||||
document.querySelectorAll("#flow-body form").forEach(form => {
|
document.querySelectorAll("#flow-body form").forEach(form => {
|
||||||
console.log(`Checking for autosubmit attribute ${form}`);
|
console.log(`pb-flow: Checking for autosubmit attribute ${form}`);
|
||||||
checkAutosubmit(form);
|
checkAutosubmit(form);
|
||||||
console.log(`Setting action for form ${form}`);
|
console.log(`pb-flow: Setting action for form ${form}`);
|
||||||
updateFormAction(form);
|
updateFormAction(form);
|
||||||
console.log(`Adding handler for form ${form}`);
|
console.log(`pb-flow: Adding handler for form ${form}`);
|
||||||
form.addEventListener('submit', (e) => {
|
form.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let formData = new FormData(form);
|
let formData = new FormData(form);
|
||||||
@ -145,6 +146,7 @@ const setFormSubmitHandlers = () => {
|
|||||||
updateCard(data);
|
updateCard(data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
form.classList.add("pb-flow-wrapped");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
"""flow views tests"""
|
"""flow views tests"""
|
||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
from passbook.flows.markers import ReevaluateMarker, StageMarker
|
from passbook.flows.markers import ReevaluateMarker, StageMarker
|
||||||
@ -12,6 +13,7 @@ from passbook.flows.planner import FlowPlan
|
|||||||
from passbook.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
|
from passbook.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
from passbook.policies.dummy.models import DummyPolicy
|
from passbook.policies.dummy.models import DummyPolicy
|
||||||
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
from passbook.policies.models import PolicyBinding
|
from passbook.policies.models import PolicyBinding
|
||||||
from passbook.policies.types import PolicyResult
|
from passbook.policies.types import PolicyResult
|
||||||
from passbook.stages.dummy.models import DummyStage
|
from passbook.stages.dummy.models import DummyStage
|
||||||
@ -20,6 +22,15 @@ POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False))
|
|||||||
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
||||||
|
|
||||||
|
|
||||||
|
def to_stage_response(request: HttpRequest, source: HttpResponse):
|
||||||
|
"""Mock for to_stage_response that returns the original response, so we can check
|
||||||
|
inheritance and member attributes"""
|
||||||
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response)
|
||||||
|
|
||||||
|
|
||||||
class TestFlowExecutor(TestCase):
|
class TestFlowExecutor(TestCase):
|
||||||
"""Test views logic"""
|
"""Test views logic"""
|
||||||
|
|
||||||
@ -48,9 +59,12 @@ class TestFlowExecutor(TestCase):
|
|||||||
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(cancel_mock.call_count, 1)
|
self.assertEqual(cancel_mock.call_count, 2)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
@patch(
|
@patch(
|
||||||
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
|
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
|
||||||
)
|
)
|
||||||
@ -66,9 +80,13 @@ class TestFlowExecutor(TestCase):
|
|||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
self.assertInHTML(FlowNonApplicableException.__doc__, response.rendered_content)
|
self.assertInHTML(FlowNonApplicableException.__doc__, response.rendered_content)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||||
|
)
|
||||||
def test_invalid_empty_flow(self):
|
def test_invalid_empty_flow(self):
|
||||||
"""Tests that an empty flow returns the correct error message"""
|
"""Tests that an empty flow returns the correct error message"""
|
||||||
flow = Flow.objects.create(
|
flow = Flow.objects.create(
|
||||||
@ -81,7 +99,8 @@ class TestFlowExecutor(TestCase):
|
|||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
self.assertInHTML(EmptyFlowException.__doc__, response.rendered_content)
|
self.assertInHTML(EmptyFlowException.__doc__, response.rendered_content)
|
||||||
|
|
||||||
def test_invalid_flow_redirect(self):
|
def test_invalid_flow_redirect(self):
|
||||||
@ -96,8 +115,10 @@ class TestFlowExecutor(TestCase):
|
|||||||
dest = "/unique-string"
|
dest = "/unique-string"
|
||||||
url = reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug})
|
url = reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug})
|
||||||
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
|
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.url, dest)
|
self.assertJSONEqual(
|
||||||
|
force_str(response.content), {"type": "redirect", "to": dest},
|
||||||
|
)
|
||||||
|
|
||||||
def test_multi_stage_flow(self):
|
def test_multi_stage_flow(self):
|
||||||
"""Test a full flow with multiple stages"""
|
"""Test a full flow with multiple stages"""
|
||||||
@ -247,7 +268,7 @@ class TestFlowExecutor(TestCase):
|
|||||||
response = self.client.post(exec_url)
|
response = self.client.post(exec_url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
force_text(response.content),
|
force_str(response.content),
|
||||||
{"type": "redirect", "to": reverse("passbook_core:overview")},
|
{"type": "redirect", "to": reverse("passbook_core:overview")},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -293,7 +314,7 @@ class TestFlowExecutor(TestCase):
|
|||||||
# First request, run the planner
|
# First request, run the planner
|
||||||
response = self.client.get(exec_url)
|
response = self.client.get(exec_url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn("dummy1", force_text(response.content))
|
self.assertIn("dummy1", force_str(response.content))
|
||||||
|
|
||||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||||
|
|
||||||
@ -316,13 +337,13 @@ class TestFlowExecutor(TestCase):
|
|||||||
# but it won't save it, hence we cant' check the plan
|
# but it won't save it, hence we cant' check the plan
|
||||||
response = self.client.get(exec_url)
|
response = self.client.get(exec_url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn("dummy4", force_text(response.content))
|
self.assertIn("dummy4", force_str(response.content))
|
||||||
|
|
||||||
# fourth request, this confirms the last stage (dummy4)
|
# fourth request, this confirms the last stage (dummy4)
|
||||||
# We do this request without the patch, so the policy results in false
|
# We do this request without the patch, so the policy results in false
|
||||||
response = self.client.post(exec_url)
|
response = self.client.post(exec_url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
force_text(response.content),
|
force_str(response.content),
|
||||||
{"type": "redirect", "to": reverse("passbook_core:overview")},
|
{"type": "redirect", "to": reverse("passbook_core:overview")},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,12 +6,10 @@ from passbook.flows.views import (
|
|||||||
CancelView,
|
CancelView,
|
||||||
FlowExecutorShellView,
|
FlowExecutorShellView,
|
||||||
FlowExecutorView,
|
FlowExecutorView,
|
||||||
FlowPermissionDeniedView,
|
|
||||||
ToDefaultFlow,
|
ToDefaultFlow,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("-/denied/", FlowPermissionDeniedView.as_view(), name="denied"),
|
|
||||||
path(
|
path(
|
||||||
"-/default/authentication/",
|
"-/default/authentication/",
|
||||||
ToDefaultFlow.as_view(designation=FlowDesignation.AUTHENTICATION),
|
ToDefaultFlow.as_view(designation=FlowDesignation.AUTHENTICATION),
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from django.http import (
|
|||||||
HttpResponseRedirect,
|
HttpResponseRedirect,
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
)
|
)
|
||||||
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||||
@ -17,13 +17,13 @@ from django.views.generic import TemplateView, View
|
|||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.audit.models import cleanse_dict
|
from passbook.audit.models import cleanse_dict
|
||||||
from passbook.core.views.utils import PermissionDeniedView
|
from passbook.core.models import PASSBOOK_USER_DEBUG
|
||||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
from passbook.flows.models import Flow, FlowDesignation, Stage
|
from passbook.flows.models import Flow, FlowDesignation, Stage
|
||||||
from passbook.flows.planner import FlowPlan, FlowPlanner
|
from passbook.flows.planner import FlowPlan, FlowPlanner
|
||||||
from passbook.lib.utils.reflection import class_to_path
|
from passbook.lib.utils.reflection import class_to_path
|
||||||
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
|
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||||
from passbook.lib.views import bad_request_message
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
# Argument used to redirect user after login
|
# Argument used to redirect user after login
|
||||||
@ -54,7 +54,7 @@ class FlowExecutorView(View):
|
|||||||
LOGGER.debug("f(exec): Redirecting to next on fail")
|
LOGGER.debug("f(exec): Redirecting to next on fail")
|
||||||
return redirect(self.request.GET.get(NEXT_ARG_NAME))
|
return redirect(self.request.GET.get(NEXT_ARG_NAME))
|
||||||
message = exc.__doc__ if exc.__doc__ else str(exc)
|
message = exc.__doc__ if exc.__doc__ else str(exc)
|
||||||
return bad_request_message(self.request, message)
|
return self.stage_invalid(error_message=message)
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
||||||
# Early check if theres an active Plan for the current session
|
# Early check if theres an active Plan for the current session
|
||||||
@ -79,10 +79,10 @@ class FlowExecutorView(View):
|
|||||||
self.plan = self._initiate_plan()
|
self.plan = self._initiate_plan()
|
||||||
except FlowNonApplicableException as exc:
|
except FlowNonApplicableException as exc:
|
||||||
LOGGER.warning("f(exec): Flow not applicable to current user", exc=exc)
|
LOGGER.warning("f(exec): Flow not applicable to current user", exc=exc)
|
||||||
return self.handle_invalid_flow(exc)
|
return to_stage_response(self.request, self.handle_invalid_flow(exc))
|
||||||
except EmptyFlowException as exc:
|
except EmptyFlowException as exc:
|
||||||
LOGGER.warning("f(exec): Flow is empty", exc=exc)
|
LOGGER.warning("f(exec): Flow is empty", exc=exc)
|
||||||
return self.handle_invalid_flow(exc)
|
return to_stage_response(self.request, self.handle_invalid_flow(exc))
|
||||||
# We don't save the Plan after getting the next stage
|
# We don't save the Plan after getting the next stage
|
||||||
# as it hasn't been successfully passed yet
|
# as it hasn't been successfully passed yet
|
||||||
next_stage = self.plan.next()
|
next_stage = self.plan.next()
|
||||||
@ -115,14 +115,7 @@ class FlowExecutorView(View):
|
|||||||
return to_stage_response(request, stage_response)
|
return to_stage_response(request, stage_response)
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
LOGGER.exception(exc)
|
LOGGER.exception(exc)
|
||||||
return to_stage_response(
|
return to_stage_response(request, FlowErrorResponse(request, exc))
|
||||||
request,
|
|
||||||
render(
|
|
||||||
request,
|
|
||||||
"flows/error.html",
|
|
||||||
{"error": exc, "tb": "".join(format_tb(exc.__traceback__))},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
"""pass post request to current stage"""
|
"""pass post request to current stage"""
|
||||||
@ -137,14 +130,7 @@ class FlowExecutorView(View):
|
|||||||
return to_stage_response(request, stage_response)
|
return to_stage_response(request, stage_response)
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
LOGGER.exception(exc)
|
LOGGER.exception(exc)
|
||||||
return to_stage_response(
|
return to_stage_response(request, FlowErrorResponse(request, exc))
|
||||||
request,
|
|
||||||
render(
|
|
||||||
request,
|
|
||||||
"flows/error.html",
|
|
||||||
{"error": exc, "tb": "".join(format_tb(exc.__traceback__))},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _initiate_plan(self) -> FlowPlan:
|
def _initiate_plan(self) -> FlowPlan:
|
||||||
planner = FlowPlanner(self.flow)
|
planner = FlowPlanner(self.flow)
|
||||||
@ -193,12 +179,17 @@ class FlowExecutorView(View):
|
|||||||
)
|
)
|
||||||
return self._flow_done()
|
return self._flow_done()
|
||||||
|
|
||||||
def stage_invalid(self) -> HttpResponse:
|
def stage_invalid(self, error_message: Optional[str] = None) -> HttpResponse:
|
||||||
"""Callback used stage when data is correct but a policy denies access
|
"""Callback used stage when data is correct but a policy denies access
|
||||||
or the user account is disabled."""
|
or the user account is disabled.
|
||||||
|
|
||||||
|
Optionally, an exception can be passed, which will be shown if the current user
|
||||||
|
is a superuser."""
|
||||||
LOGGER.debug("f(exec): Stage invalid", flow_slug=self.flow.slug)
|
LOGGER.debug("f(exec): Stage invalid", flow_slug=self.flow.slug)
|
||||||
self.cancel()
|
self.cancel()
|
||||||
return redirect_with_qs("passbook_flows:denied", self.request.GET)
|
response = AccessDeniedResponse(self.request)
|
||||||
|
response.error_message = error_message
|
||||||
|
return response
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
"""Cancel current execution and return a redirect"""
|
"""Cancel current execution and return a redirect"""
|
||||||
@ -212,8 +203,30 @@ class FlowExecutorView(View):
|
|||||||
del self.request.session[key]
|
del self.request.session[key]
|
||||||
|
|
||||||
|
|
||||||
class FlowPermissionDeniedView(PermissionDeniedView):
|
class FlowErrorResponse(TemplateResponse):
|
||||||
"""User could not be authenticated"""
|
"""Response class when an unhandled error occurs during a stage. Normal users
|
||||||
|
are shown an error message, superusers are shown a full stacktrace."""
|
||||||
|
|
||||||
|
error: Exception
|
||||||
|
|
||||||
|
def __init__(self, request: HttpRequest, error: Exception) -> None:
|
||||||
|
# For some reason pyright complains about keyword argument usage here
|
||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
super().__init__(request=request, template="flows/error.html")
|
||||||
|
self.error = error
|
||||||
|
|
||||||
|
def resolve_context(
|
||||||
|
self, context: Optional[Dict[str, Any]]
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
if not context:
|
||||||
|
context = {}
|
||||||
|
context["error"] = self.error
|
||||||
|
if self._request.user and self._request.user.is_authenticated:
|
||||||
|
if self._request.user.is_superuser or self._request.user.attributes.get(
|
||||||
|
PASSBOOK_USER_DEBUG, False
|
||||||
|
):
|
||||||
|
context["tb"] = "".join(format_tb(self.error.__traceback__))
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class FlowExecutorShellView(TemplateView):
|
class FlowExecutorShellView(TemplateView):
|
||||||
|
|||||||
@ -10,6 +10,7 @@ redis:
|
|||||||
password: ''
|
password: ''
|
||||||
cache_db: 0
|
cache_db: 0
|
||||||
message_queue_db: 1
|
message_queue_db: 1
|
||||||
|
ws_db: 2
|
||||||
|
|
||||||
debug: false
|
debug: false
|
||||||
log_level: info
|
log_level: info
|
||||||
@ -21,6 +22,10 @@ error_reporting:
|
|||||||
send_pii: false
|
send_pii: false
|
||||||
|
|
||||||
passbook:
|
passbook:
|
||||||
|
branding:
|
||||||
|
title: passbook
|
||||||
|
title_show: true
|
||||||
|
logo: /static/passbook/logo.svg
|
||||||
# Optionally add links to the footer on the login page
|
# Optionally add links to the footer on the login page
|
||||||
footer_links:
|
footer_links:
|
||||||
- name: Documentation
|
- name: Documentation
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
"""Generic models"""
|
"""Generic models"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.core.validators import URLValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.regex_helper import _lazy_re_compile
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
|
|
||||||
@ -48,3 +52,21 @@ class InheritanceForeignKey(models.ForeignKey):
|
|||||||
"""Custom ForeignKey that uses InheritanceForwardManyToOneDescriptor"""
|
"""Custom ForeignKey that uses InheritanceForwardManyToOneDescriptor"""
|
||||||
|
|
||||||
forward_related_accessor_class = InheritanceForwardManyToOneDescriptor
|
forward_related_accessor_class = InheritanceForwardManyToOneDescriptor
|
||||||
|
|
||||||
|
|
||||||
|
class DomainlessURLValidator(URLValidator):
|
||||||
|
"""Subclass of URLValidator which doesn't check the domain
|
||||||
|
(to allow hostnames without domain)"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.host_re = "(" + self.hostname_re + self.domain_re + "|localhost)"
|
||||||
|
self.regex = _lazy_re_compile(
|
||||||
|
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
|
||||||
|
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
|
||||||
|
r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
|
||||||
|
r"(?::\d{2,5})?" # port
|
||||||
|
r"(?:[/?#][^\s]*)?" # resource path
|
||||||
|
r"\Z",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"""passbook sentry integration"""
|
"""passbook sentry integration"""
|
||||||
from billiard.exceptions import WorkerLostError
|
from billiard.exceptions import WorkerLostError
|
||||||
from botocore.client import ClientError
|
from botocore.client import ClientError
|
||||||
|
from celery.exceptions import CeleryError
|
||||||
from django.core.exceptions import DisallowedHost, ValidationError
|
from django.core.exceptions import DisallowedHost, ValidationError
|
||||||
from django.db import InternalError, OperationalError, ProgrammingError
|
from django.db import InternalError, OperationalError, ProgrammingError
|
||||||
from django_redis.exceptions import ConnectionInterrupted
|
from django_redis.exceptions import ConnectionInterrupted
|
||||||
@ -8,6 +9,7 @@ from redis.exceptions import ConnectionError as RedisConnectionError
|
|||||||
from redis.exceptions import RedisError
|
from redis.exceptions import RedisError
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
from websockets.exceptions import WebSocketException
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -35,6 +37,8 @@ def before_send(event, hint):
|
|||||||
OSError,
|
OSError,
|
||||||
RedisError,
|
RedisError,
|
||||||
SentryIgnoredException,
|
SentryIgnoredException,
|
||||||
|
WebSocketException,
|
||||||
|
CeleryError,
|
||||||
)
|
)
|
||||||
if "exc_info" in hint:
|
if "exc_info" in hint:
|
||||||
_, exc_value, _ = hint["exc_info"]
|
_, exc_value, _ = hint["exc_info"]
|
||||||
|
|||||||
@ -1,11 +1,5 @@
|
|||||||
"""passbook lib template utilities"""
|
"""passbook lib template utilities"""
|
||||||
from django.template import Context, Template, loader
|
from django.template import Context, loader
|
||||||
|
|
||||||
|
|
||||||
def render_from_string(tmpl: str, ctx: Context) -> str:
|
|
||||||
"""Render template from string to string"""
|
|
||||||
template = Template(tmpl)
|
|
||||||
return template.render(ctx)
|
|
||||||
|
|
||||||
|
|
||||||
def render_to_string(template_path: str, ctx: Context) -> str:
|
def render_to_string(template_path: str, ctx: Context) -> str:
|
||||||
|
|||||||
@ -27,12 +27,12 @@ class CreateAssignPermView(CreateView):
|
|||||||
|
|
||||||
|
|
||||||
def bad_request_message(
|
def bad_request_message(
|
||||||
request: HttpRequest, message: str, title="Bad Request"
|
request: HttpRequest,
|
||||||
|
message: str,
|
||||||
|
title="Bad Request",
|
||||||
|
template="error/generic.html",
|
||||||
) -> TemplateResponse:
|
) -> TemplateResponse:
|
||||||
"""Return generic error page with message, with status code set to 400"""
|
"""Return generic error page with message, with status code set to 400"""
|
||||||
return TemplateResponse(
|
return TemplateResponse(
|
||||||
request,
|
request, template, {"message": message, "card_title": _(title)}, status=400,
|
||||||
"error/generic.html",
|
|
||||||
{"message": message, "card_title": _(title)},
|
|
||||||
status=400,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
"""Kubernetes deployment controller"""
|
"""Kubernetes deployment controller"""
|
||||||
|
from base64 import b64encode
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from kubernetes.client import (
|
from kubernetes.client import (
|
||||||
@ -24,6 +25,11 @@ from passbook import __version__
|
|||||||
from passbook.outposts.controllers.base import BaseController
|
from passbook.outposts.controllers.base import BaseController
|
||||||
|
|
||||||
|
|
||||||
|
def b64encode_str(input_string: str) -> str:
|
||||||
|
"""base64 encode string"""
|
||||||
|
return b64encode(input_string.encode()).decode()
|
||||||
|
|
||||||
|
|
||||||
class KubernetesController(BaseController):
|
class KubernetesController(BaseController):
|
||||||
"""Manage deployment of outpost in kubernetes"""
|
"""Manage deployment of outpost in kubernetes"""
|
||||||
|
|
||||||
@ -37,9 +43,9 @@ class KubernetesController(BaseController):
|
|||||||
with StringIO() as _str:
|
with StringIO() as _str:
|
||||||
dump_all(
|
dump_all(
|
||||||
[
|
[
|
||||||
self.get_deployment_secret(),
|
self.get_deployment_secret().to_dict(),
|
||||||
self.get_deployment(),
|
self.get_deployment().to_dict(),
|
||||||
self.get_service(),
|
self.get_service().to_dict(),
|
||||||
],
|
],
|
||||||
stream=_str,
|
stream=_str,
|
||||||
default_flow_style=False,
|
default_flow_style=False,
|
||||||
@ -63,15 +69,18 @@ class KubernetesController(BaseController):
|
|||||||
def get_deployment_secret(self) -> V1Secret:
|
def get_deployment_secret(self) -> V1Secret:
|
||||||
"""Get secret with token and passbook host"""
|
"""Get secret with token and passbook host"""
|
||||||
return V1Secret(
|
return V1Secret(
|
||||||
|
api_version="v1",
|
||||||
|
kind="secret",
|
||||||
|
type="Opaque",
|
||||||
metadata=self.get_object_meta(
|
metadata=self.get_object_meta(
|
||||||
name=f"passbook-outpost-{self.outpost.name}-api"
|
name=f"passbook-outpost-{self.outpost.name}-api"
|
||||||
),
|
),
|
||||||
data={
|
data={
|
||||||
"passbook_host": self.outpost.config.passbook_host,
|
"passbook_host": b64encode_str(self.outpost.config.passbook_host),
|
||||||
"passbook_host_insecure": str(
|
"passbook_host_insecure": b64encode_str(
|
||||||
self.outpost.config.passbook_host_insecure
|
str(self.outpost.config.passbook_host_insecure)
|
||||||
),
|
),
|
||||||
"token": self.outpost.token.token_uuid.hex,
|
"token": b64encode_str(self.outpost.token.token_uuid.hex),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -82,6 +91,8 @@ class KubernetesController(BaseController):
|
|||||||
for port_name, port in self.deployment_ports.items():
|
for port_name, port in self.deployment_ports.items():
|
||||||
ports.append(V1ServicePort(name=port_name, port=port))
|
ports.append(V1ServicePort(name=port_name, port=port))
|
||||||
return V1Service(
|
return V1Service(
|
||||||
|
api_version="v1",
|
||||||
|
kind="service",
|
||||||
metadata=meta,
|
metadata=meta,
|
||||||
spec=V1ServiceSpec(ports=ports, selector=meta.labels, type="ClusterIP"),
|
spec=V1ServiceSpec(ports=ports, selector=meta.labels, type="ClusterIP"),
|
||||||
)
|
)
|
||||||
@ -94,6 +105,8 @@ class KubernetesController(BaseController):
|
|||||||
container_ports.append(V1ContainerPort(container_port=port, name=port_name))
|
container_ports.append(V1ContainerPort(container_port=port, name=port_name))
|
||||||
meta = self.get_object_meta(name=f"passbook-outpost-{self.outpost.name}")
|
meta = self.get_object_meta(name=f"passbook-outpost-{self.outpost.name}")
|
||||||
return V1Deployment(
|
return V1Deployment(
|
||||||
|
api_version="apps/v1",
|
||||||
|
kind="deployment",
|
||||||
metadata=meta,
|
metadata=meta,
|
||||||
spec=V1DeploymentSpec(
|
spec=V1DeploymentSpec(
|
||||||
replicas=1,
|
replicas=1,
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
"""Outpost models"""
|
"""Outpost models"""
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import dumps, loads
|
|
||||||
from typing import Iterable, Optional
|
from typing import Iterable, Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from dacite import from_dict
|
from dacite import from_dict
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import models
|
from django.db import models, transaction
|
||||||
|
from django.db.models.base import Model
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from guardian.models import UserObjectPermission
|
||||||
from guardian.shortcuts import assign_perm
|
from guardian.shortcuts import assign_perm
|
||||||
|
|
||||||
from passbook.core.models import Provider, Token, TokenIntents, User
|
from passbook.core.models import Provider, Token, TokenIntents, User
|
||||||
@ -30,13 +31,17 @@ class OutpostConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OutpostModel:
|
class OutpostModel(Model):
|
||||||
"""Base model for providers that need more objects than just themselves"""
|
"""Base model for providers that need more objects than just themselves"""
|
||||||
|
|
||||||
def get_required_objects(self) -> Iterable[models.Model]:
|
def get_required_objects(self) -> Iterable[models.Model]:
|
||||||
"""Return a list of all required objects"""
|
"""Return a list of all required objects"""
|
||||||
return [self]
|
return [self]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class OutpostType(models.TextChoices):
|
class OutpostType(models.TextChoices):
|
||||||
"""Outpost types, currently only the reverse proxy is available"""
|
"""Outpost types, currently only the reverse proxy is available"""
|
||||||
@ -79,12 +84,12 @@ class Outpost(models.Model):
|
|||||||
@property
|
@property
|
||||||
def config(self) -> OutpostConfig:
|
def config(self) -> OutpostConfig:
|
||||||
"""Load config as OutpostConfig object"""
|
"""Load config as OutpostConfig object"""
|
||||||
return from_dict(OutpostConfig, loads(self._config))
|
return from_dict(OutpostConfig, self._config)
|
||||||
|
|
||||||
@config.setter
|
@config.setter
|
||||||
def config(self, value):
|
def config(self, value):
|
||||||
"""Dump config into json"""
|
"""Dump config into json"""
|
||||||
self._config = dumps(asdict(value))
|
self._config = asdict(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def health_cache_key(self) -> str:
|
def health_cache_key(self) -> str:
|
||||||
@ -100,24 +105,24 @@ class Outpost(models.Model):
|
|||||||
return datetime.fromtimestamp(value)
|
return datetime.fromtimestamp(value)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _create_user(self) -> User:
|
|
||||||
"""Create user and assign permissions for all required objects"""
|
|
||||||
user: User = User.objects.create(username=f"pb-outpost-{self.uuid.hex}")
|
|
||||||
user.set_unusable_password()
|
|
||||||
user.save()
|
|
||||||
for model in self.get_required_objects():
|
|
||||||
assign_perm(
|
|
||||||
f"{model._meta.app_label}.view_{model._meta.model_name}", user, model
|
|
||||||
)
|
|
||||||
return user
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user(self) -> User:
|
def user(self) -> User:
|
||||||
"""Get/create user with access to all required objects"""
|
"""Get/create user with access to all required objects"""
|
||||||
user = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}")
|
users = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}")
|
||||||
if user.exists():
|
if not users.exists():
|
||||||
return user.first()
|
user: User = User.objects.create(username=f"pb-outpost-{self.uuid.hex}")
|
||||||
return self._create_user()
|
user.set_unusable_password()
|
||||||
|
user.save()
|
||||||
|
else:
|
||||||
|
user = users.first()
|
||||||
|
# To ensure the user only has the correct permissions, we delete all of them and re-add
|
||||||
|
# the ones the user needs
|
||||||
|
with transaction.atomic():
|
||||||
|
UserObjectPermission.objects.filter(user=user).delete()
|
||||||
|
for model in self.get_required_objects():
|
||||||
|
code_name = f"{model._meta.app_label}.view_{model._meta.model_name}"
|
||||||
|
assign_perm(code_name, user, model)
|
||||||
|
return user
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def token(self) -> Token:
|
def token(self) -> Token:
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
"""Outposts Settings"""
|
"""Outposts Settings"""
|
||||||
from celery.schedules import crontab
|
# from celery.schedules import crontab
|
||||||
|
|
||||||
CELERY_BEAT_SCHEDULE = {
|
# CELERY_BEAT_SCHEDULE = {
|
||||||
"outposts_k8s": {
|
# "outposts_k8s": {
|
||||||
"task": "passbook.outposts.tasks.outpost_k8s_controller",
|
# "task": "passbook.outposts.tasks.outpost_k8s_controller",
|
||||||
"schedule": crontab(minute="*/5"), # Run every 5 minutes
|
# "schedule": crontab(minute="*/5"), # Run every 5 minutes
|
||||||
"options": {"queue": "passbook_scheduled"},
|
# "options": {"queue": "passbook_scheduled"},
|
||||||
}
|
# }
|
||||||
}
|
# }
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
"""passbook outpost signals"""
|
"""passbook outpost signals"""
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from channels.layers import get_channel_layer
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.lib.utils.reflection import class_to_path
|
||||||
from passbook.outposts.models import Outpost, OutpostModel
|
from passbook.outposts.models import Outpost, OutpostModel
|
||||||
|
from passbook.outposts.tasks import outpost_send_update
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Outpost)
|
@receiver(post_save, sender=Outpost)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def ensure_user_and_token(sender, instance, **_):
|
def ensure_user_and_token(sender, instance: Model, **_):
|
||||||
"""Ensure that token is created/updated on save"""
|
"""Ensure that token is created/updated on save"""
|
||||||
_ = instance.token
|
_ = instance.token
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
@receiver(post_save)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def post_save_update(sender, instance, **_):
|
def post_save_update(sender, instance: Model, **_):
|
||||||
"""If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved,
|
"""If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved,
|
||||||
we send a message down the relevant OutpostModels WS connection to trigger an update"""
|
we send a message down the relevant OutpostModels WS connection to trigger an update"""
|
||||||
if isinstance(instance, OutpostModel):
|
if isinstance(instance, OutpostModel):
|
||||||
LOGGER.debug("triggering outpost update from outpostmodel", instance=instance)
|
LOGGER.debug("triggering outpost update from outpostmodel", instance=instance)
|
||||||
_send_update(instance)
|
outpost_send_update.delay(class_to_path(instance.__class__), instance.pk)
|
||||||
return
|
return
|
||||||
|
|
||||||
for field in instance._meta.get_fields():
|
for field in instance._meta.get_fields():
|
||||||
@ -46,13 +46,4 @@ def post_save_update(sender, instance, **_):
|
|||||||
# Because the Outpost Model has an M2M to Provider,
|
# Because the Outpost Model has an M2M to Provider,
|
||||||
# we have to iterate over the entire QS
|
# we have to iterate over the entire QS
|
||||||
for reverse in getattr(instance, field_name).all():
|
for reverse in getattr(instance, field_name).all():
|
||||||
_send_update(reverse)
|
outpost_send_update(class_to_path(reverse.__class__), reverse.pk)
|
||||||
|
|
||||||
|
|
||||||
def _send_update(outpost_model: Model):
|
|
||||||
"""Send update trigger for each channel of an outpost model"""
|
|
||||||
for outpost in outpost_model.outpost_set.all():
|
|
||||||
channel_layer = get_channel_layer()
|
|
||||||
for channel in outpost.channels:
|
|
||||||
LOGGER.debug("sending update", channel=channel)
|
|
||||||
async_to_sync(channel_layer.send)(channel, {"type": "event.update"})
|
|
||||||
|
|||||||
@ -1,8 +1,22 @@
|
|||||||
"""outpost tasks"""
|
"""outpost tasks"""
|
||||||
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
|
from typing import Any
|
||||||
|
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from channels.layers import get_channel_layer
|
||||||
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.lib.utils.reflection import path_to_class
|
||||||
|
from passbook.outposts.models import (
|
||||||
|
Outpost,
|
||||||
|
OutpostDeploymentType,
|
||||||
|
OutpostModel,
|
||||||
|
OutpostType,
|
||||||
|
)
|
||||||
from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||||
from passbook.root.celery import CELERY_APP
|
from passbook.root.celery import CELERY_APP
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(bind=True)
|
@CELERY_APP.task(bind=True)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
@ -20,3 +34,16 @@ def outpost_k8s_controller_single(self, outpost: str, outpost_type: str):
|
|||||||
"""Launch Kubernetes manager and reconcile deployment/service/etc"""
|
"""Launch Kubernetes manager and reconcile deployment/service/etc"""
|
||||||
if outpost_type == OutpostType.PROXY:
|
if outpost_type == OutpostType.PROXY:
|
||||||
ProxyKubernetesController(outpost).run()
|
ProxyKubernetesController(outpost).run()
|
||||||
|
|
||||||
|
|
||||||
|
@CELERY_APP.task()
|
||||||
|
def outpost_send_update(model_class: str, model_pk: Any):
|
||||||
|
"""Send outpost update to all registered outposts, irregardless to which passbook
|
||||||
|
instance they are connected"""
|
||||||
|
model = path_to_class(model_class)
|
||||||
|
outpost_model: OutpostModel = model.objects.get(pk=model_pk)
|
||||||
|
for outpost in outpost_model.outpost_set.all():
|
||||||
|
channel_layer = get_channel_layer()
|
||||||
|
for channel in outpost.channels:
|
||||||
|
LOGGER.debug("sending update", channel=channel)
|
||||||
|
async_to_sync(channel_layer.send)(channel, {"type": "event.update"})
|
||||||
|
|||||||
60
passbook/outposts/tests.py
Normal file
60
passbook/outposts/tests.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""outpost tests"""
|
||||||
|
from django.test import TestCase
|
||||||
|
from guardian.models import UserObjectPermission
|
||||||
|
|
||||||
|
from passbook.crypto.models import CertificateKeyPair
|
||||||
|
from passbook.flows.models import Flow
|
||||||
|
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
|
||||||
|
from passbook.providers.proxy.models import ProxyProvider
|
||||||
|
|
||||||
|
|
||||||
|
class OutpostTests(TestCase):
|
||||||
|
"""Outpost Tests"""
|
||||||
|
|
||||||
|
def test_service_account_permissions(self):
|
||||||
|
"""Test that the service account has correct permissions"""
|
||||||
|
provider: ProxyProvider = ProxyProvider.objects.create(
|
||||||
|
name="test",
|
||||||
|
internal_host="http://localhost",
|
||||||
|
external_host="http://localhost",
|
||||||
|
authorization_flow=Flow.objects.first(),
|
||||||
|
)
|
||||||
|
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
|
||||||
|
permissions = UserObjectPermission.objects.filter(user=outpost.user)
|
||||||
|
self.assertEqual(len(permissions), 1)
|
||||||
|
self.assertEqual(permissions[0].object_pk, str(outpost.pk))
|
||||||
|
|
||||||
|
# We add a provider, user should only have access to outpost and provider
|
||||||
|
outpost.providers.add(provider)
|
||||||
|
outpost.save()
|
||||||
|
permissions = UserObjectPermission.objects.filter(user=outpost.user).order_by(
|
||||||
|
"content_type__model"
|
||||||
|
)
|
||||||
|
self.assertEqual(len(permissions), 2)
|
||||||
|
self.assertEqual(permissions[0].object_pk, str(outpost.pk))
|
||||||
|
self.assertEqual(permissions[1].object_pk, str(provider.pk))
|
||||||
|
|
||||||
|
# Provider requires a certificate-key-pair, user should have permissions for it
|
||||||
|
keypair = CertificateKeyPair.objects.first()
|
||||||
|
provider.certificate = keypair
|
||||||
|
provider.save()
|
||||||
|
permissions = UserObjectPermission.objects.filter(user=outpost.user).order_by(
|
||||||
|
"content_type__model"
|
||||||
|
)
|
||||||
|
self.assertEqual(len(permissions), 3)
|
||||||
|
self.assertEqual(permissions[0].object_pk, str(keypair.pk))
|
||||||
|
self.assertEqual(permissions[1].object_pk, str(outpost.pk))
|
||||||
|
self.assertEqual(permissions[2].object_pk, str(provider.pk))
|
||||||
|
|
||||||
|
# Remove provider from outpost, user should only have access to outpost
|
||||||
|
outpost.providers.remove(provider)
|
||||||
|
outpost.save()
|
||||||
|
permissions = UserObjectPermission.objects.filter(user=outpost.user)
|
||||||
|
self.assertEqual(len(permissions), 1)
|
||||||
|
self.assertEqual(permissions[0].object_pk, str(outpost.pk))
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user