Compare commits

...

68 Commits

Author SHA1 Message Date
713025d218 new release: 0.11.0-stable 2020-10-11 19:57:03 +02:00
58ae159835 outposts: disable Kubernetes selection for now 2020-10-11 19:40:22 +02:00
c95efe3cde docs: fix usage of user's groups 2020-10-11 19:29:22 +02:00
b6eb0bf53d providers/oauth2: add missing property_mapping template 2020-10-11 19:29:13 +02:00
610b6c7f70 policies: add PolicyAccessView, which does complete access checking 2020-10-11 19:26:20 +02:00
1ea2d99ff2 ci: run rollup build 2020-10-09 11:33:02 +02:00
67be43679c build(deps): bump boto3 from 1.15.14 to 1.15.15 (#268)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.14 to 1.15.15.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.15.14...1.15.15)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-09 11:32:02 +02:00
fd42389bd5 build(deps-dev): bump rollup-plugin-sourcemaps (#267)
Bumps [rollup-plugin-sourcemaps](https://github.com/maxdavidson/rollup-plugin-sourcemaps) from 0.6.2 to 0.6.3.
- [Release notes](https://github.com/maxdavidson/rollup-plugin-sourcemaps/releases)
- [Changelog](https://github.com/maxdavidson/rollup-plugin-sourcemaps/blob/master/CHANGELOG.md)
- [Commits](https://github.com/maxdavidson/rollup-plugin-sourcemaps/compare/v0.6.2...v0.6.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-09 11:31:47 +02:00
71b1df2fec build(deps): bump rollup in /passbook/static/static (#269)
Bumps [rollup](https://github.com/rollup/rollup) from 2.28.2 to 2.29.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.28.2...v2.29.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-09 09:08:39 +02:00
7a3122f25c docs: add reverse-proxy example config, fix outpost docker-compose 2020-10-08 09:27:28 +02:00
63041d788b core: update application list API to show applications accessible by policy 2020-10-08 09:26:50 +02:00
bfc1bae0bb build(deps): bump boto3 from 1.15.13 to 1.15.14 (#266)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.13 to 1.15.14.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.15.13...1.15.14)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-08 09:18:06 +02:00
8ab7f7fcbb core: make passbook title navigate to overview
closes #264
2020-10-07 19:27:20 +02:00
c1eb8317f7 providers/proxy: update phrasing for basic_auth_* attributes
closes #265
2020-10-07 19:27:06 +02:00
7a578e5e83 admin: dont show check when outpost hasnt connected
closes #263
2020-10-07 19:19:25 +02:00
b10912d8ba proxy: cleanup addHeadersForProxying 2020-10-07 18:02:57 +02:00
ef24b1cde2 docs: update screenshots 2020-10-07 18:02:26 +02:00
26cacc2a06 build(deps): bump boto3 from 1.15.12 to 1.15.13 (#259)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.12 to 1.15.13.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.15.12...1.15.13)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-07 11:54:32 +02:00
ca0e89c799 build(deps): bump @patternfly/patternfly in /passbook/static/static (#261)
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.42.2 to 4.50.4.
- [Release notes](https://github.com/patternfly/patternfly/releases)
- [Changelog](https://github.com/patternfly/patternfly/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/patternfly/patternfly/compare/prerelease-v4.42.2...prerelease-v4.50.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-07 09:10:47 +02:00
17950119ad build(deps): bump django-otp from 1.0.0 to 1.0.1 (#260)
Bumps [django-otp](https://github.com/django-otp/django-otp) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/django-otp/django-otp/releases)
- [Changelog](https://github.com/django-otp/django-otp/blob/master/CHANGES.rst)
- [Commits](https://github.com/django-otp/django-otp/compare/v1.0.0...v1.0.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-07 09:10:25 +02:00
876618c1ec build(deps): bump @fortawesome/fontawesome-free (#258)
Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 5.15.0 to 5.15.1.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/master/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/5.15.0...5.15.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-06 10:19:05 +02:00
2293ab69b9 build(deps): bump boto3 from 1.15.11 to 1.15.12 (#257)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.11 to 1.15.12.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.15.11...1.15.12)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-06 09:55:56 +02:00
9df00e09a4 root: fix static docker's rollup build 2020-10-06 00:06:53 +02:00
cf6ce9c915 audit: optimize eventaction, 2020-10-05 23:43:56 +02:00
3b61191614 outpost: enable docker controller 2020-10-05 23:11:44 +02:00
9954eeac86 proxy: fix broken docker healthcheck 2020-10-05 22:53:26 +02:00
ac88bd5d44 core: hide token value by default 2020-10-05 22:40:30 +02:00
2406a619df root: fix lockfile in dockerfile 2020-10-05 22:37:53 +02:00
63087c9393 root: run backups in server contianer 2020-10-05 22:30:13 +02:00
da9aaf69df admin: add metrics and charts 2020-10-05 22:10:03 +02:00
ae125dd1f0 root: fix missing docker dependency 2020-10-04 15:04:07 +02:00
f636595230 static: add fetch-fill-slot to load data for admin interface 2020-10-04 13:09:03 +02:00
d506e8f1a3 outposts: implement docker controller 2020-10-04 00:41:12 +02:00
d3a96ac7aa outposts: load token async 2020-10-04 00:29:18 +02:00
189b0ec324 admin: expose info as API 2020-10-04 00:28:58 +02:00
c5a6b4961f core: Add Token identifier as sudo-primary key 2020-10-04 00:28:43 +02:00
b590589324 root: add base template for api 2020-10-03 23:20:33 +02:00
9fb1ac98ec Backup/Restore (#256)
* lifecycle: move s3 backup settings to s3 name

* providers/oauth2: fix for alerting for missing certificatekeypair

* lifecycle: add backup commands

see #252

* lifecycle: install postgres-client for 11 and 12

* root: migrate to DBBACKUP_STORAGE_OPTIONS, add region setting

* lifecycle: auto-clean last backups

* helm: add s3 region parameter, add cronjob for backups

* docs: add backup docs

* root: remove backup scheduled task for now
2020-10-03 20:36:36 +02:00
195d8fe71f core: move name field to base Provider 2020-10-03 20:05:16 +02:00
b0602a3215 admin: implement search for all views
see #253
2020-10-03 19:32:01 +02:00
0150a5c58c admin: add SearchListMixin mixin and partial template 2020-10-03 19:05:20 +02:00
b35d27c83e admin: fix pagination template, ensure template is placed correctly in footer 2020-10-03 17:50:17 +02:00
801bb90806 root: lock pyright version 2020-10-03 15:34:53 +02:00
55a83abb26 *: remove deprecated providing_args 2020-10-02 11:18:14 +02:00
c09b4e9713 e2e: fix invalid proxy image being pulled 2020-10-02 10:30:56 +02:00
247015e955 stages/otp_*: Remove duplicate validation for OTP Codes 2020-10-02 10:30:43 +02:00
fe3634be64 build(deps): bump django from 3.1.1 to 3.1.2 (#255)
Bumps [django](https://github.com/django/django) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.1...3.1.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-02 09:50:18 +02:00
ead20b03aa build(deps): bump boto3 from 1.15.9 to 1.15.10 (#254)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.9 to 1.15.10.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.15.9...1.15.10)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-02 09:32:39 +02:00
932a475af7 docs: add notice about vcenter launch URL 2020-10-01 20:01:58 +02:00
e9a1a18ba3 providers/oauth2: ensure that when rs256 is selected, a certificate key pair is selected 2020-10-01 20:01:45 +02:00
6cd9edd38a providers/oauth2: add missing token_validity field to Forms and API 2020-10-01 20:01:28 +02:00
9b5f9167cd root: always enable dbbackup 2020-10-01 13:41:40 +02:00
1f30bcd335 root: lock postgresql to 12 in docker-compose 2020-10-01 10:42:38 +02:00
94eaeb5a60 new release: 0.10.9-stable 2020-10-01 10:24:16 +02:00
a5420fe019 providers/saml: lowercase acs URLs before checking
closes #249
2020-10-01 10:04:20 +02:00
2e1849a732 providers/oauth2: lowercase all uris before checking redirect URI
see #249
2020-10-01 10:00:44 +02:00
4039e96803 build(deps): bump boto3 from 1.15.8 to 1.15.9 (#250)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.8 to 1.15.9.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.15.8...1.15.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-01 09:50:04 +02:00
8f585eca70 stages/identification: replace buggy FilteredSelectMultiple with ArrayFieldSelectMultiple 2020-09-30 23:58:01 +02:00
516455f482 stages/identification: add case_insensitive_matching
closes #248
2020-09-30 23:48:53 +02:00
719099a5af ci: remove deploy as --recreate is deprecated 2020-09-30 23:00:05 +02:00
7f74d32253 docs: update phrasing on tautulli docs 2020-09-30 21:19:04 +02:00
525d271535 *: apply new black styling 2020-09-30 19:34:22 +02:00
9ef39f1e04 root: update black version 2020-09-30 16:39:15 +02:00
9099dc5713 root: fix missing dependencies of uvicorn 2020-09-30 16:11:28 +02:00
c3c525a3f0 lib: re-add Websockets error 2020-09-30 15:55:59 +02:00
e699dfe88c ci: fix CD not working correctly 2020-09-30 15:41:04 +02:00
c0b334eb02 lib: ignore ChannelFull error 2020-09-30 15:40:54 +02:00
815ad26b91 root: add hard uvloop and httptools dependency 2020-09-30 15:37:15 +02:00
203 changed files with 3744 additions and 1106 deletions

View File

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

View File

@ -18,11 +18,11 @@ jobs:
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:0.10.8-stable
-t beryju/passbook:0.11.0-stable
-t beryju/passbook:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.10.8-stable
run: docker push beryju/passbook:0.11.0-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.8-stable \
-t beryju/passbook-proxy:0.11.0-stable \
-t beryju/passbook-proxy:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-proxy:0.10.8-stable
run: docker push beryju/passbook-proxy:0.11.0-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.8-stable
-t beryju/passbook-static:0.11.0-stable
-t beryju/passbook-static:latest
-f static.Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.10.8-stable
run: docker push beryju/passbook-static:0.11.0-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest
test-release:
@ -114,5 +114,5 @@ jobs:
SENTRY_PROJECT: passbook
SENTRY_URL: https://sentry.beryju.org
with:
tagName: 0.10.8-stable
tagName: 0.11.0-stable
environment: beryjuorg-prod

View File

@ -16,7 +16,11 @@ COPY --from=locker /app/requirements.txt /
COPY --from=locker /app/requirements-dev.txt /
RUN apt-get update && \
apt-get install -y --no-install-recommends postgresql-client-11 build-essential && \
apt-get install -y --no-install-recommends curl ca-certificates gnupg && \
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -y --no-install-recommends postgresql-client-12 postgresql-client-11 build-essential && \
apt-get clean && \
pip install -r /requirements.txt --no-cache-dir && \
apt-get remove --purge -y build-essential && \

View File

@ -11,7 +11,7 @@ lint-fix:
black passbook e2e lifecycle
lint:
pyright pyright e2e lifecycle
pyright passbook e2e lifecycle
bandit -r passbook e2e lifecycle
pylint passbook e2e lifecycle
prospector

View File

@ -28,7 +28,7 @@ packaging = "*"
psycopg2-binary = "*"
pycryptodome = "*"
pyjwkest = "*"
uvicorn = "*"
uvicorn = {extras = ["standard"],version = "*"}
gunicorn = "*"
pyyaml = "*"
qrcode = "*"
@ -43,6 +43,7 @@ dacite = "*"
channels = "*"
channels-redis = "*"
kubernetes = "*"
docker = "*"
[requires]
python_version = "3.8"
@ -50,12 +51,11 @@ python_version = "3.8"
[dev-packages]
autopep8 = "*"
bandit = "*"
black = "==19.10b0"
black = "==20.8b1"
bumpversion = "*"
colorama = "*"
coverage = "*"
django-debug-toolbar = "*"
docker = "*"
pylint = "*"
pylint-django = "*"
selenium = "*"

376
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "39e0a747699dc7e528a215395cc505b380e40e6bd0295fdf4c373a871a9bde96"
"sha256": "77737b63b2469755fd2a3d06b23054ae42b07b3f24cf887472f05fb8ab165cc6"
},
"pipfile-spec": 6,
"requires": {
@ -16,24 +16,6 @@
]
},
"default": {
"aiohttp": {
"hashes": [
"sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e",
"sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326",
"sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a",
"sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654",
"sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a",
"sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4",
"sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17",
"sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec",
"sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd",
"sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48",
"sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59",
"sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"
],
"markers": "python_version >= '3.6'",
"version": "==3.6.2"
},
"aioredis": {
"hashes": [
"sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
@ -92,18 +74,18 @@
},
"boto3": {
"hashes": [
"sha256:348cddfd56be6c8b759544f99d20d633cc6a65d346651700ad8ac93a5214c032",
"sha256:c4ccd1f260660603f965bcc145de87e09dd1229040784fe119cd08caeb00dbe9"
"sha256:1627f97e050be59cfef839481acc73eba4b29e475a067f374a493e6b7f25601e",
"sha256:8aafa1ec72451cf70fe6d8c7e86b1a83d2e195d4dda95e5bf21e40132a38c309"
],
"index": "pypi",
"version": "==1.15.8"
"version": "==1.15.15"
},
"botocore": {
"hashes": [
"sha256:07b399997d8050d3ed1150d4d657b46558999f75246eb5b02cee78b9798b3bd5",
"sha256:53a778e6b715ad2ae39bf98e088962e8d524133fb458d83f080964254adc9885"
"sha256:3b9179edbba61c96f5d1eaa4328c9cda686bd461e102c5878c4880479c24e268",
"sha256:f59437ff69d260faa876a2bb7d76debcbbb3b1a497e9ff49550a1a5501679720"
],
"version": "==1.18.8"
"version": "==1.18.15"
},
"cachetools": {
"hashes": [
@ -281,11 +263,11 @@
},
"django": {
"hashes": [
"sha256:59c8125ca873ed3bdae9c12b146fbbd6ed8d0f743e4cf5f5817af50c51f1fc2f",
"sha256:b5fbb818e751f660fa2d576d9f40c34a4c615c8b48dd383f5216e609f383371f"
"sha256:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc",
"sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4"
],
"index": "pypi",
"version": "==3.1.1"
"version": "==3.1.2"
},
"django-cors-middleware": {
"hashes": [
@ -328,11 +310,11 @@
},
"django-otp": {
"hashes": [
"sha256:0c9edbb3f4abc9ac6e43daf0a9e0e293e99ad917641cf8d7dbc49d613bcb5cd4",
"sha256:ace831f3a0f2c2267e4f7219c78deeb3b41c2dc8ae44b03daebb4fb85dabeb43"
"sha256:2fb1c8dbd7e7ae76a65b63d89d3d8c3e1105a48bc29830b81c6e417a89380658",
"sha256:fef1f2de9a52bc37e16211b98b4323e5b34fa24739116fbe3d1ff018c17ebea8"
],
"index": "pypi",
"version": "==1.0.0"
"version": "==1.0.1"
},
"django-prometheus": {
"hashes": [
@ -381,6 +363,14 @@
"index": "pypi",
"version": "==0.3.0"
},
"docker": {
"hashes": [
"sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828",
"sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2"
],
"index": "pypi",
"version": "==4.3.1"
},
"drf-yasg": {
"hashes": [
"sha256:5572e9d5baab9f6b49318169df9789f7399d0e3c7bdac8fdb8dfccf1d5d2b1ca",
@ -412,10 +402,10 @@
},
"google-auth": {
"hashes": [
"sha256:a73e6fb6d232ed1293ef9a5301e6f8aada7880d19c65d7f63e130dc50ec05593",
"sha256:e86e72142d939a8d90a772947268aacc127ab7a1d1d6f3e0fecca7a8d74d8257"
"sha256:712dd7d140a9a1ea218e5688c7fcb04af71b431a29ec9ce433e384c60e387b98",
"sha256:9c0f71789438d703f77b94aad4ea545afaec9a65f10e6cc1bc8b89ce242244bb"
],
"version": "==1.22.0"
"version": "==1.22.1"
},
"gunicorn": {
"hashes": [
@ -427,10 +417,10 @@
},
"h11": {
"hashes": [
"sha256:311dc5478c2568cc07262e0381cdfc5b9c6ba19775905736c87e81ae6662b9fd",
"sha256:9eecfbafc980976dbff26a01dd3487644dd5d00f8038584451fc64a660f7c502"
"sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab",
"sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87"
],
"version": "==0.10.0"
"version": "==0.11.0"
},
"hiredis": {
"hashes": [
@ -483,6 +473,23 @@
],
"version": "==1.1.0"
},
"httptools": {
"hashes": [
"sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be",
"sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d",
"sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce",
"sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2",
"sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6",
"sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f",
"sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009",
"sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce",
"sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a",
"sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c",
"sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4",
"sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"
],
"version": "==0.1.1"
},
"hyperlink": {
"hashes": [
"sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af",
@ -660,28 +667,6 @@
],
"version": "==1.0.0"
},
"multidict": {
"hashes": [
"sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a",
"sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000",
"sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2",
"sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507",
"sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5",
"sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7",
"sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d",
"sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463",
"sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19",
"sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3",
"sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b",
"sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c",
"sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87",
"sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7",
"sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430",
"sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255",
"sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"
],
"version": "==4.7.6"
},
"oauthlib": {
"hashes": [
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
@ -892,6 +877,13 @@
],
"version": "==2.8.1"
},
"python-dotenv": {
"hashes": [
"sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d",
"sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"
],
"version": "==0.14.0"
},
"pytz": {
"hashes": [
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
@ -1029,10 +1021,10 @@
},
"sqlparse": {
"hashes": [
"sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
"sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
"version": "==0.3.1"
"version": "==0.4.1"
},
"structlog": {
"hashes": [
@ -1108,12 +1100,29 @@
"version": "==1.25.10"
},
"uvicorn": {
"extras": [
"standard"
],
"hashes": [
"sha256:9a8f3501d977dedf77a540a0ec3cfadf409fe48eafca2c100d45d843ac62bc7b",
"sha256:fbe9d1b764bc1f4599e1f150a0974feea0fd6380bec889c0d907ebd0a2e896a7"
"sha256:a461e76406088f448f36323f5ac774d50e5a552b6ccb54e4fca8d83ef614a7c2",
"sha256:d06a25caa8dc680ad92eb3ec67363f5281c092059613a1cc0100acba37fc0f45"
],
"index": "pypi",
"version": "==0.12.0"
"version": "==0.12.1"
},
"uvloop": {
"hashes": [
"sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd",
"sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e",
"sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09",
"sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726",
"sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891",
"sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7",
"sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5",
"sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95",
"sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"
],
"version": "==0.14.0"
},
"vine": {
"hashes": [
@ -1122,6 +1131,13 @@
],
"version": "==5.0.0"
},
"watchgod": {
"hashes": [
"sha256:59700dab7445aa8e6067a5b94f37bae90fc367554549b1ed2e9d0f4f38a90d2a",
"sha256:e9cca0ab9c63f17fc85df9fd8bd18156ff00aff04ebe5976cee473f4968c6858"
],
"version": "==0.6"
},
"wcwidth": {
"hashes": [
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
@ -1136,72 +1152,77 @@
],
"version": "==0.57.0"
},
"yarl": {
"websockets": {
"hashes": [
"sha256:04a54f126a0732af75e5edc9addeaa2113e2ca7c6fce8974a63549a70a25e50e",
"sha256:3cc860d72ed989f3b1f3abbd6ecf38e412de722fb38b8f1b1a086315cf0d69c5",
"sha256:5d84cc36981eb5a8533be79d6c43454c8e6a39ee3118ceaadbd3c029ab2ee580",
"sha256:5e447e7f3780f44f890360ea973418025e8c0cdcd7d6a1b221d952600fd945dc",
"sha256:61d3ea3c175fe45f1498af868879c6ffeb989d4143ac542163c45538ba5ec21b",
"sha256:67c5ea0970da882eaf9efcf65b66792557c526f8e55f752194eff8ec722c75c2",
"sha256:6f6898429ec3c4cfbef12907047136fd7b9e81a6ee9f105b45505e633427330a",
"sha256:7ce35944e8e61927a8f4eb78f5bc5d1e6da6d40eadd77e3f79d4e9399e263921",
"sha256:b7c199d2cbaf892ba0f91ed36d12ff41ecd0dde46cbf64ff4bfe997a3ebc925e",
"sha256:c15d71a640fb1f8e98a1423f9c64d7f1f6a3a168f803042eaf3a5b5022fde0c1",
"sha256:c22607421f49c0cb6ff3ed593a49b6a99c6ffdeaaa6c944cdda83c2393c8864d",
"sha256:c604998ab8115db802cc55cb1b91619b2831a6128a62ca7eea577fc8ea4d3131",
"sha256:d088ea9319e49273f25b1c96a3763bf19a882cff774d1792ae6fba34bd40550a",
"sha256:db9eb8307219d7e09b33bcb43287222ef35cbcf1586ba9472b0a4b833666ada1",
"sha256:e31fef4e7b68184545c3d68baec7074532e077bd1906b040ecfba659737df188",
"sha256:e32f0fb443afcfe7f01f95172b66f279938fbc6bdaebe294b0ff6747fb6db020",
"sha256:fcbe419805c9b20db9a51d33b942feddbf6e7fb468cb20686fd7089d4164c12a"
"sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5",
"sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5",
"sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308",
"sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb",
"sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a",
"sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c",
"sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170",
"sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422",
"sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8",
"sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485",
"sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f",
"sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8",
"sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc",
"sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779",
"sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989",
"sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1",
"sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092",
"sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824",
"sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d",
"sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55",
"sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36",
"sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"
],
"version": "==1.6.0"
"version": "==8.1"
},
"zope.interface": {
"hashes": [
"sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b",
"sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5",
"sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd",
"sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c",
"sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7",
"sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5",
"sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34",
"sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e",
"sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086",
"sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda",
"sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286",
"sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826",
"sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d",
"sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee",
"sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd",
"sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9",
"sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e",
"sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc",
"sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe",
"sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a",
"sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578",
"sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a",
"sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813",
"sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d",
"sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19",
"sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425",
"sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975",
"sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e",
"sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8",
"sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08",
"sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5",
"sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0",
"sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11",
"sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f",
"sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345",
"sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9",
"sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58",
"sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc",
"sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6",
"sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8"
"sha256:040f833694496065147e76581c0bf32b229a8b8c5eda120a0293afb008222387",
"sha256:11198b44e4a3d8c7a80cc20bbdd65522258a4d82fe467cd310c9fcce8ffe2ed2",
"sha256:121a9dccfe0c34be9c33b2c28225f0284f9b8e090580ffdff26c38fa16c7ffe1",
"sha256:15f3082575e7e19581a80b866664f843719b647a7f7189c811ba7f9ab3309f83",
"sha256:1d73d8986f948525536956ddd902e8a587a6846ebf4492117db16daba2865ddf",
"sha256:208e82f73b242275b8566ac07a25158e7b21fa2f14e642a7881048430612d1a6",
"sha256:2557833df892558123d791d6ff80ac4a2a0351f69c7421c7d5f0c07db72c8865",
"sha256:25ea6906f9987d42546329d06f9750e69f0ee62307a2e7092955ed0758e64f09",
"sha256:2c867914f7608674a555ac8daf20265644ac7be709e1da7d818089eebdfe544e",
"sha256:2eadac20711a795d3bb7a2bfc87c04091cb5274d9c3281b43088a1227099b662",
"sha256:37999d5ebd5d7bcd32438b725ca3470df05a7de8b1e9c0395bef24296b31ca99",
"sha256:3ae8946d51789779f76e4fa326fd6676d8c19c1c3b4c4c5e9342807185264875",
"sha256:5636cd7e60583b1608044ae4405e91575399430e66a5e1812f4bf30bcc55864e",
"sha256:570e637cb6509998555f7e4af13006d89fad6c09cfc5c4795855385391063e4b",
"sha256:590a40447ff3803c44050ce3c17c3958f11ca028dae3eacdd7b96775184394fa",
"sha256:5aab51b9c1af1b8a84f40aa49ffe1684d41810b18d6c3e94aa50194e0a563f01",
"sha256:5ffe4e0753393bcbcfc9a58133ed3d3a584634cc7cc2e667f8e3e6fbcbb2155d",
"sha256:663982381bd428a275a841009e52983cc69c471a4979ce01344fadbf72cf353d",
"sha256:6d06bf8e24dd6c473c4fbd8e16a83bd2e6d74add6ba25169043deb46d497b211",
"sha256:6e5b9a4bf133cf1887b4a04c21c10ca9f548114f19c83957b2820d5c84254940",
"sha256:70a2aed9615645bbe9d82c0f52bc7e676d2c0f8a63933d68418e0cb307f30536",
"sha256:7750746421c4395e3d2cc3d805919f4f57bb9f2a9a0ccd955566a9341050a1b4",
"sha256:7fc8708bc996e50fc7a9a2ad394e1f015348e389da26789fa6916630237143d7",
"sha256:91abd2f080065a7c007540f6bbd93ef7bdbbffa6df4a4cfab3892d8623b83c98",
"sha256:988f8b2281f3d95c66c01bdb141cefef1cc97db0d473c25c3fe2927ef00293b9",
"sha256:9f56121d8a676802044584e6cc41250bbcde069d8adf725b9b817a6b0fd87f09",
"sha256:a0f51536ce6e817a7aa25b0dca8b62feb210d4dc22cabfe8d1a92d47979372cd",
"sha256:a1cdd7390d7f66ddcebf545203ca3728c4890d605f9f2697bc8e31437906e8e7",
"sha256:b10eb4d0a77609679bf5f23708e20b1cd461a1643bd8ea42b1ca4149b1a5406c",
"sha256:b274ac8e511b55ffb62e8292316bd2baa80c10e9fe811b1aa5ce81da6b6697d8",
"sha256:c75b502af2c83fcfa2ee9c2257c1ba5806634a91a50db6129ff70e67c42c7e7b",
"sha256:c9c8e53a5472b77f6a391b515c771105011f4b40740ce53af8428d1c8ca20004",
"sha256:d867998a56c5133b9d31992beb699892e33b72150a8bf40f86cb52b8c606c83f",
"sha256:eb566cab630ec176b2d6115ed08b2cf4d921b47caa7f02cca1b4a9525223ee94",
"sha256:f61e6b95b414431ffe9dc460928fe9f351095fde074e2c2f5c6dda7b67a2192d",
"sha256:f718675fd071bcce4f7cbf9250cbaaf64e2e91ef1b0b32a1af596e7412647556",
"sha256:f9d4bfbd015e4b80dbad11c97049975f94592a6a0440e903ee647309f6252a1f",
"sha256:fae50fc12a5e8541f6f1cc4ed744ca8f76a9543876cf63f618fb0e6aca8f8375",
"sha256:fcf9c8edda7f7b2fd78069e97f4197815df5e871ec47b0f22580d330c6dec561",
"sha256:fdedce3bc5360bd29d4bb90396e8d4d3c09af49bc0383909fe84c7233c5ee675"
],
"version": "==5.1.0"
"version": "==5.1.2"
}
},
"develop": {
@ -1250,18 +1271,17 @@
},
"black": {
"hashes": [
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
"sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
],
"index": "pypi",
"version": "==19.10b0"
"version": "==20.8b1"
},
"bump2version": {
"hashes": [
"sha256:477f0e18a0d58e50bb3dbc9af7fcda464fd0ebfc7a6151d8888602d7153171a0",
"sha256:cd4f3a231305e405ed8944d8ff35bd742d9bc740ad62f483bd0ca21ce7131984"
"sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410",
"sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"
],
"version": "==1.0.0"
"version": "==1.0.1"
},
"bumpversion": {
"hashes": [
@ -1271,20 +1291,6 @@
"index": "pypi",
"version": "==0.6.0"
},
"certifi": {
"hashes": [
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
],
"version": "==2020.6.20"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
@ -1342,11 +1348,11 @@
},
"django": {
"hashes": [
"sha256:59c8125ca873ed3bdae9c12b146fbbd6ed8d0f743e4cf5f5817af50c51f1fc2f",
"sha256:b5fbb818e751f660fa2d576d9f40c34a4c615c8b48dd383f5216e609f383371f"
"sha256:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc",
"sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4"
],
"index": "pypi",
"version": "==3.1.1"
"version": "==3.1.2"
},
"django-debug-toolbar": {
"hashes": [
@ -1356,14 +1362,6 @@
"index": "pypi",
"version": "==3.1.1"
},
"docker": {
"hashes": [
"sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828",
"sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2"
],
"index": "pypi",
"version": "==4.3.1"
},
"dodgy": {
"hashes": [
"sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a",
@ -1373,10 +1371,10 @@
},
"flake8": {
"hashes": [
"sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c",
"sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"
"sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
"sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
],
"version": "==3.8.3"
"version": "==3.8.4"
},
"flake8-polyfill": {
"hashes": [
@ -1394,17 +1392,10 @@
},
"gitpython": {
"hashes": [
"sha256:080bf8e2cf1a2b907634761c2eaefbe83b69930c94c66ad11b65a8252959f912",
"sha256:1858f4fd089abe92ae465f01d5aaaf55e937eca565fb2c1fce35a51b5f85c910"
"sha256:138016d519bf4dd55b22c682c904ed2fd0235c3612b2f8f65ce218ff358deed8",
"sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"
],
"version": "==3.1.8"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
"version": "==3.1.9"
},
"iniconfig": {
"hashes": [
@ -1453,6 +1444,13 @@
],
"version": "==0.6.1"
},
"mypy-extensions": {
"hashes": [
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
],
"version": "==0.4.3"
},
"packaging": {
"hashes": [
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
@ -1568,11 +1566,11 @@
},
"pytest": {
"hashes": [
"sha256:1cd09785c0a50f9af72220dd12aa78cfa49cbffc356c61eab009ca189e018a33",
"sha256:d010e24666435b39a4cf48740b039885642b6c273a3f77be3e7e03554d2806b7"
"sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9",
"sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92"
],
"index": "pypi",
"version": "==6.1.0"
"version": "==6.1.1"
},
"pytest-django": {
"hashes": [
@ -1610,12 +1608,17 @@
"hashes": [
"sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef",
"sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c",
"sha256:3d20024a70b97b4f9546696cbf2fd30bae5f42229fbddf8661261b1eaff0deb7",
"sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b",
"sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c",
"sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63",
"sha256:49f23ebd5ac073765ecbcf046edc10d63dcab2f4ae2bce160982cb30df0c0302",
"sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc",
"sha256:5d892a4f1c999834eaa3c32bc9e8b976c5825116cde553928c4c8e7e48ebda67",
"sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be",
"sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab",
"sha256:816064fc915796ea1f26966163f6845de5af78923dfcecf6551e095f00983650",
"sha256:84cada8effefe9a9f53f9b0d2ba9b7b6f5edf8d2155f9fdbe34616e06ececf81",
"sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19",
"sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637",
"sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc",
@ -1623,6 +1626,7 @@
"sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d",
"sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b",
"sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100",
"sha256:c9443124c67b1515e4fe0bb0aa18df640965e1030f468a2a5dc2589b26d130ad",
"sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3",
"sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121",
"sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b",
@ -1632,13 +1636,6 @@
],
"version": "==2020.9.27"
},
"requests": {
"hashes": [
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
],
"version": "==2.24.0"
},
"requirements-detector": {
"hashes": [
"sha256:0d1e13e61ed243f9c3c86e6cbb19980bcb3a0e0619cde2ec1f3af70fdbee6f7b"
@ -1682,10 +1679,10 @@
},
"sqlparse": {
"hashes": [
"sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
"sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
"version": "==0.3.1"
"version": "==0.4.1"
},
"stevedore": {
"hashes": [
@ -1727,6 +1724,14 @@
],
"version": "==1.4.1"
},
"typing-extensions": {
"hashes": [
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
],
"version": "==3.7.4.3"
},
"urllib3": {
"extras": [
"secure"
@ -1739,13 +1744,6 @@
"markers": null,
"version": "==1.25.10"
},
"websocket-client": {
"hashes": [
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
"sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"
],
"version": "==0.57.0"
},
"wrapt": {
"hashes": [
"sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"

View File

@ -89,7 +89,7 @@ stages:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: npm install -g pyright
script: npm install -g pyright@1.1.75
- task: CmdLine@2
inputs:
script: |
@ -219,7 +219,8 @@ stages:
inputs:
script: |
cd passbook/static/static
yarn
npm i
npm run build
- task: CmdLine@2
displayName: Run full test suite
inputs:
@ -332,19 +333,3 @@ stages:
repository: 'beryju/passbook-static'
command: 'push'
tags: "gh-${{ variables.branchName }}"
- stage: Deploy
jobs:
- job: deploy_dev
pool:
vmImage: 'ubuntu-latest'
steps:
- task: HelmDeploy@0
inputs:
connectionType: 'Kubernetes Service Connection'
kubernetesServiceConnection: 'k8s-beryjuorg-prd'
namespace: 'passbook-dev'
command: 'upgrade'
chartType: 'FilePath'
chartPath: 'helm/'
releaseName: 'passbook-dev'
recreate: true

View File

@ -3,7 +3,7 @@ version: '3.2'
services:
postgresql:
image: postgres
image: postgres:12
volumes:
- database:/var/lib/postgresql/data
networks:
@ -23,7 +23,7 @@ services:
labels:
- traefik.enable=false
server:
image: beryju/passbook:${PASSBOOK_TAG:-0.10.8-stable}
image: beryju/passbook:${PASSBOOK_TAG:-0.11.0-stable}
command: server
environment:
PASSBOOK_REDIS__HOST: redis
@ -40,7 +40,7 @@ services:
env_file:
- .env
worker:
image: beryju/passbook:${PASSBOOK_TAG:-0.10.8-stable}
image: beryju/passbook:${PASSBOOK_TAG:-0.11.0-stable}
command: worker
networks:
- internal
@ -50,10 +50,13 @@ services:
PASSBOOK_REDIS__HOST: redis
PASSBOOK_POSTGRESQL__HOST: postgresql
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
volumes:
- ./backups:/backups
- /var/run/docker.socket:/var/run/docker.socket
env_file:
- .env
static:
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.8-stable}
image: beryju/passbook-static:${PASSBOOK_TAG:-0.11.0-stable}
networks:
- internal
labels:

View File

@ -2,20 +2,25 @@
The User object has the following attributes:
- `username`: User's username.
- `email` User's email.
- `name` User's display mame.
- `is_staff` Boolean field if user is staff.
- `is_active` Boolean field if user is active.
- `date_joined` Date user joined/was created.
- `password_change_date` Date password was last changed.
- `attributes` Dynamic attributes.
- `username`: User's username.
- `email` User's email.
- `name` User's display name.
- `is_staff` Boolean field if user is staff.
- `is_active` Boolean field if user is active.
- `date_joined` Date user joined/was created.
- `password_change_date` Date password was last changed.
- `attributes` Dynamic attributes.
- `pb_groups` This is a queryset of all the user's groups.
You can do additional filtering like `user.pb_groups.filter(name__startswith='test')`, see [here](https://docs.djangoproject.com/en/3.1/ref/models/querysets/#id4)
To get the name of all groups, you can do `[group.name for group in user.pb_groups.all()]`
## Examples
List all the User's group names:
```python
for group in user.groups.all():
for group in user.pb_groups.all():
yield group.name
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 450 KiB

View File

@ -13,7 +13,7 @@ Download the latest `docker-compose.yml` from [here](https://raw.githubuserconte
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env`
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.8-stable >> .env`
To optionally deploy a different version run `echo PASSBOOK_TAG=0.11.0-stable >> .env`
If this is a fresh passbook install run the following commands to generate a password:

View File

@ -4,14 +4,14 @@ For a mid to high-load installation, Kubernetes is recommended. passbook is inst
This installation automatically applies database migrations on startup. After the installation is done, you can use `pbadmin` as username and password.
```
```yaml
###################################
# Values directly affecting passbook
###################################
image:
name: beryju/passbook
name_static: beryju/passbook-static
tag: 0.10.8-stable
tag: 0.11.0-stable
nameOverride: ""
@ -35,8 +35,21 @@ config:
# access_key: access-key
# secret_key: secret-key
# bucket: s3-bucket
# region: eu-central-1
# host: s3-host
ingress:
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
path: /
hosts:
- passbook.k8s.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - passbook.k8s.local
###################################
# Values controlling dependencies
###################################
@ -57,16 +70,4 @@ redis:
enabled: false
# https://stackoverflow.com/a/59189742
disableCommands: []
ingress:
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
path: /
hosts:
- passbook.k8s.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - passbook.k8s.local
```

View File

@ -0,0 +1,42 @@
# passbook behind a reverse-proxy
If you want to access passbook behind a reverse-proxy, use a config like this. It is important that Websocket is enabled, so that Outposts can connect.
```
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
# Server config
listen 80;
server_name sso.domain.tld;
# 301 to SSL
location / {
return 301 https://$host$request_uri;
}
}
server {
# Server config
listen 443 ssl http2;
server_name sso.domain.tld;
# SSL Certs
ssl_certificate /etc/letsencrypt/live/domain.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.tld/privkey.pem;
# Proxy site
location / {
proxy_pass https://<hostname of your passbook server>;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
```

View File

@ -16,7 +16,7 @@ The following placeholders will be used:
## passbook Setup
Because Tautulli requires valid HTTP Basic credentials, you must save your HTTP Basic Credentials in passbook. The recommended way to do this, is to create a Group, called for example "Tautulli Users". For this group, add the following attributes:
Because Tautulli requires valid HTTP Basic credentials, you must save your HTTP Basic Credentials in passbook. The recommended way to do this is to create a Group. Name the group "Tautulli Users", for example. For this group, add the following attributes:
```yaml
tautulli_user: username
@ -31,13 +31,13 @@ Create an application in passbook. Create a Proxy provider with the following pa
If Tautulli is running in docker, and you're deploying the passbook proxy on the same host, set the value to `http://tautulli:3579`, where tautulli is the name of your container.
If Tautulli is running on a different server than where you are deploying the passbook proxy, set the value to `http://tautulli.company:3579`.
If Tautulli is running on a different server to where you are deploying the passbook proxy, set the value to `http://tautulli.company:3579`.
- External host
Set this to the external URL you will be accessing Tautulli from.
Enable the `Set HTTP-Basic Authentication` option. Set and `HTTP-Basic Username` and `HTTP-Basic Password` to `tautulli_user` and `tautulli_password` respectively. These values can be chosen freely, `tautulli_` is just used a prefix for clarity.
Enable the `Set HTTP-Basic Authentication` option. Set and `HTTP-Basic Username` and `HTTP-Basic Password` to `tautulli_user` and `tautulli_password` respectively. These values can be chosen freely, `tautulli_` is just used as a prefix for clarity.
## Tautulli Setup
@ -47,4 +47,4 @@ In Tautulli, navigate to Settings and enable the "Show Advanced" option. Navigat
Save the settings, and restart Tautulli if prompted.
Afterwards, you need to deploy an Outpost in front of Tautulli, just like descried [here](../sonarr/index.md)
Afterwards, you need to deploy an Outpost in front of Tautulli, as descried [here](../sonarr/index.md)

View File

@ -60,6 +60,8 @@ Under *Providers*, create an OAuth2/OpenID Provider with these settings:
Create an application which uses this provider. Optionally apply access restrictions to the application.
Set the Launch URL to `https://vcenter.company/ui/login/oauth2`. This will skip vCenter's User Prompt and directly log you in.
## vCenter Setup
Login as local Administrator account (most likely ends with vsphere.local). Using the Menu in the Navigation bar, navigate to *Administration -> Single Sing-on -> Configuration*.

View File

@ -0,0 +1,111 @@
# Backup and restore
!!! warning
Local backups are only supported for docker-compose installs. If you want to backup a Kubernetes instance locally, use an S3-compatible server such as [minio](https://min.io/)
### Backup
Local backups can be created by running the following command in your passbook installation directory
```
docker-compose run --rm worker backup
```
This will dump the current database into the `./backups` folder. By defaults, the last 10 Backups are kept.
To schedule these backups, use the following snippet in a crontab
```
0 0 * * * bash -c "cd <passbook install location> && docker-compose run --rm worker backup" >/dev/null
```
!!! notice
passbook does support automatic backups on a schedule, however this is currently not recommended, as there is no way to monitor these scheduled tasks.
### Restore
Run this command in your passbook installation directory
```
docker-compose run --rm worker restore
```
This will prompt you to restore from your last backup. If you want to restore from a specific file, use the `-i` flag with the filename:
```
docker-compose run --rm worker restore -i default-2020-10-03-115557.psql
```
After you've restored the backup, it is recommended to restart all services with `docker-compose restart`.
### S3 Configuration
!!! notice
To trigger backups with S3 enabled, use the same commands as above.
#### S3 Preparation
passbook expects the bucket you select to already exist. The IAM User given to passbook should have the following permissions
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObjectAcl",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject",
"s3:PutObjectAcl"
],
"Principal": {
"AWS": "arn:aws:iam::example-AWS-account-ID:user/example-user-name"
},
"Resource": [
"arn:aws:s3:::example-bucket-name/*",
"arn:aws:s3:::example-bucket-name"
]
}
]
}
```
#### docker-compose
Set the following values in your `.env` file.
```
PASSBOOK_POSTGRESQL__S3_BACKUP__ACCESS_KEY=
PASSBOOK_POSTGRESQL__S3_BACKUP__SECRET_KEY=
PASSBOOK_POSTGRESQL__S3_BACKUP__BUCKET=
PASSBOOK_POSTGRESQL__S3_BACKUP__REGION=
```
If you want to backup to an S3-compatible server, like [minio](https://min.io/), use this setting:
```
PASSBOOK_POSTGRESQL__S3_BACKUP__HOST=http://play.min.io
```
#### Kubernetes
Simply enable these options in your values.yaml file
```yaml
# Enable Database Backups to S3
backup:
access_key: access-key
secret_key: secret-key
bucket: s3-bucket
region: eu-central-1
host: s3-host
```
Afterwards, run a `helm upgrade` to update the ConfigMap. Because passbook-scheduled backups are not recommended currently, a Kubernetes CronJob is created that runs the backup daily.

View File

@ -5,7 +5,7 @@ To deploy an outpost with docker-compose, use this snippet in your docker-compo
You can also run the outpost in a separate docker-compose project, you just have to ensure that the outpost container can reach your application container.
```yaml
version: 3.5
version: '3.5'
services:
passbook_proxy:

View File

@ -104,7 +104,8 @@ class TestFlowsEnroll(SeleniumTestCase):
self.wait_for_url(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text, "foo",
self.driver.find_element(By.ID, "user-settings").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
@ -208,7 +209,8 @@ class TestFlowsEnroll(SeleniumTestCase):
self.driver.find_element(By.ID, "user-settings").click()
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text, "foo",
self.driver.find_element(By.ID, "user-settings").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"

View File

@ -21,5 +21,6 @@ class TestFlowsLogin(SeleniumTestCase):
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.assertEqual(
self.driver.find_element(By.ID, "user-settings").text, USER().username,
self.driver.find_element(By.ID, "user-settings").text,
USER().username,
)

View File

@ -48,7 +48,8 @@ class TestFlowsOTP(SeleniumTestCase):
self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER)
self.wait_for_url(self.url("passbook_core:overview"))
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text, USER().username,
self.driver.find_element(By.ID, "user-settings").text,
USER().username,
)
def test_otp_totp_setup(self):
@ -62,7 +63,8 @@ class TestFlowsOTP(SeleniumTestCase):
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.assertEqual(
self.driver.find_element(By.ID, "user-settings").text, USER().username,
self.driver.find_element(By.ID, "user-settings").text,
USER().username,
)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
@ -107,7 +109,8 @@ class TestFlowsOTP(SeleniumTestCase):
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.assertEqual(
self.driver.find_element(By.ID, "user-settings").text, USER().username,
self.driver.find_element(By.ID, "user-settings").text,
USER().username,
)
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()

View File

@ -77,7 +77,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
authorization_flow=authorization_flow,
)
Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
name="Grafana",
slug="grafana",
provider=provider,
)
self.driver.get("http://localhost:3000")
@ -129,7 +131,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
authorization_flow=authorization_flow,
)
app = Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
name="Grafana",
slug="grafana",
provider=provider,
)
self.driver.get("http://localhost:3000")
@ -143,13 +147,17 @@ class TestProviderOAuth2Github(SeleniumTestCase):
sleep(1)
self.assertEqual(
app.name, self.driver.find_element(By.ID, "application-name").text,
app.name,
self.driver.find_element(By.ID, "application-name").text,
)
self.assertEqual(
"GitHub Compatibility: Access you Email addresses",
self.driver.find_element(By.ID, "scope-user:email").text,
)
self.driver.find_element(By.CSS_SELECTOR, ("[type=submit]"),).click()
self.driver.find_element(
By.CSS_SELECTOR,
("[type=submit]"),
).click()
self.wait_for_url("http://localhost:3000/?orgId=1")
self.driver.get("http://localhost:3000/profile")
@ -192,7 +200,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
authorization_flow=authorization_flow,
)
app = Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
name="Grafana",
slug="grafana",
provider=provider,
)
negative_policy = ExpressionPolicy.objects.create(

View File

@ -104,7 +104,9 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
)
provider.save()
Application.objects.create(
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
name="Grafana",
slug=APPLICATION_SLUG,
provider=provider,
)
self.driver.get("http://localhost:3000")
@ -144,7 +146,9 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
)
provider.save()
Application.objects.create(
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
name="Grafana",
slug=APPLICATION_SLUG,
provider=provider,
)
self.driver.get("http://localhost:3000")
@ -203,7 +207,9 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
)
provider.save()
Application.objects.create(
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
name="Grafana",
slug=APPLICATION_SLUG,
provider=provider,
)
self.driver.get("http://localhost:3000")
@ -270,7 +276,9 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
)
provider.save()
app = Application.objects.create(
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
name="Grafana",
slug=APPLICATION_SLUG,
provider=provider,
)
self.driver.get("http://localhost:3000")
@ -282,7 +290,8 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.assertEqual(
app.name, self.driver.find_element(By.ID, "application-name").text,
app.name,
self.driver.find_element(By.ID, "application-name").text,
)
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
@ -340,7 +349,9 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
)
provider.save()
app = Application.objects.create(
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
name="Grafana",
slug=APPLICATION_SLUG,
provider=provider,
)
negative_policy = ExpressionPolicy.objects.create(

View File

@ -100,7 +100,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
Application.objects.create(
name=self.application_slug, slug=self.application_slug, provider=provider,
name=self.application_slug,
slug=self.application_slug,
provider=provider,
)
self.container = self.setup_client()
@ -141,7 +143,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
Application.objects.create(
name=self.application_slug, slug=self.application_slug, provider=provider,
name=self.application_slug,
slug=self.application_slug,
provider=provider,
)
self.container = self.setup_client()
@ -189,7 +193,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
app = Application.objects.create(
name=self.application_slug, slug=self.application_slug, provider=provider,
name=self.application_slug,
slug=self.application_slug,
provider=provider,
)
self.container = self.setup_client()
@ -202,7 +208,8 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.assertEqual(
app.name, self.driver.find_element(By.ID, "application-name").text,
app.name,
self.driver.find_element(By.ID, "application-name").text,
)
self.wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
@ -246,7 +253,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
)
provider.save()
app = Application.objects.create(
name=self.application_slug, slug=self.application_slug, provider=provider,
name=self.application_slug,
slug=self.application_slug,
provider=provider,
)
negative_policy = ExpressionPolicy.objects.create(

View File

@ -39,9 +39,8 @@ class TestProviderProxy(SeleniumTestCase):
def start_proxy(self, outpost: Outpost) -> Container:
"""Start proxy container based on outpost created"""
client: DockerClient = from_env()
client.images.pull("beryju/oidc-test-client")
container = client.containers.run(
image="beryju/passbook-proxy:latest",
image=f"beryju/passbook-proxy:{__version__}",
detach=True,
network_mode="host",
auto_remove=True,
@ -112,9 +111,8 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
def start_proxy(self, outpost: Outpost) -> Container:
"""Start proxy container based on outpost created"""
client: DockerClient = from_env()
client.images.pull("beryju/oidc-test-client")
container = client.containers.run(
image="beryju/passbook-proxy:latest",
image=f"beryju/passbook-proxy:{__version__}",
detach=True,
network_mode="host",
auto_remove=True,

View File

@ -84,7 +84,9 @@ class TestProviderSAML(SeleniumTestCase):
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
Application.objects.create(
name="SAML", slug="passbook-saml", provider=provider,
name="SAML",
slug="passbook-saml",
provider=provider,
)
self.container = self.setup_client(provider)
self.driver.get("http://localhost:9009")
@ -121,7 +123,9 @@ class TestProviderSAML(SeleniumTestCase):
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
app = Application.objects.create(
name="SAML", slug="passbook-saml", provider=provider,
name="SAML",
slug="passbook-saml",
provider=provider,
)
self.container = self.setup_client(provider)
self.driver.get("http://localhost:9009")
@ -131,7 +135,8 @@ class TestProviderSAML(SeleniumTestCase):
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.assertEqual(
app.name, self.driver.find_element(By.ID, "application-name").text,
app.name,
self.driver.find_element(By.ID, "application-name").text,
)
sleep(1)
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
@ -163,7 +168,9 @@ class TestProviderSAML(SeleniumTestCase):
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
Application.objects.create(
name="SAML", slug="passbook-saml", provider=provider,
name="SAML",
slug="passbook-saml",
provider=provider,
)
self.container = self.setup_client(provider)
self.driver.get(
@ -209,7 +216,9 @@ class TestProviderSAML(SeleniumTestCase):
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
app = Application.objects.create(
name="SAML", slug="passbook-saml", provider=provider,
name="SAML",
slug="passbook-saml",
provider=provider,
)
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
self.container = self.setup_client(provider)

View File

@ -144,13 +144,15 @@ class TestSourceOAuth2(SeleniumTestCase):
self.driver.get(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text, "foo",
self.driver.find_element(By.ID, "user-settings").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin",
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"admin",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
@ -225,13 +227,15 @@ class TestSourceOAuth2(SeleniumTestCase):
self.driver.get(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text, "foo",
self.driver.find_element(By.ID, "user-settings").text,
"foo",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin",
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"admin",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
@ -317,7 +321,8 @@ class TestSourceOAuth1(SeleniumTestCase):
self.driver.get(self.url("passbook_core:user-settings"))
self.assertEqual(
self.driver.find_element(By.ID, "user-settings").text, "example-user",
self.driver.find_element(By.ID, "user-settings").text,
"example-user",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_username").get_attribute("value"),

View File

@ -98,7 +98,9 @@ class TestSourceSAML(SeleniumTestCase):
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
name="test-idp-cert",
certificate_data=IDP_CERT,
key_data=IDP_KEY,
)
SAMLSource.objects.create(
@ -145,7 +147,9 @@ class TestSourceSAML(SeleniumTestCase):
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
name="test-idp-cert",
certificate_data=IDP_CERT,
key_data=IDP_KEY,
)
SAMLSource.objects.create(
@ -194,7 +198,9 @@ class TestSourceSAML(SeleniumTestCase):
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
keypair = CertificateKeyPair.objects.create(
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
name="test-idp-cert",
certificate_data=IDP_CERT,
key_data=IDP_KEY,
)
SAMLSource.objects.create(

View File

@ -1,8 +1,8 @@
apiVersion: v2
appVersion: "0.10.8-stable"
appVersion: "0.11.0-stable"
description: A Helm chart for passbook.
name: passbook
version: "0.10.8-stable"
version: "0.11.0-stable"
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
dependencies:
- name: postgresql

View File

@ -7,10 +7,11 @@ data:
POSTGRESQL__NAME: "{{ .Values.postgresql.postgresqlDatabase }}"
POSTGRESQL__USER: "{{ .Values.postgresql.postgresqlUsername }}"
{{- if .Values.backup }}
POSTGRESQL__BACKUP__ACCESS_KEY: "{{ .Values.backup.access_key }}"
POSTGRESQL__BACKUP__SECRET_KEY: "{{ .Values.backup.secret_key }}"
POSTGRESQL__BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
POSTGRESQL__BACKUP__HOST: "{{ .Values.backup.host }}"
POSTGRESQL__S3_BACKUP__ACCESS_KEY: "{{ .Values.backup.access_key }}"
POSTGRESQL__S3_BACKUP__SECRET_KEY: "{{ .Values.backup.secret_key }}"
POSTGRESQL__S3_BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
POSTGRESQL__S3_BACKUP__REGION: "{{ .Values.backup.region }}"
POSTGRESQL__S3_BACKUP__HOST: "{{ .Values.backup.host }}"
{{- end}}
REDIS__HOST: "{{ .Release.Name }}-redis-master"
ERROR_REPORTING__ENABLED: "{{ .Values.config.error_reporting.enabled }}"

View File

@ -0,0 +1,42 @@
{{- if .Values.backup }}
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: {{ include "passbook.fullname" . }}-backup
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
schedule: "0 0 * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
args: [server]
envFrom:
- configMapRef:
name: {{ include "passbook.fullname" . }}-config
prefix: PASSBOOK_
env:
- name: PASSBOOK_SECRET_KEY
valueFrom:
secretKeyRef:
name: "{{ include "passbook.fullname" . }}-secret-key"
key: "secret_key"
- name: PASSBOOK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-redis"
key: "redis-password"
- name: PASSBOOK_POSTGRESQL__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-postgresql"
key: "postgresql-password"
{{- end}}

View File

@ -4,7 +4,7 @@
image:
name: beryju/passbook
name_static: beryju/passbook-static
tag: 0.10.8-stable
tag: 0.11.0-stable
nameOverride: ""
@ -28,8 +28,21 @@ config:
# access_key: access-key
# secret_key: secret-key
# bucket: s3-bucket
# region: eu-central-1
# host: s3-host
ingress:
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
path: /
hosts:
- passbook.k8s.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - passbook.k8s.local
###################################
# Values controlling dependencies
###################################
@ -50,15 +63,3 @@ redis:
enabled: false
# https://stackoverflow.com/a/59189742
disableCommands: []
ingress:
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
path: /
hosts:
- passbook.k8s.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - passbook.k8s.local

View File

@ -1,6 +1,6 @@
#!/bin/bash -e
python -m lifecycle.wait_for_db
printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@"
printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@" > /dev/stderr
if [[ "$1" == "server" ]]; then
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
elif [[ "$1" == "worker" ]]; then
@ -9,6 +9,12 @@ elif [[ "$1" == "migrate" ]]; then
# Run system migrations first, run normal migrations after
python -m lifecycle.migrate
python -m manage migrate
elif [[ "$1" == "backup" ]]; then
python -m manage dbbackup --clean
elif [[ "$1" == "restore" ]]; then
python -m manage dbrestore ${@:2}
elif [[ "$1" == "bash" ]]; then
/bin/bash
else
python -m manage "$@"
fi

View File

@ -8,23 +8,24 @@ nav:
- Installation:
- docker-compose: installation/docker-compose.md
- Kubernetes: installation/kubernetes.md
- Reverse Proxy: installation/reverse-proxy.md
- Flows:
Overview: flow/flows.md
Examples: flow/examples/examples.md
- Stages:
- Captcha Stage: flow/stages/captcha/index.md
- Dummy Stage: flow/stages/dummy/index.md
- Email Stage: flow/stages/email/index.md
- Identification Stage: flow/stages/identification/index.md
- Invitation Stage: flow/stages/invitation/index.md
- OTP Stage: flow/stages/otp/index.md
- Password Stage: flow/stages/password/index.md
- Prompt Stage: flow/stages/prompt/index.md
- Prompt Stage Validation: flow/stages/prompt/validation.md
- User Delete Stage: flow/stages/user_delete.md
- User Login Stage: flow/stages/user_login.md
- User Logout Stage: flow/stages/user_logout.md
- User Write Stage: flow/stages/user_write.md
- Captcha Stage: flow/stages/captcha/index.md
- Dummy Stage: flow/stages/dummy/index.md
- Email Stage: flow/stages/email/index.md
- Identification Stage: flow/stages/identification/index.md
- Invitation Stage: flow/stages/invitation/index.md
- OTP Stage: flow/stages/otp/index.md
- Password Stage: flow/stages/password/index.md
- Prompt Stage: flow/stages/prompt/index.md
- Prompt Stage Validation: flow/stages/prompt/validation.md
- User Delete Stage: flow/stages/user_delete.md
- User Login Stage: flow/stages/user_login.md
- User Logout Stage: flow/stages/user_logout.md
- User Write Stage: flow/stages/user_write.md
- Sources: sources.md
- Providers:
- OAuth2: providers/oauth2.md
@ -57,6 +58,8 @@ nav:
- Ubuntu Landscape: integrations/services/ubuntu-landscape/index.md
- Sonarr: integrations/services/sonarr/index.md
- Tautulli: integrations/services/tautulli/index.md
- Maintenance:
- Backups: maintenance/backups/index.md
- Upgrading:
- to 0.9: upgrading/to-0.9.md
- to 0.10: upgrading/to-0.10.md

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = "0.10.8-stable"
__version__ = "0.11.0-stable"

View File

View File

@ -0,0 +1,80 @@
"""passbook administration overview"""
from django.core.cache import cache
from django.http import response
from drf_yasg.utils import swagger_auto_schema
from rest_framework.fields import SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet
from passbook import __version__
from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from passbook.core.models import Provider
from passbook.policies.models import Policy
from passbook.root.celery import CELERY_APP
class AdministrationOverviewSerializer(Serializer):
"""Overview View"""
version = SerializerMethodField()
version_latest = SerializerMethodField()
worker_count = SerializerMethodField()
providers_without_application = SerializerMethodField()
policies_without_binding = SerializerMethodField()
cached_policies = SerializerMethodField()
cached_flows = SerializerMethodField()
def get_version(self, _) -> str:
"""Get current version"""
return __version__
def get_version_latest(self, _) -> str:
"""Get latest version from cache"""
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache:
update_latest_version.delay()
return __version__
return version_in_cache
def get_worker_count(self, _) -> int:
"""Ping workers"""
return len(CELERY_APP.control.ping(timeout=0.5))
def get_providers_without_application(self, _) -> int:
"""Count of providers without application"""
return len(Provider.objects.filter(application=None))
def get_policies_without_binding(self, _) -> int:
"""Count of policies not bound or use in prompt stages"""
return len(
Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
)
def get_cached_policies(self, _) -> int:
"""Get cached policy count"""
return len(cache.keys("policy_*"))
def get_cached_flows(self, _) -> int:
"""Get cached flow count"""
return len(cache.keys("flow_*"))
def create(self, request: Request) -> response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class AdministrationOverviewViewSet(ViewSet):
"""Return single instance of AdministrationOverviewSerializer"""
permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: AdministrationOverviewSerializer(many=True)})
def list(self, request: Request) -> Response:
"""Return single instance of AdministrationOverviewSerializer"""
serializer = AdministrationOverviewSerializer(True)
return Response(serializer.data)

View File

@ -0,0 +1,80 @@
"""passbook administration overview"""
import time
from collections import Counter
from datetime import timedelta
from typing import Dict, List
from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour
from django.http import response
from django.utils.timezone import now
from drf_yasg.utils import swagger_auto_schema
from rest_framework.fields import SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet
from passbook.audit.models import Event, EventAction
class AdministrationMetricsSerializer(Serializer):
"""Overview View"""
logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField()
def get_events_per_1h(self, action: str) -> List[Dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(days=1)
result = (
Event.objects.filter(action=action, created__gte=date_from)
.annotate(
age=ExpressionWrapper(
now() - F("created"), output_field=DurationField()
)
)
.annotate(age_hours=ExtractHour("age"))
.values("age_hours")
.annotate(count=Count("pk"))
.order_by("age_hours")
)
data = Counter({d["age_hours"]: d["count"] for d in result})
results = []
_now = now()
for hour in range(0, -24, -1):
results.append(
{
"x": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
"y": data[hour * -1],
}
)
return results
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
return self.get_events_per_1h(EventAction.LOGIN)
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
return self.get_events_per_1h(EventAction.LOGIN_FAILED)
def create(self, request: Request) -> response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class AdministrationMetricsViewSet(ViewSet):
"""Return single instance of AdministrationMetricsSerializer"""
permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)})
def list(self, request: Request) -> Response:
"""Return single instance of AdministrationMetricsSerializer"""
serializer = AdministrationMetricsSerializer(True)
return Response(serializer.data)

View File

@ -1,9 +1,35 @@
"""YAML fields"""
"""Additional fields"""
import yaml
from django import forms
from django.utils.datastructures import MultiValueDict
from django.utils.translation import gettext_lazy as _
class ArrayFieldSelectMultiple(forms.SelectMultiple):
"""This is a Form Widget for use with a Postgres ArrayField. It implements
a multi-select interface that can be given a set of `choices`.
You can provide a `delimiter` keyword argument to specify the delimeter used.
https://gist.github.com/stephane/00e73c0002de52b1c601"""
def __init__(self, *args, **kwargs):
# Accept a `delimiter` argument, and grab it (defaulting to a comma)
self.delimiter = kwargs.pop("delimiter", ",")
super().__init__(*args, **kwargs)
def value_from_datadict(self, data, files, name):
if isinstance(data, MultiValueDict):
# Normally, we'd want a list here, which is what we get from the
# SelectMultiple superclass, but the SimpleArrayField expects to
# get a delimited string, so we're doing a little extra work.
return self.delimiter.join(data.getlist(name))
return data.get(name)
def get_context(self, name, value, attrs):
return super().get_context(name, value.split(self.delimiter), attrs)
class CodeMirrorWidget(forms.Textarea):
"""Custom Textarea-based Widget that triggers a CodeMirror editor"""
@ -49,7 +75,9 @@ class YAMLField(forms.JSONField):
converted = yaml.safe_load(value)
except yaml.YAMLError:
raise forms.ValidationError(
self.error_messages["invalid"], code="invalid", params={"value": value},
self.error_messages["invalid"],
code="invalid",
params={"value": value},
)
if isinstance(converted, str):
return YAMLString(converted)

View File

@ -18,6 +18,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
@ -62,18 +63,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-applications pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Applications.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any application." %}
{% else %}
{% trans 'Currently no applications exist. Click the button below to create one.' %}
{% endif %}
</div>
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>

View File

@ -25,7 +25,7 @@
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:overview' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:overview' %}">
{% trans 'System Status' %}
{% trans 'Overview' %}
</a>
</li>
<li class="pf-c-nav__item">

View File

@ -18,6 +18,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
@ -64,18 +65,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-key pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Certificates.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any certificates." %}
{% else %}
{% trans 'Currently no certificates exist. Click the button below to create one.' %}
{% endif %}
</div>
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>

View File

@ -18,6 +18,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<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:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-secondary" type="button">{% trans 'Import' %}</a>
@ -69,18 +70,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-process-automation pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Flows.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any flows." %}
{% else %}
{% trans 'Currently no flows exist. Click the button below to create one.' %}
{% endif %}
</div>
<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:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Import' %}</a>

View File

@ -19,6 +19,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
@ -61,18 +62,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-users pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Groups.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any groups." %}
{% else %}
{% trans 'Currently no group exist. Click the button below to create one.' %}
{% endif %}
</div>
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>

View File

@ -20,6 +20,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<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>
@ -59,12 +60,10 @@
<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 %}
{% if not ver.version %}
<i class="fas fa-question-circle"></i>
{% elif ver.outdated %}
<i class="fas fa-times pf-m-danger"></i> {% blocktrans with is=ver.version should=ver.should %}{{ is }}, should be {{ should }}{% endblocktrans %}
{% else %}
<i class="fas fa-check pf-m-success"></i> {{ ver.version }}
{% endif %}
@ -83,18 +82,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="fas fa-map-marker pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Outposts.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any outposts." %}
{% else %}
{% trans 'Currently no outposts exist. Click the button below to create one.' %}
{% endif %}
</div>
<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>

View File

@ -1,6 +1,7 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load static %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
@ -10,33 +11,51 @@
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter">
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
<i class="pf-icon pf-icon-server"></i> {% trans 'Logins over the last 24 hours' %}
</div>
</div>
<div class="pf-c-card__body" style="position: relative; height:100%; width:100%">
<canvas id="logins-last-metrics"></canvas>
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 2;grid-row-end: span 3;">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Apps with most usage' %}
</div>
</div>
<div class="pf-c-card__body">
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ application_count }}
</p>
<table class="pf-c-table pf-m-compact" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Application' %}</th>
<th role="columnheader" scope="col">{% trans 'Logins' %}</th>
<th role="columnheader" scope="col"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for app in most_used_applications %}
<tr role="row">
<td role="cell">
{{ app.application.name }}
</td>
<td role="cell">
{{ app.total_logins }}
</td>
<td role="cell">
<progress value="{{ app.total_logins }}" max="{{ most_used_applications.0.total_logins }}"></progress>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</a>
</div>
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %}
</div>
</div>
<div class="pf-c-card__body">
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ source_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
@ -54,42 +73,9 @@
</p>
{% endif %}
</div>
</a>
</div>
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Stages' %}
</div>
</div>
<div class="pf-c-card__body">
{% if stage_count < 1 %}
<p class="aggregate-status">
<i class="pficon-error-circle-o"></i> {{ stage_count }}
</p>
<p>{% trans 'No Stages configured. No Users will be able to login.' %}"></p>
{% else %}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ stage_count }}
</p>
{% endif %}
</div>
</a>
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-topology"></i> {% trans 'Flows' %}
</div>
</div>
<div class="pf-c-card__body">
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ flow_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
@ -107,22 +93,9 @@
</p>
{% endif %}
</div>
</a>
</div>
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %}
</div>
</div>
<div class="pf-c-card__body">
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ invitation_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
@ -133,9 +106,9 @@
<i class="fa fa-check-circle"></i> {{ user_count }}
</p>
</div>
</a>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
@ -161,27 +134,35 @@
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
</div>
</div>
<div class="pf-c-card__body">
{% if worker_count < 1 %}
<p class="aggregate-status">
<i class="fa fa-exclamation-triangle"></i> {{ worker_count }}
</p>
<p>{% trans 'No workers connected.' %}</p>
{% else %}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ worker_count }}
</p>
{% endif %}
</div>
<fetch-fill-slot class="pf-c-card__body" url="{% url 'passbook_api:admin_overview-list' %}" key="worker_count">
<div slot="value < 1">
<p class="aggregate-status">
<i class="fa fa-exclamation-triangle"></i> <span data-value></span>
</p>
<p>{% trans 'No workers connected.' %}</p>
</div>
<div slot="value >= 1">
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> <span data-value></span>
</p>
</div>
<div>
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
</fetch-fill-slot>
</div>
<a class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
<a class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
@ -201,7 +182,7 @@
</div>
</a>
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
@ -254,4 +235,65 @@
</div>
</div>
</div>
<script src="{% static 'node_modules/chart.js/dist/Chart.bundle.min.js' %}"></script>
<script>
var ctx = document.getElementById('logins-last-metrics').getContext('2d');
fetch("{% url 'passbook_api:admin_metrics-list' %}").then(r => r.json()).then(r => {
var myChart = new Chart(ctx, {
type: 'bar',
data: {
datasets: [
{
label: 'Failed Logins',
backgroundColor: "rgba(201, 25, 11, .5)",
spanGaps: true,
data: r.logins_failed_per_1h,
},
{
label: 'Successful Logins',
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data: r.logins_per_1h,
},
]
},
options: {
maintainAspectRatio: false,
spanGaps: true,
scales: {
xAxes: [{
stacked: true,
gridLines: {
color: "rgba(0, 0, 0, 0)",
},
type: 'time',
offset: true,
ticks: {
callback: function (value, index, values) {
const date = new Date();
const delta = (date - values[index].value);
const ago = Math.round(delta / 1000 / 3600);
console.log(ago);
return `${ago} Hours ago`;
},
autoSkip: true,
maxTicksLimit: 8
}
}],
yAxes: [{
ticks: {
stepSize: 1
},
stacked: true,
gridLines: {
color: "rgba(0, 0, 0, 0)",
}
}]
}
}
});
});
</script>
{% endblock %}

View File

@ -18,6 +18,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
@ -78,18 +79,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-infrastructure pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Policies.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any policies." %}
{% else %}
{% trans 'Currently no policies exist. Click the button below to create one.' %}
{% endif %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">

View File

@ -75,7 +75,7 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}

View File

@ -19,6 +19,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
@ -72,18 +73,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-blueprint pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Property Mappings.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any property mappings." %}
{% else %}
{% trans 'Currently no property mappings exist. Click the button below to create one.' %}
{% endif %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">

View File

@ -20,6 +20,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
@ -91,18 +92,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon-integration pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Providers.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any providers." %}
{% else %}
{% trans 'Currently no providers exist. Click the button below to create one.' %}
{% endif %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">

View File

@ -20,6 +20,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
@ -85,18 +86,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-middleware pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Sources.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any sources." %}
{% else %}
{% trans 'Currently no sources exist. Click the button below to create one.' %}
{% endif %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">

View File

@ -11,8 +11,7 @@
<i class="pf-icon pf-icon-plugged"></i>
{% trans 'Stages' %}
</h1>
<p>{% trans "Stages are single steps of a Flow that a user is guided through." %}
</p>
<p>{% trans "Stages are single steps of a Flow that a user is guided through." %}</p>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
@ -20,6 +19,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
@ -81,18 +81,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-plugged pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Stages.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any stages." %}
{% else %}
{% trans 'Currently no stages exist. Click the button below to create one.' %}
{% endif %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">

View File

@ -81,7 +81,7 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}

View File

@ -19,6 +19,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
@ -54,18 +55,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-migration pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Invitations.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any invitations." %}
{% else %}
{% trans 'Currently no invitations exist. Click the button below to create one.' %}
{% endif %}
</div>
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>

View File

@ -19,6 +19,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
@ -80,18 +81,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-plugged pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Stage Prompts.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any stage prompts." %}
{% else %}
{% trans 'Currently no stage prompts exist. Click the button below to create one.' %}
{% endif %}
</div>
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>

View File

@ -18,13 +18,14 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
{% include 'partials/pagination.html' %}
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Token' %}</th>
<th role="columnheader" scope="col">{% trans 'Identifier' %}</th>
<th role="columnheader" scope="col">{% trans 'User' %}</th>
<th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
@ -35,9 +36,7 @@
{% for token in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<div>{{ token.pk.hex }}</div>
</div>
<div>{{ token.identifier }}</div>
</th>
<td role="cell">
<span>
@ -65,18 +64,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="fas fa-key pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Tokens.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any token." %}
{% else %}
{% trans 'Currently no tokens exist.' %}
{% endif %}
</div>
</div>
</div>

View File

@ -17,6 +17,7 @@
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
@ -61,18 +62,27 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<i class="pf-icon pf-icon-user pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Users.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any users." %}
{% else %}
{% trans 'Currently no users exist. How did you even get here.' %}
{% endif %}
</div>
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>

View File

@ -191,10 +191,20 @@ urlpatterns = [
),
# Flows
path("flows/", flows.FlowListView.as_view(), name="flows"),
path("flows/create/", flows.FlowCreateView.as_view(), name="flow-create",),
path("flows/import/", flows.FlowImportView.as_view(), name="flow-import",),
path(
"flows/<uuid:pk>/update/", flows.FlowUpdateView.as_view(), name="flow-update",
"flows/create/",
flows.FlowCreateView.as_view(),
name="flow-create",
),
path(
"flows/import/",
flows.FlowImportView.as_view(),
name="flow-import",
),
path(
"flows/<uuid:pk>/update/",
flows.FlowUpdateView.as_view(),
name="flow-update",
),
path(
"flows/<uuid:pk>/execute/",
@ -202,10 +212,14 @@ urlpatterns = [
name="flow-execute",
),
path(
"flows/<uuid:pk>/export/", flows.FlowExportView.as_view(), name="flow-export",
"flows/<uuid:pk>/export/",
flows.FlowExportView.as_view(),
name="flow-export",
),
path(
"flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
"flows/<uuid:pk>/delete/",
flows.FlowDeleteView.as_view(),
name="flow-delete",
),
# Property Mappings
path(
@ -273,9 +287,15 @@ urlpatterns = [
name="certificatekeypair-delete",
),
# Outposts
path("outposts/", outposts.OutpostListView.as_view(), name="outposts",),
path(
"outposts/create/", outposts.OutpostCreateView.as_view(), name="outpost-create",
"outposts/",
outposts.OutpostListView.as_view(),
name="outposts",
),
path(
"outposts/create/",
outposts.OutpostCreateView.as_view(),
name="outpost-create",
),
path(
"outposts/<uuid:pk>/update/",

View File

@ -12,6 +12,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.core.forms.applications import ApplicationForm
@ -20,7 +21,11 @@ from passbook.lib.views import CreateAssignPermView
class ApplicationListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all applications"""
@ -29,6 +34,15 @@ class ApplicationListView(
ordering = "name"
template_name = "administration/application/list.html"
search_fields = [
"name",
"slug",
"meta_launch_url",
"meta_icon_url",
"meta_description",
"meta_publisher",
]
class ApplicationCreateView(
SuccessMessageMixin,

View File

@ -12,6 +12,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.crypto.forms import CertificateKeyPairForm
@ -20,7 +21,11 @@ from passbook.lib.views import CreateAssignPermView
class CertificateKeyPairListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all keypairs"""
@ -29,6 +34,8 @@ class CertificateKeyPairListView(
ordering = "name"
template_name = "administration/certificatekeypair/list.html"
search_fields = ["name"]
class CertificateKeyPairCreateView(
SuccessMessageMixin,

View File

@ -14,6 +14,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.flows.forms import FlowForm, FlowImportForm
@ -28,7 +29,11 @@ from passbook.lib.views import CreateAssignPermView
class FlowListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all flows"""
@ -36,6 +41,7 @@ class FlowListView(
permission_required = "passbook_flows.view_flow"
ordering = "name"
template_name = "administration/flow/list.html"
search_fields = ["name", "slug", "designation", "title"]
class FlowCreateView(
@ -100,7 +106,9 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user})
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug,
"passbook_flows:flow-executor-shell",
self.request.GET,
flow_slug=flow.slug,
)

View File

@ -12,6 +12,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.core.forms.groups import GroupForm
@ -20,7 +21,11 @@ from passbook.lib.views import CreateAssignPermView
class GroupListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all groups"""
@ -28,6 +33,7 @@ class GroupListView(
permission_required = "passbook_core.view_group"
ordering = "name"
template_name = "administration/group/list.html"
search_fields = ["name", "attributes"]
class GroupCreateView(

View File

@ -15,6 +15,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.lib.views import CreateAssignPermView
@ -23,7 +24,11 @@ from passbook.outposts.models import Outpost, OutpostConfig
class OutpostListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all outposts"""
@ -31,6 +36,7 @@ class OutpostListView(
permission_required = "passbook_outposts.view_outpost"
ordering = "name"
template_name = "administration/outpost/list.html"
search_fields = ["name", "_config"]
class OutpostCreateView(

View File

@ -1,7 +1,10 @@
"""passbook administration overview"""
from typing import Union
from django.conf import settings
from django.core.cache import cache
from django.db.models import Count
from django.db.models.fields.json import KeyTextTransform
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
from packaging.version import LegacyVersion, Version, parse
@ -9,11 +12,9 @@ from packaging.version import LegacyVersion, Version, parse
from passbook import __version__
from passbook.admin.mixins import AdminRequiredMixin
from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from passbook.core.models import Application, Provider, Source, User
from passbook.flows.models import Flow, Stage
from passbook.audit.models import Event, EventAction
from passbook.core.models import Provider, User
from passbook.policies.models import Policy
from passbook.root.celery import CELERY_APP
from passbook.stages.invitation.models import Invitation
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
@ -32,22 +33,32 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
"""Get latest version from cache"""
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache:
update_latest_version.delay()
if not settings.DEBUG:
update_latest_version.delay()
return parse(__version__)
return parse(version_in_cache)
def get_most_used_applications(self):
"""Get Most used applications, total login counts and unique users that have used them."""
return (
Event.objects.filter(action=EventAction.AUTHORIZE_APPLICATION)
.exclude(context__authorized_application=None)
.annotate(application=KeyTextTransform("authorized_application", "context"))
.annotate(user_pk=KeyTextTransform("pk", "user"))
.values("application")
.annotate(total_logins=Count("application"))
.annotate(unique_users=Count("user_pk", distinct=True))
.values("unique_users", "application", "total_logins")
.order_by("-total_logins")[:15]
)
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()) - 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())
kwargs["flow_count"] = len(Flow.objects.all())
kwargs["invitation_count"] = len(Invitation.objects.all())
kwargs["version"] = parse(__version__)
kwargs["version_latest"] = self.get_latest_version()
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs["most_used_applications"] = self.get_most_used_applications()
kwargs["providers_without_application"] = Provider.objects.filter(
application=None
)

View File

@ -22,6 +22,7 @@ from passbook.admin.views.utils import (
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.policies.models import Policy, PolicyBinding
@ -29,7 +30,11 @@ from passbook.policies.process import PolicyProcess, PolicyRequest
class PolicyListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
InheritanceListView,
):
"""Show list of all policies"""
@ -37,6 +42,7 @@ class PolicyListView(
permission_required = "passbook_policies.view_policy"
ordering = "name"
template_name = "administration/policy/list.html"
search_fields = ["name"]
class PolicyCreateView(

View File

@ -14,13 +14,18 @@ from passbook.admin.views.utils import (
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.core.models import PropertyMapping
class PropertyMappingListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
InheritanceListView,
):
"""Show list of all property_mappings"""
@ -28,6 +33,7 @@ class PropertyMappingListView(
permission_required = "passbook_core.view_propertymapping"
template_name = "administration/property_mapping/list.html"
ordering = "name"
search_fields = ["name", "expression"]
class PropertyMappingCreateView(

View File

@ -14,13 +14,18 @@ from passbook.admin.views.utils import (
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.core.models import Provider
class ProviderListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
InheritanceListView,
):
"""Show list of all providers"""
@ -28,6 +33,7 @@ class ProviderListView(
permission_required = "passbook_core.add_provider"
template_name = "administration/provider/list.html"
ordering = "id"
search_fields = ["id", "name"]
class ProviderCreateView(

View File

@ -14,13 +14,18 @@ from passbook.admin.views.utils import (
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.core.models import Source
class SourceListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
InheritanceListView,
):
"""Show list of all sources"""
@ -28,6 +33,7 @@ class SourceListView(
permission_required = "passbook_core.view_source"
ordering = "name"
template_name = "administration/source/list.html"
search_fields = ["name", "slug"]
class SourceCreateView(

View File

@ -14,13 +14,18 @@ from passbook.admin.views.utils import (
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.flows.models import Stage
class StageListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
InheritanceListView,
):
"""Show list of all stages"""
@ -28,6 +33,7 @@ class StageListView(
template_name = "administration/stage/list.html"
permission_required = "passbook_flows.view_stage"
ordering = "name"
search_fields = ["name"]
class StageCreateView(

View File

@ -13,6 +13,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.lib.views import CreateAssignPermView
@ -22,7 +23,11 @@ from passbook.stages.invitation.signals import invitation_created
class InvitationListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all invitations"""
@ -30,6 +35,7 @@ class InvitationListView(
permission_required = "passbook_stages_invitation.view_invitation"
template_name = "administration/stage_invitation/list.html"
ordering = "-expires"
search_fields = ["created_by__username", "expires", "fixed_data"]
class InvitationCreateView(

View File

@ -12,6 +12,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.lib.views import CreateAssignPermView
@ -20,7 +21,11 @@ from passbook.stages.prompt.models import Prompt
class PromptListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all prompts"""
@ -28,6 +33,12 @@ class PromptListView(
permission_required = "passbook_stages_prompt.view_prompt"
ordering = "order"
template_name = "administration/stage_prompt/list.html"
search_fields = [
"field_key",
"label",
"type",
"placeholder",
]
class PromptCreateView(

View File

@ -5,12 +5,20 @@ from django.utils.translation import gettext as _
from django.views.generic import ListView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView, UserPaginateListMixin
from passbook.admin.views.utils import (
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.core.models import Token
class TokenListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all tokens"""
@ -18,6 +26,12 @@ class TokenListView(
permission_required = "passbook_core.view_token"
ordering = "expires"
template_name = "administration/token/list.html"
search_fields = [
"identifier",
"intent",
"user__username",
"description",
]
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):

View File

@ -21,6 +21,7 @@ from passbook.admin.forms.users import UserForm
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.core.models import Token, User
@ -28,7 +29,11 @@ from passbook.lib.views import CreateAssignPermView
class UserListView(
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all users"""
@ -36,6 +41,7 @@ class UserListView(
permission_required = "passbook_core.view_user"
ordering = "username"
template_name = "administration/user/list.html"
search_fields = ["username", "name", "attributes"]
def get_queryset(self):
return super().get_queryset().exclude(pk=get_anonymous_user().pk)
@ -101,7 +107,9 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Create token for user and return link"""
super().get(request, *args, **kwargs)
token = Token.objects.create(user=self.object)
token, _ = Token.objects.get_or_create(
identifier="password-reset-temp", user=self.object
)
querystring = urlencode({"token": token.token_uuid})
link = request.build_absolute_uri(
reverse("passbook_flows:default-recovery") + f"?{querystring}"

View File

@ -1,13 +1,15 @@
"""passbook admin util views"""
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.postgres.search import SearchQuery, SearchVector
from django.db.models import QuerySet
from django.http import Http404
from django.http.request import HttpRequest
from django.views.generic import DeleteView, ListView, UpdateView
from django.views.generic.list import MultipleObjectMixin
from passbook.lib.utils.reflection import all_subclasses
from passbook.lib.views import CreateAssignPermView
@ -32,6 +34,26 @@ class InheritanceListView(ListView):
return super().get_queryset().select_subclasses()
class SearchListMixin(MultipleObjectMixin):
"""Accept search query using `search` querystring parameter. Requires self.search_fields,
a list of all fields to search. Can contain special lookups like __icontains"""
search_fields: List[str]
def get_queryset(self) -> QuerySet:
queryset = super().get_queryset()
if "search" in self.request.GET:
raw_query = self.request.GET["search"]
if raw_query == "":
# Empty query, don't search at all
return queryset
search = SearchQuery(raw_query, search_type="websearch")
return queryset.annotate(search=SearchVector(*self.search_fields)).filter(
search=search
)
return queryset
class InheritanceCreateView(CreateAssignPermView):
"""CreateView for objects using InheritanceManager"""

View File

@ -1,30 +0,0 @@
"""permission classes for django restframework"""
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
from passbook.policies.engine import PolicyEngine
from passbook.policies.models import PolicyBindingModel
class CustomObjectPermissions(DjangoObjectPermissions):
"""Similar to `DjangoObjectPermissions`, but adding 'view' permissions."""
perms_map = {
"GET": ["%(app_label)s.view_%(model_name)s"],
"OPTIONS": ["%(app_label)s.view_%(model_name)s"],
"HEAD": ["%(app_label)s.view_%(model_name)s"],
"POST": ["%(app_label)s.add_%(model_name)s"],
"PUT": ["%(app_label)s.change_%(model_name)s"],
"PATCH": ["%(app_label)s.change_%(model_name)s"],
"DELETE": ["%(app_label)s.delete_%(model_name)s"],
}
class PolicyPermissions(BasePermission):
"""Permission checker based on PolicyEngine"""
policy_engine: PolicyEngine
def has_object_permission(self, request, view, obj: PolicyBindingModel) -> bool:
self.policy_engine = PolicyEngine(obj.policies, request.user, request)
self.policy_engine.request.obj = obj
return self.policy_engine.build().passing

View File

@ -0,0 +1,7 @@
{% extends "rest_framework/base.html" %}
{% block branding %}
<span class='navbar-brand'>
passbook
</span>
{% endblock %}

View File

@ -3,8 +3,10 @@ from django.urls import path, re_path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import routers
from rest_framework.permissions import AllowAny
from passbook.api.permissions import CustomObjectPermissions
from passbook.admin.api.overview import AdministrationOverviewViewSet
from passbook.admin.api.overview_metrics import AdministrationMetricsViewSet
from passbook.api.v2.messages import MessagesViewSet
from passbook.audit.api import EventViewSet
from passbook.core.api.applications import ApplicationViewSet
@ -12,6 +14,7 @@ from passbook.core.api.groups import GroupViewSet
from passbook.core.api.propertymappings import PropertyMappingViewSet
from passbook.core.api.providers import ProviderViewSet
from passbook.core.api.sources import SourceViewSet
from passbook.core.api.tokens import TokenViewSet
from passbook.core.api.users import UserViewSet
from passbook.crypto.api import CertificateKeyPairViewSet
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
@ -49,9 +52,17 @@ from passbook.stages.user_write.api import UserWriteStageViewSet
router = routers.DefaultRouter()
router.register("root/messages", MessagesViewSet, basename="messages")
router.register(
"admin/overview", AdministrationOverviewViewSet, basename="admin_overview"
)
router.register("admin/metrics", AdministrationMetricsViewSet, basename="admin_metrics")
router.register("core/applications", ApplicationViewSet)
router.register("core/groups", GroupViewSet)
router.register("core/users", UserViewSet)
router.register("core/tokens", TokenViewSet)
router.register("outposts/outposts", OutpostViewSet)
router.register("outposts/proxy", OutpostConfigViewSet)
@ -114,7 +125,9 @@ info = openapi.Info(
license=openapi.License(name="MIT License"),
)
SchemaView = get_schema_view(
info, public=True, permission_classes=(CustomObjectPermissions,),
info,
public=True,
permission_classes=(AllowAny,),
)
urlpatterns = [

View File

@ -47,7 +47,10 @@ class Migration(migrations.Migration):
),
("date", models.DateTimeField(auto_now_add=True)),
("app", models.TextField()),
("context", models.JSONField(blank=True, default=dict),),
(
"context",
models.JSONField(blank=True, default=dict),
),
("client_ip", models.GenericIPAddressField(null=True)),
("created", models.DateTimeField(auto_now_add=True)),
(

View File

@ -49,10 +49,15 @@ class Migration(migrations.Migration):
),
),
migrations.AddField(
model_name="event", name="user_json", field=models.JSONField(default=dict),
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.RemoveField(
model_name="event",
name="user",
),
migrations.RenameField(
model_name="event", old_name="user_json", new_name="user"
),

View File

@ -0,0 +1,37 @@
# Generated by Django 3.1.2 on 2020-10-05 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_audit", "0004_auto_20200921_1829"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("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"),
]
),
),
]

View File

@ -96,14 +96,14 @@ class EventAction(models.TextChoices):
LOGIN_FAILED = "login_failed"
LOGOUT = "logout"
SIGN_UP = "sign_up"
AUTHORIZE_APPLICATION = "authorize_application"
USER_WRITE = "user_write"
SUSPICIOUS_REQUEST = "suspicious_request"
PASSWORD_SET = "password_set" # noqa # nosec
INVITE_CREATED = "invitation_created"
INVITE_USED = "invitation_used"
AUTHORIZE_APPLICATION = "authorize_application"
SOURCE_LINKED = "source_linked"
IMPERSONATION_STARTED = "impersonation_started"

View File

@ -12,6 +12,7 @@ from django.http import HttpRequest
from passbook.audit.models import Event, EventAction
from passbook.core.models import User
from passbook.core.signals import password_changed
from passbook.stages.invitation.models import Invitation
from passbook.stages.invitation.signals import invitation_created, invitation_used
from passbook.stages.user_write.signals import user_write
@ -58,9 +59,12 @@ def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
@receiver(user_write)
# pylint: disable=unused-argument
def on_user_write(sender, request: HttpRequest, user: User, data: Dict[str, Any], **_):
def on_user_write(
sender, request: HttpRequest, user: User, data: Dict[str, Any], **kwargs
):
"""Log User write"""
thread = EventNewThread("stages/user_write", request, **data)
thread = EventNewThread(EventAction.USER_WRITE, request, **data)
thread.kwargs["created"] = kwargs.get("created", False)
thread.user = user
thread.run()
@ -93,3 +97,11 @@ def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_
EventAction.INVITE_USED, request, invitation_uuid=invitation.invite_uuid.hex
)
thread.run()
@receiver(password_changed)
# pylint: disable=unused-argument
def on_password_changed(sender, user: User, password: str, **_):
"""Log password change"""
thread = EventNewThread(EventAction.PASSWORD_SET, None, user=user)
thread.run()

View File

@ -77,7 +77,7 @@
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
</div>

View File

@ -1,8 +1,13 @@
"""Application API Views"""
from django.db.models import QuerySet
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from passbook.core.models import Application
from passbook.policies.engine import PolicyEngine
class ApplicationSerializer(ModelSerializer):
@ -29,3 +34,24 @@ class ApplicationViewSet(ModelViewSet):
queryset = Application.objects.all()
serializer_class = ApplicationSerializer
lookup_field = "slug"
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends):
if backend == ObjectPermissionsFilter:
continue
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
def list(self, request: Request, *_, **__) -> Response:
"""Custom list method that checks Policy based access instead of guardian"""
queryset = self._filter_queryset_for_list(self.get_queryset())
allowed_applications = []
for application in queryset.order_by("name"):
engine = PolicyEngine(application, self.request.user, self.request)
engine.build()
if engine.passing:
allowed_applications.append(application)
serializer = self.get_serializer(allowed_applications, many=True)
return Response(serializer.data)

View File

@ -17,7 +17,7 @@ class ProviderSerializer(ModelSerializer):
class Meta:
model = Provider
fields = ["pk", "authorization_flow", "property_mappings", "__type__"]
fields = ["pk", "name", "authorization_flow", "property_mappings", "__type__"]
class ProviderViewSet(ReadOnlyModelViewSet):

View File

@ -0,0 +1,22 @@
"""Tokens API Viewset"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.core.models import Token
class TokenSerializer(ModelSerializer):
"""Token Serializer"""
class Meta:
model = Token
fields = ["pk", "identifier", "intent", "user", "description"]
class TokenViewSet(ModelViewSet):
"""Token Viewset"""
queryset = Token.objects.all()
lookup_field = "identifier"
serializer_class = TokenSerializer

View File

@ -108,11 +108,18 @@ class Migration(migrations.Migration):
("uuid", models.UUIDField(default=uuid.uuid4, editable=False)),
("name", models.TextField(help_text="User's display name.")),
("password_change_date", models.DateTimeField(auto_now_add=True)),
("attributes", models.JSONField(blank=True, default=dict),),
(
"attributes",
models.JSONField(blank=True, default=dict),
),
],
options={"permissions": (("reset_user_password", "Reset Password"),),},
options={
"permissions": (("reset_user_password", "Reset Password"),),
},
bases=(guardian.mixins.GuardianUserMixin, models.Model),
managers=[("objects", django.contrib.auth.models.UserManager()),],
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name="PropertyMapping",
@ -192,7 +199,9 @@ class Migration(migrations.Migration):
),
),
],
options={"unique_together": {("user", "source")},},
options={
"unique_together": {("user", "source")},
},
),
migrations.CreateModel(
name="Token",
@ -223,7 +232,10 @@ class Migration(migrations.Migration):
),
),
],
options={"verbose_name": "Token", "verbose_name_plural": "Tokens",},
options={
"verbose_name": "Token",
"verbose_name_plural": "Tokens",
},
),
migrations.CreateModel(
name="Provider",
@ -258,7 +270,10 @@ class Migration(migrations.Migration):
),
),
("name", models.CharField(max_length=80, verbose_name="name")),
("attributes", models.JSONField(blank=True, default=dict),),
(
"attributes",
models.JSONField(blank=True, default=dict),
),
(
"parent",
models.ForeignKey(
@ -270,7 +285,9 @@ class Migration(migrations.Migration):
),
),
],
options={"unique_together": {("name", "parent")},},
options={
"unique_together": {("name", "parent")},
},
),
migrations.CreateModel(
name="Application",

View File

@ -12,7 +12,10 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RemoveField(model_name="application", name="skip_authorization",),
migrations.RemoveField(
model_name="application",
name="skip_authorization",
),
migrations.AddField(
model_name="source",
name="authentication_flow",

View File

@ -25,8 +25,14 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RemoveField(model_name="user", name="is_superuser",),
migrations.RemoveField(model_name="user", name="is_staff",),
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",

View File

@ -13,7 +13,10 @@ def create_default_admin_group(apps: Apps, schema_editor: BaseDatabaseSchemaEdit
# Creates a default admin group
group, _ = Group.objects.using(db_alias).get_or_create(
is_superuser=True, defaults={"name": "passbook Admins",}
is_superuser=True,
defaults={
"name": "passbook Admins",
},
)
group.users.set(User.objects.filter(username="pbadmin"))
group.save()
@ -26,8 +29,14 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RemoveField(model_name="user", name="is_superuser",),
migrations.RemoveField(model_name="user", name="is_staff",),
migrations.RemoveField(
model_name="user",
name="is_superuser",
),
migrations.RemoveField(
model_name="user",
name="is_staff",
),
migrations.AlterField(
model_name="user",
name="pb_groups",
@ -44,6 +53,9 @@ class Migration(migrations.Migration):
),
migrations.RunPython(create_default_admin_group),
migrations.AlterModelManagers(
name="user", managers=[("objects", passbook.core.models.UserManager()),],
name="user",
managers=[
("objects", passbook.core.models.UserManager()),
],
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 3.1.2 on 2020-10-03 17:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0010_auto_20200917_1021"),
]
operations = [
migrations.AddField(
model_name="provider",
name="name_temp",
field=models.TextField(default=""),
preserve_default=False,
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.1.2 on 2020-10-03 17:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0011_provider_name_temp"),
("passbook_providers_oauth2", "0006_remove_oauth2provider_name"),
("passbook_providers_saml", "0006_remove_samlprovider_name"),
]
operations = [
migrations.RenameField(
model_name="provider",
old_name="name_temp",
new_name="name",
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 3.1.2 on 2020-10-03 21:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0012_auto_20201003_1737"),
]
operations = [
migrations.AddField(
model_name="token",
name="identifier",
field=models.TextField(default=""),
preserve_default=False,
),
migrations.AlterField(
model_name="token",
name="intent",
field=models.TextField(
choices=[
("verification", "Intent Verification"),
("api", "Intent Api"),
("recovery", "Intent Recovery"),
],
default="verification",
),
),
migrations.AlterUniqueTogether(
name="token",
unique_together={("identifier", "user")},
),
]

View File

@ -56,7 +56,12 @@ class Group(models.Model):
class Meta:
unique_together = (("name", "parent",),)
unique_together = (
(
"name",
"parent",
),
)
class UserManager(DjangoUserManager):
@ -119,6 +124,8 @@ class User(GuardianUserMixin, AbstractUser):
class Provider(models.Model):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
name = models.TextField()
authorization_flow = models.ForeignKey(
Flow,
on_delete=models.CASCADE,
@ -143,11 +150,8 @@ class Provider(models.Model):
"""Return Form class used to edit this object"""
raise NotImplementedError
# This class defines no field for easier inheritance
def __str__(self):
if hasattr(self, "name"):
return getattr(self, "name")
return super().__str__()
return self.name
class Application(PolicyBindingModel):
@ -288,17 +292,20 @@ class ExpiringModel(models.Model):
class TokenIntents(models.TextChoices):
"""Intents a Token can be created for."""
# Single user token
# Single use token
INTENT_VERIFICATION = "verification"
# Allow access to API
INTENT_API = "api"
INTENT_RECOVERY = "recovery"
class Token(ExpiringModel):
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
identifier = models.TextField()
intent = models.TextField(
choices=TokenIntents.choices, default=TokenIntents.INTENT_VERIFICATION
)
@ -306,14 +313,13 @@ class Token(ExpiringModel):
description = models.TextField(default="", blank=True)
def __str__(self):
return (
f"Token {self.token_uuid.hex} {self.description} (expires={self.expires})"
)
return f"Token {self.identifier} (expires={self.expires})"
class Meta:
verbose_name = _("Token")
verbose_name_plural = _("Tokens")
unique_together = (("identifier", "user"),)
class PropertyMapping(models.Model):

View File

@ -1,4 +1,5 @@
"""passbook core signals"""
from django.core.signals import Signal
password_changed = Signal(providing_args=["user", "password"])
# Arguments: user: User, password: str
password_changed = Signal()

View File

@ -19,7 +19,7 @@
<i class="fas fa-bars" aria-hidden="true"></i>
</button>
</div>
<a class="pf-c-page__header-brand-link">
<a href="{% url 'passbook_core:overview' %}" class="pf-c-page__header-brand-link">
<div class="pf-c-brand pb-brand">
<img src="{{ config.passbook.branding.logo }}" alt="passbook icon">
{% if config.passbook.branding.title_show %}

View File

@ -1,43 +1,42 @@
{% load i18n %}
{% load passbook_utils %}
<div class="pf-c-toolbar__item pf-m-pagination">
<div class="pf-c-pagination">
<div class="pf-c-pagination__total-items">
<b>{{ page_obj.start_index }} - {{ page_obj.end_index }}</b>of
<b>{{ page_obj.count }}</b>
</div>
{% with param=get_param|default:'page' %}
<nav class="pf-c-pagination__nav" aria-label="Pagination">
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to first page" href="?{{ param }}=1">
<i class="fas fa-angle-double-left" aria-hidden="true"></i>
</a>
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to previous page"
{% if page_obj.has_previous %}
href="?{{ param }}={{ page_obj.previous_page_number }}"
{% else %}
disabled
{% endif %}>
<i class="fas fa-angle-left" aria-hidden="true"></i>
</a>
<div class="pf-c-pagination__nav-page-select">
<span>
{% blocktrans with current=page_obj.number total=page_obj.paginator.num_pages %}
{{ current }} of {{ total }}
{% endblocktrans %}
</span>
<div class="pf-c-toolbar__item pf-m-pagination ">
<div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md">
<div class="pf-c-pagination pf-m-compact pf-m-compact pf-m-hidden pf-m-visible-on-md">
<div class="pf-c-options-menu">
<div class="pf-c-options-menu__toggle pf-m-text pf-m-plain">
<span class="pf-c-options-menu__toggle-text">
{% blocktrans with start_index=page_obj.start_index end_index=page_obj.end_index total_items=paginator.count %}
{{ start_index }} - {{ end_index }} of {{ total_items }}
{% endblocktrans %}
</span>
</div>
</div>
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to next page"
{% if page_obj.has_next %}
href="?{{ param }}={{ page_obj.next_page_number }}"
{% else %}
disabled
{% endif %}>
<i class="fas fa-angle-right" aria-hidden="true"></i>
</a>
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to last page" href="?{{ param }}={{ page_obj.num_pages }}">
<i class="fas fa-angle-double-right" aria-hidden="true"></i>
</a>
</nav>
{% endwith %}
<nav class="pf-c-pagination__nav" aria-label="Pagination">
<div class="pf-c-pagination__nav-control pf-m-prev">
<a class="pf-c-button pf-m-plain"
{% if page_obj.has_previous %}
href="?{% query_transform page=page_obj.previous_page_number %}"
{% else %}
disabled
{% endif %}
aria-label="{% trans 'Go to previous page' %}">
<i class="fas fa-angle-left" aria-hidden="true"></i>
</a>
</div>
<div class="pf-c-pagination__nav-control pf-m-next">
<a class="pf-c-button pf-m-plain"
{% if page_obj.has_next %}
href="?{% query_transform page=page_obj.next_page_number %}"
{% else %}
disabled
{% endif %}
aria-label="{% trans 'Go to next page' %}">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</a>
</div>
</nav>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
<div class="pf-c-toolbar__group pf-m-filter-group">
<div class="pf-c-toolbar__item pf-m-search-filter">
<form class="pf-c-input-group" method="GET">
{# include page data for pagination #}
<input type="hidden" name="page" value="{{ page_obj.number }}">
<input class="pf-c-form-control" name="search" type="search" placeholder="Search..." value="{{ request.GET.search }}">
<button class="pf-c-button pf-m-control" type="submit">
<i class="fas fa-search" aria-hidden="true"></i>
</button>
</form>
</div>
</div>

View File

@ -30,9 +30,9 @@ def user_stages(context: RequestContext) -> List[UIUserSettings]:
def user_sources(context: RequestContext) -> List[UIUserSettings]:
"""Return a list of all sources which are enabled for the user"""
user = context.get("request").user
_all_sources: Iterable[Source] = (
Source.objects.filter(enabled=True).select_subclasses()
)
_all_sources: Iterable[Source] = Source.objects.filter(
enabled=True
).select_subclasses()
matching_sources: List[UIUserSettings] = []
for source in _all_sources:
user_settings = source.ui_user_settings

View File

@ -36,7 +36,8 @@ class CertificateBuilder:
x509.Name(
[
x509.NameAttribute(
NameOID.COMMON_NAME, "passbook Self-signed Certificate",
NameOID.COMMON_NAME,
"passbook Self-signed Certificate",
),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "passbook"),
x509.NameAttribute(
@ -49,7 +50,8 @@ class CertificateBuilder:
x509.Name(
[
x509.NameAttribute(
NameOID.COMMON_NAME, "passbook Self-signed Certificate",
NameOID.COMMON_NAME,
"passbook Self-signed Certificate",
),
]
)

Some files were not shown because too many files have changed in this diff Show More