Compare commits
129 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
4eaa46e717 | |||
59e8dca499 | |||
945d5bfaf6 | |||
dbcdab05ff | |||
e2cc2843d8 | |||
241d59be8d | |||
74251a8883 | |||
585afd1bcd | |||
8358574484 | |||
cbcdaaf532 | |||
f99eaa85ac | |||
5007a6befe | |||
50c75087b8 | |||
438e4efd49 | |||
c7ca95ff2b | |||
9f403a71ed | |||
2f4139df65 | |||
f3ee8f7d9c | |||
5fa3729702 | |||
87f44fada4 | |||
c0026f3e16 | |||
c1051059f4 | |||
c25eda63ba | |||
c90906c968 | |||
f6b52b9281 | |||
b04f92c8b4 | |||
a02fcb0a7a | |||
c1ea605c7e | |||
116be0b3c0 | |||
438250b3a9 | |||
5e6acee2a5 | |||
8b4222e7bb | |||
4af563ce89 | |||
77842fab58 | |||
5689f25c39 | |||
a69c494feb | |||
83408b6ae0 | |||
d30abc64d0 | |||
6674d3e017 | |||
4749c3fad0 | |||
18886697d6 | |||
e75c9e9a79 | |||
5a3c1137ab | |||
ddca46e24a | |||
22a9abf7bf | |||
fb16502466 | |||
421bd13ddf | |||
404c9ef753 | |||
a57b545093 | |||
d8530f238d | |||
fe4a0c3b44 | |||
e0c104ee5c | |||
6ab8794754 | |||
316e6cb17f | |||
9d5d99290c | |||
20ffe833de | |||
d4d026bf6a | |||
dfe093b2b9 | |||
60739e620e | |||
d6cc6770b8 | |||
ddc1022461 | |||
2c2226610e | |||
cba78b4de7 | |||
1eeb64ee39 | |||
22dea62084 | |||
5ff1dd8426 | |||
da15a8878f | |||
bf33828ac1 | |||
950a1fc77e | |||
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 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.10.0-stable
|
||||
current_version = 0.10.6-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
@ -15,8 +15,6 @@ values =
|
||||
beta
|
||||
stable
|
||||
|
||||
[bumpversion:file:README.md]
|
||||
|
||||
[bumpversion:file:docs/installation/docker-compose.md]
|
||||
|
||||
[bumpversion:file:docs/installation/kubernetes.md]
|
||||
@ -30,3 +28,5 @@ values =
|
||||
[bumpversion:file:.github/workflows/release.yml]
|
||||
|
||||
[bumpversion:file:passbook/__init__.py]
|
||||
|
||||
[bumpversion:file:proxy/pkg/version.go]
|
||||
|
@ -1,5 +1,6 @@
|
||||
[run]
|
||||
source = passbook
|
||||
relative_files = true
|
||||
omit =
|
||||
*/asgi.py
|
||||
manage.py
|
||||
|
17
.github/workflows/release.yml
vendored
17
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.10.0-stable
|
||||
-t beryju/passbook:0.10.6-stable
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.10.0-stable
|
||||
run: docker push beryju/passbook:0.10.6-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-proxy:
|
||||
@ -48,11 +48,11 @@ jobs:
|
||||
cd proxy
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-proxy:0.10.0-stable \
|
||||
-t beryju/passbook-proxy:0.10.6-stable \
|
||||
-t beryju/passbook-proxy:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-proxy:0.10.0-stable
|
||||
run: docker push beryju/passbook-proxy:0.10.6-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-proxy:latest
|
||||
build-static:
|
||||
@ -77,11 +77,11 @@ jobs:
|
||||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:0.10.0-stable
|
||||
-t beryju/passbook-static:0.10.6-stable
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.10.0-stable
|
||||
run: docker push beryju/passbook-static:0.10.6-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
@ -93,6 +93,9 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run test suite in final docker images
|
||||
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 up --no-start
|
||||
docker-compose start postgresql redis
|
||||
@ -111,5 +114,5 @@ jobs:
|
||||
SENTRY_PROJECT: passbook
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 0.10.0-stable
|
||||
tagName: 0.10.6-stable
|
||||
environment: beryjuorg-prod
|
||||
|
7
.github/workflows/tag.yml
vendored
7
.github/workflows/tag.yml
vendored
@ -13,7 +13,10 @@ jobs:
|
||||
- uses: actions/checkout@master
|
||||
- name: Pre-release test
|
||||
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 build \
|
||||
--no-cache \
|
||||
@ -46,7 +49,7 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ steps.get_version.outputs.result }}
|
||||
draft: false
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Upload packaged Helm Chart
|
||||
id: upload-release-asset
|
||||
|
@ -1,9 +1,16 @@
|
||||
[MASTER]
|
||||
|
||||
disable=arguments-differ,no-self-use,fixme,locally-disabled,too-many-ancestors,too-few-public-methods,import-outside-toplevel,bad-continuation,signature-differs,similarities,cyclic-import
|
||||
|
||||
load-plugins=pylint_django,pylint.extensions.bad_builtin
|
||||
|
||||
extension-pkg-whitelist=lxml
|
||||
|
||||
# Allow constants to be shorter than normal (and lowercase, for settings.py)
|
||||
const-rgx=[a-zA-Z0-9_]{1,40}$
|
||||
|
||||
ignored-modules=django-otp
|
||||
jobs=12
|
||||
ignore=migrations
|
||||
max-attributes=12
|
||||
|
||||
jobs=12
|
||||
|
2
Makefile
2
Makefile
@ -1,7 +1,7 @@
|
||||
all: lint-fix lint coverage gen
|
||||
|
||||
coverage:
|
||||
coverage run --concurrency=multiprocessing manage.py test --failfast
|
||||
coverage run --concurrency=multiprocessing manage.py test --failfast -v 3
|
||||
coverage combine
|
||||
coverage html
|
||||
coverage report
|
||||
|
195
Pipfile.lock
generated
195
Pipfile.lock
generated
@ -74,18 +74,18 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:20edd03ae4c4e141b0d8a9a9afc773af4345d54b68202b6aa502956b57b18b3f",
|
||||
"sha256:b596a80181fecd775ccc009286400f4d785136f250967895cb34beeeef65eb1f"
|
||||
"sha256:44073b1b1823ffc9edcf9027afbca908dad6bd5000f512ca73f929f6a604ae24",
|
||||
"sha256:888be45e289ba56c4e47cfae5d6b08f097bc981d077fbe6521a6d3dc7a4d757e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.14.59"
|
||||
"version": "==1.15.1"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:193f193a66ac79106725e14dd73e28ed36bcec99b37156538a2202d061056a58",
|
||||
"sha256:e55a4fc652537f5ccb2362133f3928ebeafb04ee9fe15ea11c2df80ba4ef8a12"
|
||||
"sha256:6bdf60281c2e80360fe904851a1a07df3dcfe066fe88dc7fba2b5e626ac05c8c",
|
||||
"sha256:d6bdf51c8880aa9974e6b61d2f7d9d1debe407287e2e9e60f36c789fe8ba6790"
|
||||
],
|
||||
"version": "==1.17.60"
|
||||
"version": "==1.18.1"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -111,36 +111,44 @@
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e",
|
||||
"sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c",
|
||||
"sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e",
|
||||
"sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1",
|
||||
"sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4",
|
||||
"sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2",
|
||||
"sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c",
|
||||
"sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0",
|
||||
"sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798",
|
||||
"sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1",
|
||||
"sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4",
|
||||
"sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731",
|
||||
"sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4",
|
||||
"sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c",
|
||||
"sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487",
|
||||
"sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e",
|
||||
"sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f",
|
||||
"sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123",
|
||||
"sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c",
|
||||
"sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b",
|
||||
"sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650",
|
||||
"sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad",
|
||||
"sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75",
|
||||
"sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82",
|
||||
"sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7",
|
||||
"sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15",
|
||||
"sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa",
|
||||
"sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"
|
||||
"sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d",
|
||||
"sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b",
|
||||
"sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4",
|
||||
"sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f",
|
||||
"sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3",
|
||||
"sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579",
|
||||
"sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537",
|
||||
"sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e",
|
||||
"sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05",
|
||||
"sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171",
|
||||
"sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca",
|
||||
"sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522",
|
||||
"sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c",
|
||||
"sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc",
|
||||
"sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d",
|
||||
"sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808",
|
||||
"sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828",
|
||||
"sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869",
|
||||
"sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d",
|
||||
"sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9",
|
||||
"sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0",
|
||||
"sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc",
|
||||
"sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15",
|
||||
"sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c",
|
||||
"sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a",
|
||||
"sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3",
|
||||
"sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1",
|
||||
"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": {
|
||||
"hashes": [
|
||||
@ -327,11 +335,11 @@
|
||||
},
|
||||
"django-storages": {
|
||||
"hashes": [
|
||||
"sha256:1e37da57678e6cf1e9914f84099a305323e4e1f261afe54fdb703cae7aa6fbc3",
|
||||
"sha256:36ed8dab33d761954498189592ce005920095fcbc02dab4184eb51393c370991"
|
||||
"sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c",
|
||||
"sha256:652275ab7844538c462b62810276c0244866f345878256a9e0e86f5b1283ae18"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.10"
|
||||
"version": "==1.10.1"
|
||||
},
|
||||
"djangorestframework": {
|
||||
"hashes": [
|
||||
@ -348,14 +356,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.3.0"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
|
||||
"sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
|
||||
"sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
|
||||
],
|
||||
"version": "==0.15.2"
|
||||
},
|
||||
"drf-yasg": {
|
||||
"hashes": [
|
||||
"sha256:5572e9d5baab9f6b49318169df9789f7399d0e3c7bdac8fdb8dfccf1d5d2b1ca",
|
||||
@ -387,10 +387,10 @@
|
||||
},
|
||||
"google-auth": {
|
||||
"hashes": [
|
||||
"sha256:bcbd9f970e7144fe933908aa286d7a12c44b7deb6d78a76871f0377a29d09789",
|
||||
"sha256:f4d5093f13b1b1c0a434ab1dc851cd26a983f86a4d75c95239974e33ed406a87"
|
||||
"sha256:7084c50c03f7a8a5696ef4500e65df0c525a0f6909f3c70b9ee65900a230c755",
|
||||
"sha256:dcf86c5adc3a8a7659be190b12bb8912ae019cfd9ee2a571ea881e289fafbe39"
|
||||
],
|
||||
"version": "==1.21.1"
|
||||
"version": "==1.21.2"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@ -749,20 +749,25 @@
|
||||
"sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2",
|
||||
"sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68",
|
||||
"sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2",
|
||||
"sha256:663f8de2b3df2e744d6e1610506e0ea4e213bde906795953c1e82279c169f0a7",
|
||||
"sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739",
|
||||
"sha256:709b9f144d23e290b9863121d1ace14a72e01f66ea9c903fbdc690520dfdfcf0",
|
||||
"sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149",
|
||||
"sha256:80d57177a0b7c14d4594c62bbb47fe2f6309ad3b0a34348a291d570925c97a82",
|
||||
"sha256:87006cf0d81505408f1ae4f55cf8a5d95a8e029a4793360720ae17c6500f7ecc",
|
||||
"sha256:9f62d21bc693f3d7d444f17ed2ad7a913b4c37c15cd807895d013c39c0517dfd",
|
||||
"sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23",
|
||||
"sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c",
|
||||
"sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e",
|
||||
"sha256:bcd5b8416e73e4b0d48afba3704d8c826414764dafaed7a1a93c442188d90ccc",
|
||||
"sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a",
|
||||
"sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8",
|
||||
"sha256:cecbf67e81d6144a50dc615629772859463b2e4f815d0c082fa421db362f040e",
|
||||
"sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a",
|
||||
"sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6",
|
||||
"sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a",
|
||||
"sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21",
|
||||
"sha256:f2e045224074d5664dc9cbabbf4f4d4d46f1ee90f24780e3a9a668fd096ff17f",
|
||||
"sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345",
|
||||
"sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982",
|
||||
"sha256:fbe65d5cfe04ff2f7684160d50f5118bdefb01e3af4718eeb618bfed40f19d94"
|
||||
@ -835,9 +840,9 @@
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:27515d2d5db0629c7dadf6fbe76973eb56f098c1b01d36de42eb69220d2c19e4"
|
||||
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
||||
],
|
||||
"version": "==0.17.2"
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
@ -952,11 +957,11 @@
|
||||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
"sha256:97bff68e57402ad39674e6fe2545df0d5eea41c3d51e280c170761705c8c20ff",
|
||||
"sha256:a16caf9ce892623081cbb9a95f6c1f892778bb123909b0ed7afdfb52ce7a58a1"
|
||||
"sha256:1a086486ff9da15791f294f6e9915eb3747d161ef64dee2d038a4d0b4a369b24",
|
||||
"sha256:45486deb031cea6bbb25a540d7adb4dd48cd8a1cc31e6a5ce9fb4f792a572e9a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.17.4"
|
||||
"version": "==0.17.6"
|
||||
},
|
||||
"service-identity": {
|
||||
"hashes": [
|
||||
@ -1269,43 +1274,43 @@
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb",
|
||||
"sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3",
|
||||
"sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716",
|
||||
"sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034",
|
||||
"sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3",
|
||||
"sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8",
|
||||
"sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0",
|
||||
"sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f",
|
||||
"sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4",
|
||||
"sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962",
|
||||
"sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d",
|
||||
"sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b",
|
||||
"sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4",
|
||||
"sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3",
|
||||
"sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258",
|
||||
"sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59",
|
||||
"sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01",
|
||||
"sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd",
|
||||
"sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b",
|
||||
"sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d",
|
||||
"sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89",
|
||||
"sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd",
|
||||
"sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b",
|
||||
"sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d",
|
||||
"sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46",
|
||||
"sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546",
|
||||
"sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082",
|
||||
"sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b",
|
||||
"sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4",
|
||||
"sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8",
|
||||
"sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811",
|
||||
"sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd",
|
||||
"sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651",
|
||||
"sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"
|
||||
"sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
|
||||
"sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
|
||||
"sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
|
||||
"sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
|
||||
"sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
|
||||
"sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
|
||||
"sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
|
||||
"sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
|
||||
"sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
|
||||
"sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
|
||||
"sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
|
||||
"sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
|
||||
"sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
|
||||
"sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
|
||||
"sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
|
||||
"sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
|
||||
"sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
|
||||
"sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
|
||||
"sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
|
||||
"sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
|
||||
"sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
|
||||
"sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
|
||||
"sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
|
||||
"sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
|
||||
"sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
|
||||
"sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
|
||||
"sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
|
||||
"sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
|
||||
"sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
|
||||
"sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
|
||||
"sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
|
||||
"sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
|
||||
"sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
|
||||
"sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.2.1"
|
||||
"version": "==5.3"
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
@ -1317,11 +1322,11 @@
|
||||
},
|
||||
"django-debug-toolbar": {
|
||||
"hashes": [
|
||||
"sha256:eabbefe89881bbe4ca7c980ff102e3c35c8e8ad6eb725041f538988f2f39a943",
|
||||
"sha256:ff94725e7aae74b133d0599b9bf89bd4eb8f5d2c964106e61d11750228c8774c"
|
||||
"sha256:23129b4f771605c7ccf8733cc53558a68c5d463d60cdc83408d34b713acf4f5f",
|
||||
"sha256:7c9bf93eabb1e745fe1fca830242d49f3c839d35163e5b53914009ed111209b1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.2"
|
||||
"version": "==3.0"
|
||||
},
|
||||
"docker": {
|
||||
"hashes": [
|
||||
@ -1550,11 +1555,11 @@
|
||||
},
|
||||
"pytest-django": {
|
||||
"hashes": [
|
||||
"sha256:64f99d565dd9497af412fcab2989fe40982c1282d4118ff422b407f3f7275ca5",
|
||||
"sha256:664e5f42242e5e182519388f01b9f25d824a9feb7cd17d8f863c8d776f38baf9"
|
||||
"sha256:4de6dbd077ed8606616958f77655fed0d5e3ee45159475671c7fa67596c6dba6",
|
||||
"sha256:c33e3d3da14d8409b125d825d4e74da17bb252191bf6fc3da6856e27a8b73ea4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.9.0"
|
||||
"version": "==3.10.0"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
|
15
README.md
15
README.md
@ -13,20 +13,7 @@ passbook is an open-source Identity Provider focused on flexibility and versatil
|
||||
|
||||
## Installation
|
||||
|
||||
For small/test setups it is recommended to use 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-stable
|
||||
# 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 small/test setups it is recommended to use docker-compose, see the [documentation](https://passbook.beryju.org/installation/docker-compose/)
|
||||
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org//installation/kubernetes/)
|
||||
|
||||
|
@ -6,7 +6,8 @@ As passbook is currently in a pre-stable, only the latest "stable" version is su
|
||||
|
||||
| Version | Supported |
|
||||
| -------- | ------------------ |
|
||||
| 0.8.15 | :white_check_mark: |
|
||||
| 0.9.x | :white_check_mark: |
|
||||
| 0.10.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -8,6 +8,10 @@ variables:
|
||||
POSTGRES_DB: passbook
|
||||
POSTGRES_USER: passbook
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}:
|
||||
branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }}
|
||||
${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}:
|
||||
branchName: ${{ replace(variables['System.PullRequest.SourceBranch'], 'refs/heads/', '') }}
|
||||
|
||||
stages:
|
||||
- stage: Lint
|
||||
@ -117,6 +121,41 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run ./manage.py migrate
|
||||
- job: migrations_from_previous_release
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
dockerComposeFile: 'scripts/ci.docker-compose.yml'
|
||||
action: 'Run services'
|
||||
buildImages: false
|
||||
- task: CmdLine@2
|
||||
displayName: Prepare Last tagged release
|
||||
inputs:
|
||||
script: |
|
||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
displayName: Migrate to last tagged release
|
||||
inputs:
|
||||
script: pipenv run ./manage.py migrate
|
||||
- task: CmdLine@2
|
||||
displayName: Install current branch
|
||||
inputs:
|
||||
script: |
|
||||
set -x
|
||||
git checkout ${{ variables.branchName }}
|
||||
pipenv sync --dev
|
||||
- task: CmdLine@2
|
||||
displayName: Migrate to current branch
|
||||
inputs:
|
||||
script: pipenv run ./manage.py migrate
|
||||
- job: coverage_unittest
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
@ -139,7 +178,7 @@ stages:
|
||||
displayName: Run full test suite
|
||||
inputs:
|
||||
script: |
|
||||
pipenv run coverage run ./manage.py test passbook
|
||||
pipenv run coverage run ./manage.py test passbook -v 3
|
||||
mkdir output-unittest
|
||||
mv unittest.xml output-unittest/unittest.xml
|
||||
mv .coverage output-unittest/coverage
|
||||
@ -181,7 +220,14 @@ stages:
|
||||
- task: CmdLine@2
|
||||
displayName: Run full test suite
|
||||
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
|
||||
displayName: Prepare unittests and coverage for upload
|
||||
inputs:
|
||||
@ -225,11 +271,9 @@ stages:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
find .
|
||||
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
|
||||
pipenv run coverage xml
|
||||
pipenv run coverage html
|
||||
find .
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: 'Cobertura'
|
||||
@ -260,7 +304,7 @@ stages:
|
||||
repository: 'beryju/passbook'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'Dockerfile'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
tags: "gh-${{ variables.branchName }}"
|
||||
- job: build_static
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
@ -277,14 +321,14 @@ stages:
|
||||
repository: 'beryju/passbook-static'
|
||||
command: 'build'
|
||||
Dockerfile: 'static.Dockerfile'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
tags: "gh-${{ variables.branchName }}"
|
||||
arguments: "--network=beryjupassbook_default"
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook-static'
|
||||
command: 'push'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
tags: "gh-${{ variables.branchName }}"
|
||||
- stage: Deploy
|
||||
jobs:
|
||||
- job: deploy_dev
|
||||
|
@ -14,6 +14,8 @@ services:
|
||||
- POSTGRES_DB=passbook
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
env_file:
|
||||
- .env
|
||||
redis:
|
||||
image: redis
|
||||
networks:
|
||||
@ -21,13 +23,12 @@ services:
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
server:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-stable}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.6-stable}
|
||||
command: server
|
||||
environment:
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
|
||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
PASSBOOK_LOG_LEVEL: debug
|
||||
ports:
|
||||
- 8000
|
||||
@ -37,8 +38,10 @@ services:
|
||||
- traefik.port=8000
|
||||
- traefik.docker.network=internal
|
||||
- traefik.frontend.rule=PathPrefix:/
|
||||
env_file:
|
||||
- .env
|
||||
worker:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-stable}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.6-stable}
|
||||
command: worker
|
||||
networks:
|
||||
- internal
|
||||
@ -46,12 +49,13 @@ services:
|
||||
- traefik.enable=false
|
||||
environment:
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
|
||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
PASSBOOK_LOG_LEVEL: debug
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.0-stable}
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.6-stable}
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
|
@ -84,15 +84,6 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "9922212c-47a2-475a-9905-abeb5e621652"
|
||||
},
|
||||
"model": "passbook_policies_expression.expressionpolicy",
|
||||
"attrs": {
|
||||
"name": "policy-enrollment-password-equals",
|
||||
"expression": "# Verifies that the passwords are equal\r\nreturn request.context['password'] == request.context['password_repeat']"
|
||||
}
|
||||
},{
|
||||
"identifiers": {
|
||||
"pk": "096e6282-6b30-4695-bd03-3b143eab5580",
|
||||
"name": "default-enrollment-email-verficiation"
|
||||
@ -135,9 +126,6 @@
|
||||
"cb954fd4-65a5-4ad9-b1ee-180ee9559cf4",
|
||||
"7db91ee8-4290-4e08-8d39-63f132402515",
|
||||
"d30b5eb4-7787-4072-b1ba-65b46e928920"
|
||||
],
|
||||
"validation_policies": [
|
||||
"9922212c-47a2-475a-9905-abeb5e621652"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -55,16 +55,6 @@
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "cd042fc6-cc92-4b98-b7e6-f4729df798d8"
|
||||
},
|
||||
"model": "passbook_policies_expression.expressionpolicy",
|
||||
"attrs": {
|
||||
"name": "default-password-change-password-equal",
|
||||
"expression": "# Check that both passwords are equal.\nreturn request.context['password'] == request.context['password_repeat']"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "e54045a7-6ecb-4ad9-ad37-28e72d8e565e",
|
||||
@ -118,9 +108,6 @@
|
||||
"fields": [
|
||||
"7db91ee8-4290-4e08-8d39-63f132402515",
|
||||
"d30b5eb4-7787-4072-b1ba-65b46e928920"
|
||||
],
|
||||
"validation_policies": [
|
||||
"cd042fc6-cc92-4b98-b7e6-f4729df798d8"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -39,7 +39,6 @@ This designates a flow for unenrollment. This flow can contain any amount of ver
|
||||
This designates a flow for recovery. This flow normally contains an [**identification**](stages/identification/index.md) stage to find the user. It can also contain any amount of verification stages, such as [**email**](stages/email/index.md) or [**captcha**](stages/captcha/index.md).
|
||||
Afterwards, use the [**prompt**](stages/prompt/index.md) stage to ask the user for a new password and the [**user_write**](stages/user_write.md) stage to update the password.
|
||||
|
||||
### Change Password
|
||||
### Setup
|
||||
|
||||
This designates a flow for password changes. This flow can contain any amount of verification stages, such as [**email**](stages/email/index.md) or [**captcha**](stages/captcha/index.md).
|
||||
Afterwards, use the [**prompt**](stages/prompt/index.md) stage to ask the user for a new password and the [**user_write**](stages/user_write.md) stage to update the password.
|
||||
This designates a flow for general setup. This designation doesn't have any constraints in what you can do. For example, by default this designation is used to configure Factors, like change a password and setup TOTP.
|
||||
|
@ -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.
|
||||
|
||||
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env`
|
||||
|
||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.6-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-stable
|
||||
# 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
|
||||
@ -32,4 +39,6 @@ Now you can pull the Docker images needed by running `docker-compose pull`. Afte
|
||||
|
||||
passbook will then be reachable via HTTP on port 80, and HTTPS on port 443. You can optionally configure the packaged traefik to use Let's Encrypt certificates for TLS Encryption.
|
||||
|
||||
If you plan to access passbook via a reverse proxy which does SSL Termination, make sure you use the HTTPS port, so passbook is aware of the SSL connection.
|
||||
|
||||
The initial setup process also creates a default admin user, the username and password for which is `pbadmin`. It is highly recommended to change this password as soon as you log in.
|
||||
|
@ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.10.0-stable
|
||||
tag: 0.10.6-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
|
BIN
docs/integrations/services/sentry/auth.png
Normal file
BIN
docs/integrations/services/sentry/auth.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 316 KiB |
@ -15,27 +15,31 @@ From https://sentry.io
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `sentry.company` is the FQDN of the Sentry install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
- `sentry.company` is the FQDN of the Sentry install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook. Create an OpenID provider with the following parameters:
|
||||
Create an application in passbook. Create a SAML Provider with the following values
|
||||
|
||||
- Client Type: `Confidential`
|
||||
- Response types: `code (Authorization Code Flow)`
|
||||
- JWT Algorithm: `RS256`
|
||||
- Redirect URIs: `https://sentry.company/auth/sso/`
|
||||
- Scopes: `openid email`
|
||||
- ACS URL: `https://sentry.company/saml/acs/<sentry organisation name>/`
|
||||
- Audience: `https://sentry.company/saml/metadata/<sentry organisation name>/`
|
||||
- Issuer: `passbook`
|
||||
- Service Provider Binding: `Post`
|
||||
- Property Mapping: Select all Autogenerated Mappings
|
||||
|
||||
## Sentry
|
||||
|
||||
**This guide assumes you've installed Sentry using [getsentry/onpremise](https://github.com/getsentry/onpremise)**
|
||||
|
||||
- Add `sentry-auth-oidc` to `onpremise/sentry/requirements.txt` (Create the file if it doesn't exist yet)
|
||||
- Add the following block to your `onpremise/sentry/sentry.conf.py`:
|
||||
```
|
||||
OIDC_ISSUER = "passbook"
|
||||
OIDC_CLIENT_ID = "<Client ID from passbook>"
|
||||
OIDC_CLIENT_SECRET = "<Client Secret from passbook>"
|
||||
OIDC_SCOPE = "openid email"
|
||||
OIDC_DOMAIN = "https://passbook.company/application/oidc/"
|
||||
```
|
||||
Navigate to Settings -> Auth, and click on Configure next to SAML2
|
||||
|
||||

|
||||
|
||||
In passbook, get the Metadata URL by right-clicking `Download Metadata` and selecting Copy Link Address, and paste that URL into Sentry.
|
||||
|
||||
On the next screen, input these Values
|
||||
|
||||
IdP User ID: `urn:oid:0.9.2342.19200300.100.1.1`
|
||||
User Email: `urn:oid:0.9.2342.19200300.100.1.3`
|
||||
First Name: `urn:oid:2.5.4.3`
|
||||
|
||||
After confirming, Sentry will authenticate with passbook, and you should be redirected back to a page confirming your settings.
|
||||
|
37
docs/integrations/services/sonarr/index.md
Normal file
37
docs/integrations/services/sonarr/index.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Sonarr Integration
|
||||
|
||||
!!! note
|
||||
These instructions apply to all projects in the *arr Family. If you use multiple of these projects, you can assign them to the same Outpost.
|
||||
|
||||
## What is Sonarr
|
||||
|
||||
From https://github.com/Sonarr/Sonarr
|
||||
|
||||
!!! note ""
|
||||
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `sonarr.company` is the FQDN of the Sonarr install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook. Create a Proxy Provider with the following values
|
||||
|
||||
- Internal host
|
||||
|
||||
If Sonarr is running in docker, and you're deploying the passbook proxy on the same host, set the value to `http://sonarr:8989`, where sonarr is the name of your container.
|
||||
|
||||
If Sonarr is running on a different server than where you are deploying the passbook proxy, set the value to `http://sonarr.company:8989`.
|
||||
|
||||
- External host
|
||||
|
||||
Set this to the external URL you will be accessing Sonarr from.
|
||||
|
||||
## Deployment
|
||||
|
||||
Create an outpost deployment for the provider you've created above, as described [here](../../../outposts/outposts.md). Deploy this Outpost either on the same host or a different host that can access Sonarr.
|
||||
|
||||
The outpost will connect to passbook and configure itself.
|
@ -16,14 +16,15 @@ From https://docs.ansible.com/ansible/2.5/reference_appendices/tower.html
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `awx.company` is the FQDN of the AWX/Tower install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
- `awx.company` is the FQDN of the AWX/Tower install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML provider with the following parameters:
|
||||
|
||||
- ACS URL: `https://awx.company/sso/complete/saml/`
|
||||
- Audience: `awx`
|
||||
- Issuer: `https://awx.company/sso/metadata/saml/`
|
||||
- ACS URL: `https://awx.company/sso/complete/saml/`
|
||||
- Audience: `awx`
|
||||
- Service Provider Binding: Post
|
||||
- Issuer: `https://awx.company/sso/metadata/saml/`
|
||||
|
||||
You can of course use a custom signing certificate, and adjust durations.
|
||||
|
||||
|
58
docs/integrations/services/ubuntu-landscape/index.md
Normal file
58
docs/integrations/services/ubuntu-landscape/index.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Ubuntu Landscape Integration
|
||||
|
||||
## What is Ubuntu Landscape
|
||||
|
||||
From https://en.wikipedia.org/wiki/Landscape_(software)
|
||||
|
||||
!!! note ""
|
||||
|
||||
Landscape is a systems management tool developed by Canonical. It can be run on-premises or in the cloud depending on the needs of the user. It is primarily designed for use with Ubuntu derivatives such as Desktop, Server, and Core.
|
||||
|
||||
!!! warning
|
||||
|
||||
This requires passbook 0.10.3 or newer.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `landscape.company` is the FQDN of the Landscape server.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Landscape uses the OpenID-Connect Protocol for single-sign on.
|
||||
|
||||
## passbook Setup
|
||||
|
||||
Create an OAuth2/OpenID-Connect Provider with the default settings. Set the Redirect URIs to `https://landscape.company/login/handle-openid`. Select all Autogenerated Scopes.
|
||||
|
||||
Keep Note of the Client ID and the Client Secret.
|
||||
|
||||
Create an application and assign access policies to the application. Set the application's provider to the provider you've just created.
|
||||
|
||||
## Landscape Setup
|
||||
|
||||
On the Landscape Server, edit the file `/etc/landscape/service.conf` and add the following snippet under the `[landscape]` section:
|
||||
|
||||
```
|
||||
oidc-issuer = https://passbook.company/application/o/<slug of the application you've created>/
|
||||
oidc-client-id = <client ID of the provider you've created>
|
||||
oidc-client-secret = <client Secret of the provider you've created>
|
||||
```
|
||||
|
||||
Afterwards, run `sudo lsctl restart` to restart the Landscape services.
|
||||
|
||||
## Appendix
|
||||
|
||||
To make an OpenID-Connect User admin, you have to insert some rows into the database.
|
||||
|
||||
First login with your passbook user, and make sure the user is created successfully.
|
||||
|
||||
Run `sudo -u postgres psql landscape-standalone-main` on the Landscape server to open a PostgreSQL Prompt.
|
||||
Then run `select * from person;` to get a list of all users. Take note of the ID given to your new user.
|
||||
|
||||
Run the following commands to make this user an administrator:
|
||||
|
||||
```sql
|
||||
INSERT INTO person_account VALUES (<user id>, 1);
|
||||
INSERT INTO person_access VALUES (<user id>, 1, 1);
|
||||
```
|
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 (ADFS Compatibility Mode, sends id_token as access_token)
|
||||
- JWT Algorithm: RS256
|
||||
- Redirect URI: `https://vcenter.company/ui/login/oauth2/authcode`
|
||||
- Post Logout Redirect URIs: `https://vcenter.company/ui/login`
|
||||
- Sub Mode: If your Email address Schema matches your UPN, select "Based on the User's Email...", otherwise select "Based on the User's UPN...".
|
||||
- Scopes: Select the Scope Mapping you've created in Step 1
|
||||
|
||||

|
||||
|
||||
### Step 4
|
||||
|
||||
Create an application which uses this provider. Optionally apply access restrictions to the application.
|
||||
|
||||
## 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 |
@ -15,6 +15,6 @@ services:
|
||||
- 4443:4443
|
||||
environment:
|
||||
PASSBOOK_HOST: https://your-passbook.tld
|
||||
PASSBOOK_INSECURE: 'true'
|
||||
PASSBOOK_INSECURE: 'false'
|
||||
PASSBOOK_TOKEN: token-generated-by-passbook
|
||||
```
|
||||
|
@ -5,8 +5,8 @@ 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
|
||||
api_version: v1
|
||||
kind: secret
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
@ -14,65 +14,14 @@ metadata:
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
name: passbook-outpost-api
|
||||
string_data:
|
||||
stringData:
|
||||
passbook_host: '__PASSBOOK_URL__'
|
||||
passbook_host_insecure: 'true'
|
||||
token: '__PASSBOOK_TOKEN__'
|
||||
type: Opaque
|
||||
---
|
||||
api_version: 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:
|
||||
match_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
|
||||
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
|
||||
value_from:
|
||||
secret_key_ref:
|
||||
key: passbook_host
|
||||
name: passbook-outpost-api
|
||||
- name: PASSBOOK_TOKEN
|
||||
value_from:
|
||||
secret_key_ref:
|
||||
key: token
|
||||
name: passbook-outpost-api
|
||||
- name: PASSBOOK_INSECURE
|
||||
value_from:
|
||||
secret_key_ref:
|
||||
key: passbook_host_insecure
|
||||
name: passbook-outpost-api
|
||||
image: beryju/passbook-proxy:0.10.0
|
||||
name: proxy
|
||||
ports:
|
||||
- containerPort: 4180
|
||||
name: http
|
||||
protocol: TCP
|
||||
- containerPort: 4443
|
||||
name: http
|
||||
protocol: TCP
|
||||
---
|
||||
api_version: v1
|
||||
kind: service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
@ -96,4 +45,55 @@ spec:
|
||||
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
|
||||
```
|
||||
|
9
docs/outposts/upgrading.md
Normal file
9
docs/outposts/upgrading.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Upgrading an Outpost
|
||||
|
||||
In the Outpost Overview list, you'll see if any deployed outposts are out of date.
|
||||
|
||||

|
||||
|
||||
To upgrade the Outpost to the latest version, simple adjust the docker tag of the outpost the the new version.
|
||||
|
||||
Since the configuration is managed by passbook, that's all you have to do.
|
BIN
docs/outposts/upgrading_outdated.png
Normal file
BIN
docs/outposts/upgrading_outdated.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
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 |
@ -10,7 +10,6 @@ from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.stages.email.models import EmailStage, EmailTemplates
|
||||
from passbook.stages.identification.models import IdentificationStage
|
||||
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
@ -59,16 +58,9 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.validation_policies.set([password_policy])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
@ -152,16 +144,9 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.validation_policies.set([password_policy])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
|
@ -6,6 +6,7 @@ from unittest.case import skipUnless
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import Application
|
||||
@ -214,7 +215,10 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
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.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"Permission denied",
|
||||
|
@ -33,6 +33,7 @@ from passbook.providers.oauth2.models import (
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
APPLICATION_SLUG = "grafana"
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
@ -69,6 +70,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
"GF_AUTH_GENERIC_OAUTH_API_URL": (
|
||||
self.url("passbook_providers_oauth2:userinfo")
|
||||
),
|
||||
"GF_AUTH_SIGNOUT_REDIRECT_URL": (
|
||||
self.url(
|
||||
"passbook_providers_oauth2:end-session",
|
||||
application_slug=APPLICATION_SLUG,
|
||||
)
|
||||
),
|
||||
"GF_LOG_LEVEL": "debug",
|
||||
},
|
||||
}
|
||||
@ -97,7 +104,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
)
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
@ -137,7 +144,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
)
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
@ -171,6 +178,72 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
USER().email,
|
||||
)
|
||||
|
||||
def test_authorization_logout(self):
|
||||
"""test OpenID Provider flow with logout"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||
)
|
||||
)
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
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)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[href='/logout']").click()
|
||||
self.wait_for_url(
|
||||
self.url(
|
||||
"passbook_providers_oauth2:end-session",
|
||||
application_slug=APPLICATION_SLUG,
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.ID, "logout").click()
|
||||
|
||||
def test_authorization_consent_explicit(self):
|
||||
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
|
||||
sleep(1)
|
||||
@ -195,7 +268,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
)
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
@ -271,7 +344,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
)
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
||||
)
|
||||
|
||||
negative_policy = ExpressionPolicy.objects.create(
|
||||
@ -285,7 +358,10 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
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)
|
||||
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.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"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.deployment_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)
|
@ -8,6 +8,7 @@ from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
@ -206,7 +207,10 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
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.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"Permission denied",
|
||||
|
@ -50,6 +50,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
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")
|
||||
|
@ -1,15 +1,15 @@
|
||||
apiVersion: v2
|
||||
appVersion: "0.10.0-stable"
|
||||
appVersion: "0.10.6-stable"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.10.0-stable"
|
||||
version: "0.10.6-stable"
|
||||
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 9.3.2
|
||||
version: 9.4.1
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: install.postgresql
|
||||
- name: redis
|
||||
version: 10.7.16
|
||||
version: 10.9.0
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: install.redis
|
||||
|
@ -22,10 +22,29 @@ spec:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.beryju.org/component: web
|
||||
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:
|
||||
- name: passbook-database-migrations
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: Always
|
||||
args: [migrate]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
@ -50,7 +69,6 @@ spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: Always
|
||||
args: [server]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
@ -93,7 +111,7 @@ spec:
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 200M
|
||||
memory: 300M
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 350M
|
||||
memory: 500M
|
||||
|
@ -22,6 +22,26 @@ spec:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.beryju.org/component: worker
|
||||
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:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
@ -50,7 +70,7 @@ spec:
|
||||
resources:
|
||||
requests:
|
||||
cpu: 150m
|
||||
memory: 300M
|
||||
memory: 400M
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 500M
|
||||
memory: 600M
|
||||
|
@ -4,7 +4,7 @@
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.10.0-stable
|
||||
tag: 0.10.6-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
|
@ -4,7 +4,7 @@ printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap",
|
||||
if [[ "$1" == "server" ]]; then
|
||||
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
|
||||
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
|
||||
# Run system migrations first, run normal migrations after
|
||||
python -m lifecycle.migrate
|
||||
|
@ -47,5 +47,5 @@ logconfig_dict = {
|
||||
if Path("/var/run/secrets/kubernetes.io").exists():
|
||||
workers = 2
|
||||
else:
|
||||
worker = cpu_count()
|
||||
worker = cpu_count() * 2 + 1
|
||||
threads = 4
|
||||
|
@ -1,16 +1,28 @@
|
||||
#!/usr/bin/env python
|
||||
"""This file needs to be run from the root of the project to correctly
|
||||
import passbook. This is done by the dockerfile."""
|
||||
from json import dumps
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
|
||||
from psycopg2 import OperationalError, connect
|
||||
from redis import Redis
|
||||
from redis.exceptions import RedisError
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
def j_print(event: str, log_level: str = "info", **kwargs):
|
||||
"""Print event in the same format as structlog with JSON.
|
||||
Used before structlog is configured."""
|
||||
data = {
|
||||
"event": event,
|
||||
"level": log_level,
|
||||
"logger": __name__,
|
||||
}
|
||||
data.update(**kwargs)
|
||||
print(dumps(data), file=stderr)
|
||||
|
||||
|
||||
while True:
|
||||
try:
|
||||
@ -24,7 +36,7 @@ while True:
|
||||
break
|
||||
except OperationalError:
|
||||
sleep(1)
|
||||
LOGGER.warning("PostgreSQL Connection failed, retrying...")
|
||||
j_print("PostgreSQL Connection failed, retrying...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
@ -38,4 +50,4 @@ while True:
|
||||
break
|
||||
except RedisError:
|
||||
sleep(1)
|
||||
LOGGER.warning("Redis Connection failed, retrying...")
|
||||
j_print("Redis Connection failed, retrying...")
|
||||
|
@ -32,6 +32,7 @@ nav:
|
||||
- Proxy: providers/proxy.md
|
||||
- Outposts:
|
||||
- Overview: outposts/outposts.md
|
||||
- Upgrading: outposts/upgrading.md
|
||||
- Deploy on docker-compose: outposts/deploy-docker-compose.md
|
||||
- Deploy on Kubernetes: outposts/deploy-kubernetes.md
|
||||
- Expressions:
|
||||
@ -52,9 +53,14 @@ nav:
|
||||
- Harbor: integrations/services/harbor/index.md
|
||||
- Sentry: integrations/services/sentry/index.md
|
||||
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
|
||||
- VMware vCenter: integrations/services/vmware-vcenter/index.md
|
||||
- Ubuntu Landscape: integrations/services/ubuntu-landscape/index.md
|
||||
- Sonarr: integrations/services/sonarr/index.md
|
||||
- Upgrading:
|
||||
- to 0.9: upgrading/to-0.9.md
|
||||
- to 0.10: upgrading/to-0.10.md
|
||||
- Troubleshooting:
|
||||
- Access problems: troubleshooting/access.md
|
||||
|
||||
repo_name: "BeryJu/passbook"
|
||||
repo_url: https://github.com/BeryJu/passbook
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.10.0-stable"
|
||||
__version__ = "0.10.6-stable"
|
||||
|
@ -15,11 +15,8 @@ class CodeMirrorWidget(forms.Textarea):
|
||||
self.mode = mode
|
||||
|
||||
def render(self, *args, **kwargs):
|
||||
if "attrs" not in kwargs:
|
||||
kwargs["attrs"] = {}
|
||||
attrs = kwargs["attrs"]
|
||||
if "class" not in attrs:
|
||||
attrs["class"] = ""
|
||||
attrs = kwargs.setdefault("attrs", {})
|
||||
attrs.setdefault("class", "")
|
||||
attrs["class"] += " codemirror"
|
||||
attrs["data-cm-mode"] = self.mode
|
||||
return super().render(*args, **kwargs)
|
||||
@ -56,6 +53,8 @@ class YAMLField(forms.JSONField):
|
||||
)
|
||||
if isinstance(converted, str):
|
||||
return YAMLString(converted)
|
||||
if converted is None:
|
||||
return {}
|
||||
return converted
|
||||
|
||||
def bound_data(self, data, initial):
|
||||
|
@ -12,7 +12,7 @@ class UserForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = ["username", "name", "email", "is_staff", "is_active", "attributes"]
|
||||
fields = ["username", "name", "email", "is_active", "attributes"]
|
||||
widgets = {
|
||||
"name": forms.TextInput,
|
||||
"attributes": CodeMirrorWidget,
|
||||
|
@ -1,26 +0,0 @@
|
||||
"""passbook admin Middleware to impersonate users"""
|
||||
|
||||
from passbook.core.models import User
|
||||
|
||||
|
||||
def impersonate(get_response):
|
||||
"""Middleware to impersonate users"""
|
||||
|
||||
def middleware(request):
|
||||
"""Middleware to impersonate users"""
|
||||
|
||||
# User is superuser and has __impersonate ID set
|
||||
if request.user.is_superuser and "__impersonate" in request.GET:
|
||||
request.session["impersonate_id"] = request.GET["__impersonate"]
|
||||
# user wants to stop impersonation
|
||||
elif "__unimpersonate" in request.GET and "impersonate_id" in request.session:
|
||||
del request.session["impersonate_id"]
|
||||
|
||||
# Actually impersonate user
|
||||
if request.user.is_superuser and "impersonate_id" in request.session:
|
||||
request.user = User.objects.get(pk=request.session["impersonate_id"])
|
||||
|
||||
response = get_response(request)
|
||||
return response
|
||||
|
||||
return middleware
|
@ -1,5 +0,0 @@
|
||||
"""passbook admin settings"""
|
||||
|
||||
MIDDLEWARE = [
|
||||
"passbook.admin.middleware.impersonate",
|
||||
]
|
@ -50,7 +50,7 @@
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ group.user_set.all|length }}
|
||||
{{ group.users.all|length }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -3,18 +3,7 @@
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.pf-m-success {
|
||||
color: var(--pf-global--success-color--100);
|
||||
}
|
||||
.pf-m-danger {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
@ -32,7 +21,7 @@
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<a href="{% url 'passbook_admin:outpost-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
@ -43,6 +32,7 @@
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Providers' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Health' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Version' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -50,7 +40,7 @@
|
||||
{% for outpost in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<a href="{% url 'passbook_outposts:setup' outpost_pk=outpost.pk %}">{{ outpost.name }}</a>
|
||||
<span>{{ outpost.name }}</span>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
@ -58,7 +48,7 @@
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
{% with health=outpost.health %}
|
||||
{% with health=outpost.deployment_health %}
|
||||
{% if health %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ health|naturaltime }}
|
||||
{% else %}
|
||||
@ -66,10 +56,28 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{% with ver=outpost.deployment_version %}
|
||||
{% if ver.outdated %}
|
||||
{% if ver.version == "" %}
|
||||
<i class="fas fa-times pf-m-danger"></i> -
|
||||
{% else %}
|
||||
<i class="fas fa-times pf-m-danger"></i> {% blocktrans with is=ver.version should=ver.should %}{{ is }}, should be {{ should }}{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ ver.version }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</span>
|
||||
</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-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>
|
||||
{% get_htmls outpost as htmls %}
|
||||
{% for html in htmls %}
|
||||
{{ html|safe }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -55,7 +55,7 @@
|
||||
<th role="columnheader">
|
||||
<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>
|
||||
<small>{% trans 'Warning: Policy is not assigned.' %}</small>
|
||||
{% else %}
|
||||
|
@ -55,7 +55,7 @@
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
<a class="pf-c-button pf-m-tertiary" href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a>
|
||||
<a class="pf-c-button pf-m-tertiary" href="{% url 'passbook_core:overview' %}?__impersonate={{ user.pk }}">{% trans 'Impersonate' %}</a>
|
||||
<a class="pf-c-button pf-m-tertiary" href="{% url 'passbook_core:impersonate-init' user_id=user.pk %}">{% trans 'Impersonate' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -8,7 +8,7 @@ from django.test import Client, TestCase
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -16,7 +16,9 @@ class TestAdmin(TestCase):
|
||||
"""Generic admin tests"""
|
||||
|
||||
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.force_login(self.user)
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
"""passbook Outpost administration"""
|
||||
from dataclasses import asdict
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
@ -12,7 +15,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.outposts.forms import OutpostForm
|
||||
from passbook.outposts.models import Outpost
|
||||
from passbook.outposts.models import Outpost, OutpostConfig
|
||||
|
||||
|
||||
class OutpostListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
@ -41,6 +44,13 @@ class OutpostCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:outposts")
|
||||
success_message = _("Successfully created Outpost")
|
||||
|
||||
def get_initial(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"_config": asdict(
|
||||
OutpostConfig(passbook_host=self.request.build_absolute_uri("/"))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class OutpostUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
@ -53,7 +63,7 @@ class OutpostUpdateView(
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:outposts")
|
||||
success_message = _("Successfully updated Certificate-Key Pair")
|
||||
success_message = _("Successfully updated Outpost")
|
||||
|
||||
|
||||
class OutpostDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
@ -64,4 +74,4 @@ class OutpostDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessa
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:outposts")
|
||||
success_message = _("Successfully deleted Certificate-Key Pair")
|
||||
success_message = _("Successfully deleted Outpost")
|
||||
|
@ -1,8 +1,10 @@
|
||||
"""passbook administration overview"""
|
||||
from typing import Union
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import redirect, reverse
|
||||
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 passbook import __version__
|
||||
@ -16,7 +18,7 @@ from passbook.stages.invitation.models import Invitation
|
||||
VERSION_CACHE_KEY = "passbook_latest_version"
|
||||
|
||||
|
||||
def latest_version() -> Version:
|
||||
def latest_version() -> Union[LegacyVersion, Version]:
|
||||
"""Get latest release from GitHub, cached"""
|
||||
if not cache.get(VERSION_CACHE_KEY):
|
||||
try:
|
||||
@ -45,7 +47,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["application_count"] = len(Application.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["source_count"] = len(Source.objects.all())
|
||||
kwargs["stage_count"] = len(Stage.objects.all())
|
||||
@ -58,7 +60,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
application=None
|
||||
)
|
||||
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_flows"] = len(cache.keys("flow_*"))
|
||||
|
@ -9,11 +9,12 @@ from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.policies.forms import PolicyBindingForm
|
||||
from passbook.policies.models import PolicyBinding, PolicyBindingModel
|
||||
from passbook.policies.models import PolicyBinding
|
||||
|
||||
|
||||
class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
@ -29,13 +30,18 @@ class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
# Since `select_subclasses` does not work with a foreign key, we have to do two queries here
|
||||
# First, get all pbm objects that have bindings attached
|
||||
objects = (
|
||||
PolicyBindingModel.objects.filter(policies__isnull=False)
|
||||
get_objects_for_user(
|
||||
self.request.user, "passbook_policies.view_policybindingmodel"
|
||||
)
|
||||
.filter(policies__isnull=False)
|
||||
.select_subclasses()
|
||||
.select_related()
|
||||
.order_by("pk")
|
||||
)
|
||||
for pbm in objects:
|
||||
pbm.bindings = PolicyBinding.objects.filter(target__pk=pbm.pbm_uuid)
|
||||
pbm.bindings = get_objects_for_user(
|
||||
self.request.user, self.permission_required
|
||||
).filter(target__pk=pbm.pbm_uuid)
|
||||
return objects
|
||||
|
||||
|
||||
|
87
passbook/audit/middleware.py
Normal file
87
passbook/audit/middleware.py
Normal file
@ -0,0 +1,87 @@
|
||||
"""Audit middleware"""
|
||||
from functools import partial
|
||||
from typing import Callable
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from passbook.audit.models import Event, EventAction, model_to_dict
|
||||
from passbook.audit.signals import EventNewThread
|
||||
from passbook.core.middleware import LOCAL
|
||||
|
||||
|
||||
class AuditMiddleware:
|
||||
"""Register handlers for duration of request-response that log creation/update/deletion
|
||||
of models"""
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
# Connect signal for automatic logging
|
||||
if hasattr(request, "user") and getattr(
|
||||
request.user, "is_authenticated", False
|
||||
):
|
||||
post_save_handler = partial(
|
||||
self.post_save_handler, user=request.user, request=request
|
||||
)
|
||||
pre_delete_handler = partial(
|
||||
self.pre_delete_handler, user=request.user, request=request
|
||||
)
|
||||
post_save.connect(
|
||||
post_save_handler,
|
||||
dispatch_uid=LOCAL.passbook["request_id"],
|
||||
weak=False,
|
||||
)
|
||||
pre_delete.connect(
|
||||
pre_delete_handler,
|
||||
dispatch_uid=LOCAL.passbook["request_id"],
|
||||
weak=False,
|
||||
)
|
||||
|
||||
response = self.get_response(request)
|
||||
|
||||
post_save.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
|
||||
pre_delete.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
|
||||
|
||||
return response
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def process_exception(self, request: HttpRequest, exception: Exception):
|
||||
"""Unregister handlers in case of exception"""
|
||||
post_save.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
|
||||
pre_delete.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
|
||||
|
||||
@staticmethod
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_handler(
|
||||
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
|
||||
):
|
||||
"""Signal handler for all object's post_save"""
|
||||
if isinstance(instance, Event):
|
||||
return
|
||||
|
||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||
EventNewThread(
|
||||
action, request, user=user, kwargs={"model": model_to_dict(instance)}
|
||||
).run()
|
||||
|
||||
@staticmethod
|
||||
# pylint: disable=unused-argument
|
||||
def pre_delete_handler(
|
||||
user: User, request: HttpRequest, sender, instance: Model, **_
|
||||
):
|
||||
"""Signal handler for all object's pre_delete"""
|
||||
if isinstance(instance, Event):
|
||||
return
|
||||
|
||||
EventNewThread(
|
||||
EventAction.MODEL_DELETED,
|
||||
request,
|
||||
user=user,
|
||||
kwargs={"model": model_to_dict(instance)},
|
||||
).run()
|
33
passbook/audit/migrations/0002_auto_20200918_2116.py
Normal file
33
passbook/audit/migrations/0002_auto_20200918_2116.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-18 21:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_audit", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("LOGIN", "login"),
|
||||
("LOGIN_FAILED", "login_failed"),
|
||||
("LOGOUT", "logout"),
|
||||
("AUTHORIZE_APPLICATION", "authorize_application"),
|
||||
("SUSPICIOUS_REQUEST", "suspicious_request"),
|
||||
("SIGN_UP", "sign_up"),
|
||||
("PASSWORD_RESET", "password_reset"),
|
||||
("INVITE_CREATED", "invitation_created"),
|
||||
("INVITE_USED", "invitation_used"),
|
||||
("IMPERSONATION_STARTED", "impersonation_started"),
|
||||
("IMPERSONATION_ENDED", "impersonation_ended"),
|
||||
("CUSTOM", "custom"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
59
passbook/audit/migrations/0003_auto_20200917_1155.py
Normal file
59
passbook/audit/migrations/0003_auto_20200917_1155.py
Normal file
@ -0,0 +1,59 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-17 11:55
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import passbook.audit.models
|
||||
|
||||
|
||||
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
Event = apps.get_model("passbook_audit", "Event")
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
for event in Event.objects.all():
|
||||
event.delete()
|
||||
# Because event objects cannot be updated, we have to re-create them
|
||||
event.pk = None
|
||||
event.user_json = (
|
||||
passbook.audit.models.get_user(event.user) if event.user else {}
|
||||
)
|
||||
event._state.adding = True
|
||||
event.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_audit", "0002_auto_20200918_2116"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("LOGIN", "login"),
|
||||
("LOGIN_FAILED", "login_failed"),
|
||||
("LOGOUT", "logout"),
|
||||
("AUTHORIZE_APPLICATION", "authorize_application"),
|
||||
("SUSPICIOUS_REQUEST", "suspicious_request"),
|
||||
("SIGN_UP", "sign_up"),
|
||||
("PASSWORD_RESET", "password_reset"),
|
||||
("INVITE_CREATED", "invitation_created"),
|
||||
("INVITE_USED", "invitation_used"),
|
||||
("IMPERSONATION_STARTED", "impersonation_started"),
|
||||
("IMPERSONATION_ENDED", "impersonation_ended"),
|
||||
("CUSTOM", "custom"),
|
||||
]
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="event", name="user_json", field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.RunPython(convert_user_to_json),
|
||||
migrations.RemoveField(model_name="event", name="user",),
|
||||
migrations.RenameField(
|
||||
model_name="event", old_name="user_json", new_name="user"
|
||||
),
|
||||
]
|
37
passbook/audit/migrations/0004_auto_20200921_1829.py
Normal file
37
passbook/audit/migrations/0004_auto_20200921_1829.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-21 18:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_audit", "0003_auto_20200917_1155"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("sign_up", "Sign Up"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("invitation_created", "Invite Created"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("custom_", "Custom Prefix"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
@ -1,23 +1,27 @@
|
||||
"""passbook audit models"""
|
||||
from enum import Enum
|
||||
from inspect import getmodule, stack
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models.base import Model
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.debug import SafeExceptionReporterFilter
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from guardian.utils import get_anonymous_user
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.middleware import (
|
||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||
SESSION_IMPERSONATE_USER,
|
||||
)
|
||||
from passbook.core.models import User
|
||||
from passbook.lib.utils.http import get_client_ip
|
||||
|
||||
LOGGER = get_logger()
|
||||
LOGGER = get_logger("passbook.audit")
|
||||
|
||||
|
||||
def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
@ -36,6 +40,35 @@ def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
return final_dict
|
||||
|
||||
|
||||
def model_to_dict(model: Model) -> Dict[str, Any]:
|
||||
"""Convert model to dict"""
|
||||
name = str(model)
|
||||
if hasattr(model, "name"):
|
||||
name = model.name
|
||||
return {
|
||||
"app": model._meta.app_label,
|
||||
"model_name": model._meta.model_name,
|
||||
"pk": model.pk,
|
||||
"name": name,
|
||||
}
|
||||
|
||||
|
||||
def get_user(user: User, original_user: Optional[User] = None) -> Dict[str, Any]:
|
||||
"""Convert user object to dictionary, optionally including the original user"""
|
||||
if isinstance(user, AnonymousUser):
|
||||
user = get_anonymous_user()
|
||||
user_data = {
|
||||
"username": user.username,
|
||||
"pk": user.pk,
|
||||
"email": user.email,
|
||||
}
|
||||
if original_user:
|
||||
original_data = get_user(original_user)
|
||||
original_data["on_behalf_of"] = user_data
|
||||
return original_data
|
||||
return user_data
|
||||
|
||||
|
||||
def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
"""clean source of all Models that would interfere with the JSONField.
|
||||
Models are replaced with a dictionary of {
|
||||
@ -48,18 +81,7 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
if isinstance(value, dict):
|
||||
final_dict[key] = sanitize_dict(value)
|
||||
elif isinstance(value, models.Model):
|
||||
model_content_type = ContentType.objects.get_for_model(value)
|
||||
name = str(value)
|
||||
if hasattr(value, "name"):
|
||||
name = value.name
|
||||
final_dict[key] = sanitize_dict(
|
||||
{
|
||||
"app": model_content_type.app_label,
|
||||
"model_name": model_content_type.model,
|
||||
"pk": value.pk,
|
||||
"name": name,
|
||||
}
|
||||
)
|
||||
final_dict[key] = sanitize_dict(model_to_dict(value))
|
||||
elif isinstance(value, UUID):
|
||||
final_dict[key] = value.hex
|
||||
else:
|
||||
@ -67,36 +89,39 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
return final_dict
|
||||
|
||||
|
||||
class EventAction(Enum):
|
||||
class EventAction(models.TextChoices):
|
||||
"""All possible actions to save into the audit log"""
|
||||
|
||||
LOGIN = "login"
|
||||
LOGIN_FAILED = "login_failed"
|
||||
LOGOUT = "logout"
|
||||
|
||||
SIGN_UP = "sign_up"
|
||||
AUTHORIZE_APPLICATION = "authorize_application"
|
||||
SUSPICIOUS_REQUEST = "suspicious_request"
|
||||
SIGN_UP = "sign_up"
|
||||
PASSWORD_RESET = "password_reset" # noqa # nosec
|
||||
PASSWORD_SET = "password_set" # noqa # nosec
|
||||
|
||||
INVITE_CREATED = "invitation_created"
|
||||
INVITE_USED = "invitation_used"
|
||||
CUSTOM = "custom"
|
||||
|
||||
@staticmethod
|
||||
def as_choices():
|
||||
"""Generate choices of actions used for database"""
|
||||
return tuple(
|
||||
(x, y.value) for x, y in getattr(EventAction, "__members__").items()
|
||||
)
|
||||
SOURCE_LINKED = "source_linked"
|
||||
|
||||
IMPERSONATION_STARTED = "impersonation_started"
|
||||
IMPERSONATION_ENDED = "impersonation_ended"
|
||||
|
||||
MODEL_CREATED = "model_created"
|
||||
MODEL_UPDATED = "model_updated"
|
||||
MODEL_DELETED = "model_deleted"
|
||||
|
||||
CUSTOM_PREFIX = "custom_"
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
"""An individual audit log event"""
|
||||
|
||||
event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
action = models.TextField(choices=EventAction.as_choices())
|
||||
user = models.JSONField(default=dict)
|
||||
action = models.TextField(choices=EventAction.choices)
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
app = models.TextField()
|
||||
context = models.JSONField(default=dict, blank=True)
|
||||
@ -111,20 +136,18 @@ class Event(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def new(
|
||||
action: EventAction,
|
||||
action: Union[str, EventAction],
|
||||
app: Optional[str] = None,
|
||||
_inspect_offset: int = 1,
|
||||
**kwargs,
|
||||
) -> "Event":
|
||||
"""Create new Event instance from arguments. Instance is NOT saved."""
|
||||
if not isinstance(action, EventAction):
|
||||
raise ValueError(
|
||||
f"action must be EventAction instance but was {type(action)}"
|
||||
)
|
||||
action = EventAction.CUSTOM_PREFIX + action
|
||||
if not app:
|
||||
app = getmodule(stack()[_inspect_offset][0]).__name__
|
||||
cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs))
|
||||
event = Event(action=action.value, app=app, context=cleaned_kwargs)
|
||||
event = Event(action=action, app=app, context=cleaned_kwargs)
|
||||
return event
|
||||
|
||||
def from_http(
|
||||
@ -134,12 +157,19 @@ class Event(models.Model):
|
||||
Events independently from requests.
|
||||
`user` arguments optionally overrides user from requests."""
|
||||
if hasattr(request, "user"):
|
||||
if isinstance(request.user, AnonymousUser):
|
||||
self.user = get_anonymous_user()
|
||||
else:
|
||||
self.user = request.user
|
||||
self.user = get_user(
|
||||
request.user,
|
||||
request.session.get(SESSION_IMPERSONATE_ORIGINAL_USER, None),
|
||||
)
|
||||
if user:
|
||||
self.user = user
|
||||
self.user = get_user(user)
|
||||
# Check if we're currently impersonating, and add that user
|
||||
if hasattr(request, "session"):
|
||||
if SESSION_IMPERSONATE_ORIGINAL_USER in request.session:
|
||||
self.user = get_user(request.session[SESSION_IMPERSONATE_ORIGINAL_USER])
|
||||
self.user["on_behalf_of"] = get_user(
|
||||
request.session[SESSION_IMPERSONATE_USER]
|
||||
)
|
||||
# User 255.255.255.255 as fallback if IP cannot be determined
|
||||
self.client_ip = get_client_ip(request) or "255.255.255.255"
|
||||
# If there's no app set, we get it from the requests too
|
||||
|
@ -20,12 +20,12 @@ from passbook.stages.user_write.signals import user_write
|
||||
class EventNewThread(Thread):
|
||||
"""Create Event in background thread"""
|
||||
|
||||
action: EventAction
|
||||
action: str
|
||||
request: HttpRequest
|
||||
kwargs: Dict[str, Any]
|
||||
user: Optional[User] = None
|
||||
|
||||
def __init__(self, action: EventAction, request: HttpRequest, **kwargs):
|
||||
def __init__(self, action: str, request: HttpRequest, **kwargs):
|
||||
super().__init__()
|
||||
self.action = action
|
||||
self.request = request
|
||||
@ -57,7 +57,7 @@ def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
|
||||
# pylint: disable=unused-argument
|
||||
def on_user_write(sender, request: HttpRequest, user: User, data: Dict[str, Any], **_):
|
||||
"""Log User write"""
|
||||
thread = EventNewThread(EventAction.CUSTOM, request, **data)
|
||||
thread = EventNewThread("stages/user_write", request, **data)
|
||||
thread.user = user
|
||||
thread.run()
|
||||
|
||||
|
@ -40,12 +40,28 @@
|
||||
</div>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<code>{{ entry.context }}</code>
|
||||
<div>
|
||||
<div>
|
||||
<code>{{ entry.context }}</code>
|
||||
</div>
|
||||
{% if entry.user.on_behalf_of %}
|
||||
<small>
|
||||
{% blocktrans with username=entry.user.on_behalf_of.username %}
|
||||
On behalf of {{ username }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ entry.user }}
|
||||
</span>
|
||||
<div>
|
||||
<div>{{ entry.user.username }}</div>
|
||||
<small>
|
||||
{% blocktrans with pk=entry.user.pk %}
|
||||
ID: {{ pk }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
|
@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from passbook.audit.models import Event, EventAction
|
||||
from passbook.audit.models import Event
|
||||
from passbook.policies.dummy.models import DummyPolicy
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ class TestAuditEvent(TestCase):
|
||||
|
||||
def test_new_with_model(self):
|
||||
"""Create a new Event passing a model as kwarg"""
|
||||
event = Event.new(EventAction.CUSTOM, test={"model": get_anonymous_user()})
|
||||
event = Event.new("unittest", test={"model": get_anonymous_user()})
|
||||
event.save() # We save to ensure nothing is un-saveable
|
||||
model_content_type = ContentType.objects.get_for_model(get_anonymous_user())
|
||||
self.assertEqual(
|
||||
@ -24,7 +24,7 @@ class TestAuditEvent(TestCase):
|
||||
def test_new_with_uuid_model(self):
|
||||
"""Create a new Event passing a model (with UUID PK) as kwarg"""
|
||||
temp_model = DummyPolicy.objects.create(name="test", result=True)
|
||||
event = Event.new(EventAction.CUSTOM, model=temp_model)
|
||||
event = Event.new("unittest", model=temp_model)
|
||||
event.save() # We save to ensure nothing is un-saveable
|
||||
model_content_type = ContentType.objects.get_for_model(temp_model)
|
||||
self.assertEqual(
|
||||
|
@ -11,7 +11,7 @@ class GroupSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
model = Group
|
||||
fields = ["pk", "name", "parent", "user_set", "attributes"]
|
||||
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
|
||||
|
||||
|
||||
class GroupViewSet(ModelViewSet):
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""User API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.serializers import BooleanField, ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.core.models import User
|
||||
@ -8,10 +8,12 @@ from passbook.core.models import User
|
||||
class UserSerializer(ModelSerializer):
|
||||
"""User Serializer"""
|
||||
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = ["pk", "username", "name", "email"]
|
||||
fields = ["pk", "username", "name", "is_superuser", "email"]
|
||||
|
||||
|
||||
class UserViewSet(ModelViewSet):
|
||||
|
@ -29,7 +29,16 @@ class ApplicationForm(forms.ModelForm):
|
||||
]
|
||||
widgets = {
|
||||
"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_publisher": forms.TextInput(),
|
||||
}
|
||||
|
@ -18,21 +18,19 @@ class GroupForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.pk:
|
||||
self.initial["members"] = self.instance.user_set.values_list(
|
||||
"pk", flat=True
|
||||
)
|
||||
self.initial["members"] = self.instance.users.values_list("pk", flat=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
if instance.pk:
|
||||
instance.user_set.clear()
|
||||
instance.user_set.add(*self.cleaned_data["members"])
|
||||
instance.users.clear()
|
||||
instance.users.add(*self.cleaned_data["members"])
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Group
|
||||
fields = ["name", "parent", "members", "attributes"]
|
||||
fields = ["name", "is_superuser", "parent", "members", "attributes"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"attributes": CodeMirrorWidget,
|
||||
|
56
passbook/core/middleware.py
Normal file
56
passbook/core/middleware.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""passbook admin Middleware to impersonate users"""
|
||||
from logging import Logger
|
||||
from threading import local
|
||||
from typing import Callable
|
||||
from uuid import uuid4
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
SESSION_IMPERSONATE_USER = "passbook_impersonate_user"
|
||||
SESSION_IMPERSONATE_ORIGINAL_USER = "passbook_impersonate_original_user"
|
||||
LOCAL = local()
|
||||
|
||||
|
||||
class ImpersonateMiddleware:
|
||||
"""Middleware to impersonate users"""
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
# No permission checks are done here, they need to be checked before
|
||||
# SESSION_IMPERSONATE_USER is set.
|
||||
|
||||
if SESSION_IMPERSONATE_USER in request.session:
|
||||
request.user = request.session[SESSION_IMPERSONATE_USER]
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
class RequestIDMiddleware:
|
||||
"""Add a unique ID to every request"""
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
if not hasattr(request, "request_id"):
|
||||
request_id = uuid4().hex
|
||||
setattr(request, "request_id", request_id)
|
||||
LOCAL.passbook = {"request_id": request_id}
|
||||
response = self.get_response(request)
|
||||
response["X-passbook-id"] = request.request_id
|
||||
del LOCAL.passbook["request_id"]
|
||||
return response
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def structlog_add_request_id(logger: Logger, method_name: str, event_dict):
|
||||
"""If threadlocal has passbook defined, add request_id to log"""
|
||||
if hasattr(LOCAL, "passbook"):
|
||||
event_dict["request_id"] = LOCAL.passbook.get("request_id", "")
|
||||
return event_dict
|
@ -1,7 +1,7 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-23 16:40
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -14,9 +14,7 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
pbadmin, _ = User.objects.using(db_alias).get_or_create(
|
||||
username="pbadmin", email="root@localhost", name="passbook Default Admin"
|
||||
)
|
||||
pbadmin.set_password("pbadmin") # noqa # nosec
|
||||
pbadmin.is_superuser = True
|
||||
pbadmin.is_staff = True
|
||||
pbadmin.set_password("pbadmin", signal=False) # noqa # nosec
|
||||
pbadmin.save()
|
||||
|
||||
|
||||
@ -27,5 +25,15 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(model_name="user", name="is_superuser",),
|
||||
migrations.RemoveField(model_name="user", name="is_staff",),
|
||||
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()),],
|
||||
),
|
||||
]
|
24
passbook/core/migrations/0010_auto_20200917_1021.py
Normal file
24
passbook/core/migrations/0010_auto_20200917_1021.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-17 10:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0009_group_is_superuser"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={
|
||||
"permissions": (
|
||||
("reset_user_password", "Reset Password"),
|
||||
("impersonate", "Can impersonate other users"),
|
||||
),
|
||||
"verbose_name": "User",
|
||||
"verbose_name_plural": "Users",
|
||||
},
|
||||
),
|
||||
]
|
@ -4,6 +4,7 @@ from typing import Any, Optional, Type
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
from django.db import models
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.forms import ModelForm
|
||||
@ -22,6 +23,7 @@ from passbook.lib.models import CreatedUpdatedModel
|
||||
from passbook.policies.models import PolicyBindingModel
|
||||
|
||||
LOGGER = get_logger()
|
||||
PASSBOOK_USER_DEBUG = "passbook_user_debug"
|
||||
|
||||
|
||||
def default_token_duration():
|
||||
@ -33,7 +35,12 @@ class Group(models.Model):
|
||||
"""Custom Group model which supports a basic hierarchy"""
|
||||
|
||||
group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
|
||||
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(
|
||||
"Group",
|
||||
blank=True,
|
||||
@ -51,6 +58,14 @@ class Group(models.Model):
|
||||
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):
|
||||
"""Custom User model to allow easier adding o f user-based settings"""
|
||||
|
||||
@ -58,20 +73,35 @@ class User(GuardianUserMixin, AbstractUser):
|
||||
name = models.TextField(help_text=_("User's display name."))
|
||||
|
||||
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)
|
||||
|
||||
attributes = models.JSONField(default=dict, blank=True)
|
||||
|
||||
def set_password(self, password):
|
||||
if self.pk:
|
||||
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, signal=True):
|
||||
if self.pk and signal:
|
||||
password_changed.send(sender=self, user=self, password=password)
|
||||
self.password_change_date = now()
|
||||
return super().set_password(password)
|
||||
|
||||
class Meta:
|
||||
|
||||
permissions = (("reset_user_password", "Reset Password"),)
|
||||
permissions = (
|
||||
("reset_user_password", "Reset Password"),
|
||||
("impersonate", "Can impersonate other users"),
|
||||
)
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
|
||||
@ -92,6 +122,12 @@ class Provider(models.Model):
|
||||
|
||||
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]:
|
||||
"""Return Form class used to edit this object"""
|
||||
raise NotImplementedError
|
||||
@ -119,6 +155,14 @@ class Application(PolicyBindingModel):
|
||||
meta_description = 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.get_provider().launch_url
|
||||
return None
|
||||
|
||||
def get_provider(self) -> Optional[Provider]:
|
||||
"""Get casted provider instance"""
|
||||
if not self.provider:
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""passbook core tasks"""
|
||||
from django.utils.timezone import now
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import ExpiringModel
|
||||
@ -12,5 +13,10 @@ def clean_expired_models():
|
||||
"""Remove expired objects"""
|
||||
for cls in ExpiringModel.__subclasses__():
|
||||
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)
|
||||
|
@ -6,8 +6,8 @@
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff2' %}" as="font" type="font/woff2">
|
||||
<link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff' %}" as="font" type="font/woff">
|
||||
<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 name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>{% block title %}{% trans title|default:config.passbook.branding.title %}{% endblock %}</title>
|
||||
@ -21,13 +21,13 @@
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% if 'impersonate_id' in request.session %}
|
||||
{% if 'passbook_impersonate_user' in request.session %}
|
||||
<div class="pf-c-banner pf-m-warning pf-c-alert pf-m-sticky">
|
||||
<div class="pf-l-flex pf-m-justify-content-center pf-m-justify-content-space-between-on-lg pf-m-nowrap" style="height: 100%;">
|
||||
<div class=""></div>
|
||||
<div class="pf-u-display-none pf-u-display-block-on-lg">
|
||||
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
|
||||
<a href="?__unimpersonate=True" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
|
||||
<a href="{% url 'passbook_core:impersonate-end' %}?back={{ request.get_full_path }}" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
|
||||
</div>
|
||||
<div class=""></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 %}
|
@ -7,6 +7,7 @@
|
||||
<style>
|
||||
img.app-icon {
|
||||
max-height: 72px;
|
||||
width: auto !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -24,7 +25,7 @@
|
||||
{% if applications %}
|
||||
<div class="pf-l-gallery pf-m-gutter">
|
||||
{% 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">
|
||||
{% if not app.meta_icon_url %}
|
||||
<i class="pf-icon pf-icon-arrow"></i>
|
||||
|
55
passbook/core/tests/test_impersonation.py
Normal file
55
passbook/core/tests/test_impersonation.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""impersonation tests"""
|
||||
from django.shortcuts import reverse
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
from passbook.core.models import User
|
||||
|
||||
|
||||
class TestImpersonation(TestCase):
|
||||
"""impersonation tests"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.other_user = User.objects.create(username="to-impersonate")
|
||||
self.pbadmin = User.objects.get(username="pbadmin")
|
||||
|
||||
def test_impersonate_simple(self):
|
||||
"""test simple impersonation and un-impersonation"""
|
||||
self.client.force_login(self.pbadmin)
|
||||
|
||||
self.client.get(
|
||||
reverse(
|
||||
"passbook_core:impersonate-init", kwargs={"user_id": self.other_user.pk}
|
||||
)
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("passbook_core:overview"))
|
||||
self.assertIn(self.other_user.username, response.content.decode())
|
||||
self.assertNotIn(self.pbadmin.username, response.content.decode())
|
||||
|
||||
self.client.get(reverse("passbook_core:impersonate-end"))
|
||||
|
||||
response = self.client.get(reverse("passbook_core:overview"))
|
||||
self.assertNotIn(self.other_user.username, response.content.decode())
|
||||
self.assertIn(self.pbadmin.username, response.content.decode())
|
||||
|
||||
def test_impersonate_denied(self):
|
||||
"""test impersonation without permissions"""
|
||||
self.client.force_login(self.other_user)
|
||||
|
||||
self.client.get(
|
||||
reverse(
|
||||
"passbook_core:impersonate-init", kwargs={"user_id": self.pbadmin.pk}
|
||||
)
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("passbook_core:overview"))
|
||||
self.assertIn(self.other_user.username, response.content.decode())
|
||||
self.assertNotIn(self.pbadmin.username, response.content.decode())
|
||||
|
||||
def test_un_impersonate_empty(self):
|
||||
"""test un-impersonation without impersonating first"""
|
||||
self.client.force_login(self.other_user)
|
||||
|
||||
response = self.client.get(reverse("passbook_core:impersonate-end"))
|
||||
self.assertRedirects(response, reverse("passbook_core:overview"))
|
@ -13,7 +13,7 @@ class TestOverviewViews(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_superuser(
|
||||
self.user = User.objects.create_user(
|
||||
username="unittest user",
|
||||
email="unittest@example.com",
|
||||
password="".join(
|
||||
|
@ -13,7 +13,7 @@ class TestUserViews(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_superuser(
|
||||
self.user = User.objects.create_user(
|
||||
username="unittest user",
|
||||
email="unittest@example.com",
|
||||
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,11 +1,22 @@
|
||||
"""passbook URL Configuration"""
|
||||
from django.urls import path
|
||||
|
||||
from passbook.core.views import overview, user
|
||||
from passbook.core.views import impersonate, overview, user
|
||||
|
||||
urlpatterns = [
|
||||
# User views
|
||||
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"),
|
||||
# Overview
|
||||
path("", overview.OverviewView.as_view(), name="overview"),
|
||||
# Impersonation
|
||||
path(
|
||||
"-/impersonation/<int:user_id>/",
|
||||
impersonate.ImpersonateInitView.as_view(),
|
||||
name="impersonate-init",
|
||||
),
|
||||
path(
|
||||
"-/impersonation/end/",
|
||||
impersonate.ImpersonateEndView.as_view(),
|
||||
name="impersonate-end",
|
||||
),
|
||||
]
|
||||
|
58
passbook/core/views/impersonate.py
Normal file
58
passbook/core/views/impersonate.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""passbook impersonation views"""
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views import View
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.audit.models import Event, EventAction
|
||||
from passbook.core.middleware import (
|
||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||
SESSION_IMPERSONATE_USER,
|
||||
)
|
||||
from passbook.core.models import User
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class ImpersonateInitView(View):
|
||||
"""Initiate Impersonation"""
|
||||
|
||||
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
|
||||
"""Impersonation handler, checks permissions"""
|
||||
if not request.user.has_perm("impersonate"):
|
||||
LOGGER.debug(
|
||||
"User attempted to impersonate without permissions", user=request.user
|
||||
)
|
||||
return HttpResponse("Unauthorized", status=401)
|
||||
|
||||
user_to_be = get_object_or_404(User, pk=user_id)
|
||||
|
||||
request.session[SESSION_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||
request.session[SESSION_IMPERSONATE_USER] = user_to_be
|
||||
|
||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||
|
||||
return redirect("passbook_core:overview")
|
||||
|
||||
|
||||
class ImpersonateEndView(View):
|
||||
"""End User impersonation"""
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
"""End Impersonation handler"""
|
||||
if (
|
||||
SESSION_IMPERSONATE_USER not in request.session
|
||||
or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||
):
|
||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||
return redirect("passbook_core:overview")
|
||||
|
||||
original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
del request.session[SESSION_IMPERSONATE_USER]
|
||||
del request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
||||
|
||||
return redirect("passbook_core:overview")
|
@ -1,14 +0,0 @@
|
||||
"""passbook core utils view"""
|
||||
from django.utils.translation import gettext 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):
|
||||
"""Exception raised when a Flow does not apply to a user."""
|
||||
"""Flow does not apply to current user (denied by policy)."""
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
class Command(BaseCommand): # pragma: no cover
|
||||
"""Apply flow from commandline"""
|
||||
|
||||
@no_translations
|
||||
|
@ -177,6 +177,6 @@ class FlowPlanner:
|
||||
marker = ReevaluateMarker(binding=binding, user=user)
|
||||
plan.markers.append(marker)
|
||||
LOGGER.debug(
|
||||
"f(plan): Finished building", flow=self.flow, duration_s=span.timestamp,
|
||||
"f(plan): Finished building", flow=self.flow,
|
||||
)
|
||||
return plan
|
||||
|
57
passbook/flows/templates/flows/denied_shell.html
Normal file
57
passbook/flows/templates/flows/denied_shell.html
Normal file
@ -0,0 +1,57 @@
|
||||
{% extends 'login/base.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 'Request has been denied.' %}
|
||||
</p>
|
||||
{% if error %}
|
||||
<hr>
|
||||
<p>
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if policy_result %}
|
||||
<hr>
|
||||
<em>
|
||||
{% trans 'Explanation:' %}
|
||||
</em>
|
||||
<ul class="pf-c-list">
|
||||
{% for source_result in policy_result.source_results %}
|
||||
<li>
|
||||
{% blocktrans with name=source_result.source_policy.name result=source_result.passing %}
|
||||
Policy '{{ name }}' returned result '{{ result }}'
|
||||
{% endblocktrans %}
|
||||
{% if source_result.messages %}
|
||||
<ul class="pf-c-list">
|
||||
{% for message in source_result.messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
@ -105,15 +105,10 @@ class TestFlowTransfer(TransactionTestCase):
|
||||
order=2,
|
||||
type=FieldTypes.PASSWORD,
|
||||
)
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name=generate_client_id(), expression="return True",
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name=generate_client_id())
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.validation_policies.set([password_policy])
|
||||
first_stage.save()
|
||||
|
||||
flow = Flow.objects.create(
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""flow views tests"""
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_str
|
||||
@ -12,6 +13,7 @@ from passbook.flows.planner import FlowPlan
|
||||
from passbook.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
|
||||
from passbook.lib.config import CONFIG
|
||||
from passbook.policies.dummy.models import DummyPolicy
|
||||
from passbook.policies.http import AccessDeniedResponse
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.policies.types import PolicyResult
|
||||
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))
|
||||
|
||||
|
||||
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):
|
||||
"""Test views logic"""
|
||||
|
||||
@ -48,9 +59,12 @@ class TestFlowExecutor(TestCase):
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||
),
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(cancel_mock.call_count, 1)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(cancel_mock.call_count, 2)
|
||||
|
||||
@patch(
|
||||
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
@patch(
|
||||
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
|
||||
)
|
||||
@ -66,9 +80,13 @@ class TestFlowExecutor(TestCase):
|
||||
response = self.client.get(
|
||||
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)
|
||||
|
||||
@patch(
|
||||
"passbook.flows.views.to_stage_response", TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
def test_invalid_empty_flow(self):
|
||||
"""Tests that an empty flow returns the correct error message"""
|
||||
flow = Flow.objects.create(
|
||||
@ -81,7 +99,8 @@ class TestFlowExecutor(TestCase):
|
||||
response = self.client.get(
|
||||
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)
|
||||
|
||||
def test_invalid_flow_redirect(self):
|
||||
@ -96,8 +115,10 @@ class TestFlowExecutor(TestCase):
|
||||
dest = "/unique-string"
|
||||
url = reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug})
|
||||
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, dest)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content), {"type": "redirect", "to": dest},
|
||||
)
|
||||
|
||||
def test_multi_stage_flow(self):
|
||||
"""Test a full flow with multiple stages"""
|
||||
|
@ -6,12 +6,10 @@ from passbook.flows.views import (
|
||||
CancelView,
|
||||
FlowExecutorShellView,
|
||||
FlowExecutorView,
|
||||
FlowPermissionDeniedView,
|
||||
ToDefaultFlow,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("-/denied/", FlowPermissionDeniedView.as_view(), name="denied"),
|
||||
path(
|
||||
"-/default/authentication/",
|
||||
ToDefaultFlow.as_view(designation=FlowDesignation.AUTHENTICATION),
|
||||
|
@ -9,7 +9,7 @@ from django.http import (
|
||||
HttpResponseRedirect,
|
||||
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.utils.decorators import method_decorator
|
||||
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 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.models import Flow, FlowDesignation, Stage
|
||||
from passbook.flows.planner import FlowPlan, FlowPlanner
|
||||
from passbook.lib.utils.reflection import class_to_path
|
||||
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()
|
||||
# Argument used to redirect user after login
|
||||
@ -54,7 +54,7 @@ class FlowExecutorView(View):
|
||||
LOGGER.debug("f(exec): Redirecting to next on fail")
|
||||
return redirect(self.request.GET.get(NEXT_ARG_NAME))
|
||||
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:
|
||||
# Early check if theres an active Plan for the current session
|
||||
@ -79,10 +79,10 @@ class FlowExecutorView(View):
|
||||
self.plan = self._initiate_plan()
|
||||
except FlowNonApplicableException as 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:
|
||||
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
|
||||
# as it hasn't been successfully passed yet
|
||||
next_stage = self.plan.next()
|
||||
@ -115,14 +115,7 @@ class FlowExecutorView(View):
|
||||
return to_stage_response(request, stage_response)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
LOGGER.exception(exc)
|
||||
return to_stage_response(
|
||||
request,
|
||||
render(
|
||||
request,
|
||||
"flows/error.html",
|
||||
{"error": exc, "tb": "".join(format_tb(exc.__traceback__))},
|
||||
),
|
||||
)
|
||||
return to_stage_response(request, FlowErrorResponse(request, exc))
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""pass post request to current stage"""
|
||||
@ -137,14 +130,7 @@ class FlowExecutorView(View):
|
||||
return to_stage_response(request, stage_response)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
LOGGER.exception(exc)
|
||||
return to_stage_response(
|
||||
request,
|
||||
render(
|
||||
request,
|
||||
"flows/error.html",
|
||||
{"error": exc, "tb": "".join(format_tb(exc.__traceback__))},
|
||||
),
|
||||
)
|
||||
return to_stage_response(request, FlowErrorResponse(request, exc))
|
||||
|
||||
def _initiate_plan(self) -> FlowPlan:
|
||||
planner = FlowPlanner(self.flow)
|
||||
@ -193,12 +179,19 @@ class FlowExecutorView(View):
|
||||
)
|
||||
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
|
||||
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)
|
||||
self.cancel()
|
||||
return redirect_with_qs("passbook_flows:denied", self.request.GET)
|
||||
response = AccessDeniedResponse(
|
||||
self.request, template="flows/denied_shell.html"
|
||||
)
|
||||
response.error_message = error_message
|
||||
return to_stage_response(self.request, response)
|
||||
|
||||
def cancel(self):
|
||||
"""Cancel current execution and return a redirect"""
|
||||
@ -212,8 +205,30 @@ class FlowExecutorView(View):
|
||||
del self.request.session[key]
|
||||
|
||||
|
||||
class FlowPermissionDeniedView(PermissionDeniedView):
|
||||
"""User could not be authenticated"""
|
||||
class FlowErrorResponse(TemplateResponse):
|
||||
"""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):
|
||||
|
@ -10,6 +10,7 @@ redis:
|
||||
password: ''
|
||||
cache_db: 0
|
||||
message_queue_db: 1
|
||||
ws_db: 2
|
||||
|
||||
debug: false
|
||||
log_level: info
|
||||
|
@ -1,9 +1,23 @@
|
||||
"""logging helpers"""
|
||||
from logging import Logger
|
||||
from os import getpid
|
||||
from typing import Callable
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def add_process_id(logger, method_name, event_dict):
|
||||
def add_process_id(logger: Logger, method_name: str, event_dict):
|
||||
"""Add the current process ID"""
|
||||
event_dict["pid"] = getpid()
|
||||
return event_dict
|
||||
|
||||
|
||||
def add_common_fields(environment: str) -> Callable:
|
||||
"""Add a common field to easily search for passbook logs"""
|
||||
|
||||
def add_common_field(logger: Logger, method_name: str, event_dict):
|
||||
"""Add a common field to easily search for passbook logs"""
|
||||
event_dict["app"] = "passbook"
|
||||
event_dict["app_environment"] = environment
|
||||
return event_dict
|
||||
|
||||
return add_common_field
|
||||
|
@ -1,13 +1,16 @@
|
||||
"""passbook sentry integration"""
|
||||
from billiard.exceptions import WorkerLostError
|
||||
from botocore.client import ClientError
|
||||
from celery.exceptions import CeleryError
|
||||
from django.core.exceptions import DisallowedHost, ValidationError
|
||||
from django.db import InternalError, OperationalError, ProgrammingError
|
||||
from django_redis.exceptions import ConnectionInterrupted
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from redis.exceptions import ConnectionError as RedisConnectionError
|
||||
from redis.exceptions import RedisError
|
||||
from rest_framework.exceptions import APIException
|
||||
from structlog import get_logger
|
||||
from websockets.exceptions import WebSocketException
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@ -35,6 +38,9 @@ def before_send(event, hint):
|
||||
OSError,
|
||||
RedisError,
|
||||
SentryIgnoredException,
|
||||
WebSocketException,
|
||||
CeleryError,
|
||||
LDAPException,
|
||||
)
|
||||
if "exc_info" in hint:
|
||||
_, exc_value, _ = hint["exc_info"]
|
||||
|
@ -14,7 +14,7 @@ def _get_client_ip_from_meta(meta: Dict[str, Any]) -> Optional[str]:
|
||||
)
|
||||
for _header in headers:
|
||||
if _header in meta:
|
||||
return meta.get(_header)
|
||||
return meta.get(_header).split(", ")[0]
|
||||
return None
|
||||
|
||||
|
||||
|
@ -1,11 +1,5 @@
|
||||
"""passbook lib template utilities"""
|
||||
from django.template import Context, Template, loader
|
||||
|
||||
|
||||
def render_from_string(tmpl: str, ctx: Context) -> str:
|
||||
"""Render template from string to string"""
|
||||
template = Template(tmpl)
|
||||
return template.render(ctx)
|
||||
from django.template import Context, loader
|
||||
|
||||
|
||||
def render_to_string(template_path: str, ctx: Context) -> str:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user