Compare commits

..

66 Commits

Author SHA1 Message Date
36b1f8ba36 new release: 0.8.3-beta 2020-02-20 15:14:49 +01:00
6c889eff27 core: fix application icons not loading, fix with_sources being broken 2020-02-20 14:30:06 +01:00
9d8675e54b new release: 0.8.2-beta 2020-02-20 13:57:46 +01:00
22ae986c0b root: add logger name to log output 2020-02-20 13:52:14 +01:00
2bef5f3911 policies: struct -> types to match core 2020-02-20 13:52:05 +01:00
3c2b8e5ee1 all: prefix all UI related methods with ui_, switch to property and return dataclass 2020-02-20 13:51:41 +01:00
c96571bdba core: fix discord logo being hard to see 2020-02-20 13:50:05 +01:00
2dfd93afb1 core: add more fields for metadata of applications 2020-02-20 13:45:22 +01:00
1d22e30c70 lib: sentry ignore Redis and OSError 2020-02-19 17:13:44 +01:00
07b7951390 sources/ldap: handle user_sync errors better, show warning when user exists already 2020-02-19 16:20:33 +01:00
995615d0a0 policies/expression: Return False if Policy returns Undefined and log warning 2020-02-19 16:19:02 +01:00
ac273aab75 core: raise PropertyMappingExpressionException when PropertyMapping returns Undefined 2020-02-19 16:18:31 +01:00
44cd03654d core: base set maximum-scale to 1 2020-02-19 15:11:25 +01:00
3e2375f970 new release: 0.8.1-beta 2020-02-19 11:31:05 +01:00
38ad8e5fd3 policies/expression: fix pb_is_sso_flow 2020-02-19 11:01:20 +01:00
c481558a46 helm: fix error that FLUSHDB Command is not available 2020-02-19 10:57:57 +01:00
e27a05a7fc lib/sentry: ignore django validation error 2020-02-19 10:54:29 +01:00
e4886f0c6f new release: 0.8.0-beta 2020-02-19 10:29:52 +01:00
8b2ce5476a policies/expression: add annotation to update docs, name jinja filters/funcs more clearly 2020-02-19 10:23:42 +01:00
1b82283a20 docs: update policy types, add docs for expression policies 2020-02-19 10:21:28 +01:00
7f3d0113c2 policies: remove redundant policies which can be easily implemented with expressions 2020-02-19 09:51:15 +01:00
0f6dd33a6b api: add expression policy to API URLs 2020-02-19 09:49:57 +01:00
5b79b3fd22 policies/expression: move evaluation code into separate class 2020-02-19 09:49:38 +01:00
d68c72f1fa lib: remove method_decorator Mixins 2020-02-18 22:28:47 +01:00
9267d0c1dd all: general maintenance, prepare for pyright 2020-02-18 22:12:51 +01:00
865abc005a sources/oauth: remove leading spaces in default URLs 2020-02-18 21:49:53 +01:00
a2725d5b82 sources/oauth: remove redundant OAuth2Clients 2020-02-18 21:49:40 +01:00
4a05bc6e02 sources/oauth: improve default OAuth2 Client, send access_token as Bearer Authz 2020-02-18 21:49:23 +01:00
4e8238603a all: cleanup logging to be structured 2020-02-18 21:35:58 +01:00
ff25c1c057 admin: load custom policy templates 2020-02-18 21:35:21 +01:00
78cddca0d7 admin: fix user object being overwritten when deleting a user 2020-02-18 21:35:06 +01:00
4742ee1d93 docs: add aws integration 2020-02-18 20:14:54 +01:00
0c2dc309e7 providers/saml: fix metadata URLs using incorrect params 2020-02-18 20:14:28 +01:00
144935d10f docs: add ansible tower/awx integration guide 2020-02-18 17:33:31 +01:00
74ad1b6759 factors: strip port for domain check 2020-02-18 17:05:30 +01:00
591d2f89a1 audit: log event creation on save 2020-02-18 17:05:11 +01:00
7c353f9297 sources/oauth: remove supervisr 2020-02-18 17:01:08 +01:00
cd1af15c56 core: sort applications by name 2020-02-18 17:00:56 +01:00
878169ea2e core: only show icon on login page if defined 2020-02-18 17:00:26 +01:00
38dfb03668 new release: 0.7.17-beta 2020-02-18 16:29:23 +01:00
e2631cec0e factors/view: show concise error message when domain is mis-configured 2020-02-18 16:29:04 +01:00
5dad853f8a docs: use note blocks instead of code blocks for product description 2020-02-18 15:34:41 +01:00
9f00843441 policies/expression: add Expression based policy 2020-02-18 15:12:50 +01:00
f31cd7dec6 core: check PropertyMapping's expression syntax before save 2020-02-18 15:12:05 +01:00
1c1afca31f providers/saml: fix linting error 2020-02-18 11:34:04 +01:00
fbd4bdef33 providers/saml: add modal to show metadata without download 2020-02-18 10:57:43 +01:00
5b22f9b6c3 providers/saml: transition to dataclass from dict, cleanup unused templates, add missing autosubmit_form 2020-02-18 10:57:30 +01:00
083e317028 lib: add helper method for 400 response with message 2020-02-18 10:13:53 +01:00
95416623b3 sources/ldap: better handle property mapping evaluation errors 2020-02-18 10:13:05 +01:00
813b2676de providers/saml: better handle PropertyMapping evaluation errors 2020-02-18 10:12:42 +01:00
aeca66a288 providers/saml: change assertion_valid_not_before default to -5 minutes 2020-02-17 21:32:23 +01:00
04a5428148 new release: 0.7.16-beta 2020-02-17 21:02:54 +01:00
73b173b92a admin: fix form missing on update pages 2020-02-17 21:02:47 +01:00
7cbf20a71c admin: fix CodeMirror field not loading correctly 2020-02-17 21:02:35 +01:00
7a98e6d92b new release: 0.7.15-beta 2020-02-17 20:45:56 +01:00
49e915f98b Merge pull request #4 from BeryJu/propertymapping-jinja
PropertyMappings using Jinja
2020-02-17 20:45:04 +01:00
3aa2f1e892 *: propertymapping template -> expression 2020-02-17 20:38:14 +01:00
bc4b7ef44d providers/saml: add custom help text for templates, add docs for User Object reference 2020-02-17 20:30:14 +01:00
9400b01a55 admin: parameterise generic from's base template 2020-02-17 20:29:41 +01:00
e57da71dcf sources/ldap: update LDAP source to use new property mappings 2020-02-17 17:55:48 +01:00
7268afaaf9 providers/saml: update to new PropertyMappings 2020-02-17 17:50:11 +01:00
205183445c admin: add support for template field and Jinja2 highlighting 2020-02-17 17:48:53 +01:00
a08bdfdbcd root: remove prospector from Pipfile as it causes lock issues, install in CI 2020-02-17 17:48:18 +01:00
e6c47fee26 core: add template field to PropertyMapping 2020-02-17 17:47:51 +01:00
a5629c5155 providers/saml: add changeable signature and digest algorithm 2020-02-17 16:28:18 +01:00
41689fe3ce sources/* add missing migrations 2020-02-17 16:27:35 +01:00
145 changed files with 1691 additions and 1369 deletions

View File

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

View File

@ -56,7 +56,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-pipenv- ${{ runner.os }}-pipenv-
- name: Install dependencies - name: Install dependencies
run: pip install -U pip pipenv && pipenv install --dev run: pip install -U pip pipenv && pipenv install --dev && pipenv install --dev prospector --skip-lock
- name: Lint with prospector - name: Lint with prospector
run: pipenv run prospector run: pipenv run prospector
bandit: bandit:

View File

@ -16,11 +16,11 @@ jobs:
- name: Building Docker Image - name: Building Docker Image
run: docker build run: docker build
--no-cache --no-cache
-t beryju/passbook:0.7.14-beta -t beryju/passbook:0.8.3-beta
-t beryju/passbook:latest -t beryju/passbook:latest
-f Dockerfile . -f Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.7.14-beta run: docker push beryju/passbook:0.8.3-beta
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook:latest run: docker push beryju/passbook:latest
build-gatekeeper: build-gatekeeper:
@ -37,11 +37,11 @@ jobs:
cd gatekeeper cd gatekeeper
docker build \ docker build \
--no-cache \ --no-cache \
-t beryju/passbook-gatekeeper:0.7.14-beta \ -t beryju/passbook-gatekeeper:0.8.3-beta \
-t beryju/passbook-gatekeeper:latest \ -t beryju/passbook-gatekeeper:latest \
-f Dockerfile . -f Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-gatekeeper:0.7.14-beta run: docker push beryju/passbook-gatekeeper:0.8.3-beta
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-gatekeeper:latest run: docker push beryju/passbook-gatekeeper:latest
build-static: build-static:
@ -66,11 +66,11 @@ jobs:
run: docker build run: docker build
--no-cache --no-cache
--network=$(docker network ls | grep github | awk '{print $1}') --network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:0.7.14-beta -t beryju/passbook-static:0.8.3-beta
-t beryju/passbook-static:latest -t beryju/passbook-static:latest
-f static.Dockerfile . -f static.Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.7.14-beta run: docker push beryju/passbook-static:0.8.3-beta
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest run: docker push beryju/passbook-static:latest
test-release: test-release:

View File

@ -40,6 +40,7 @@ signxml = "*"
structlog = "*" structlog = "*"
swagger-spec-validator = "*" swagger-spec-validator = "*"
urllib3 = {extras = ["secure"],version = "*"} urllib3 = {extras = ["secure"],version = "*"}
jinja2 = "*"
[requires] [requires]
python_version = "3.8" python_version = "3.8"
@ -51,7 +52,6 @@ bumpversion = "*"
colorama = "*" colorama = "*"
coverage = "*" coverage = "*"
django-debug-toolbar = "*" django-debug-toolbar = "*"
prospector = "*"
pylint = "*" pylint = "*"
pylint-django = "*" pylint-django = "*"
unittest-xml-reporting = "*" unittest-xml-reporting = "*"

324
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "3693ac70f66ada5c8c8784e6e2d5c2f0ee75219e7141dde1075347234366e314" "sha256": "c9232d99b062ee14eda43d176481f16da78ce53f912c844db3f33f1b3e8a47b7"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -23,6 +23,13 @@
], ],
"version": "==2.5.2" "version": "==2.5.2"
}, },
"asgiref": {
"hashes": [
"sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
"sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"
],
"version": "==3.2.3"
},
"asn1crypto": { "asn1crypto": {
"hashes": [ "hashes": [
"sha256:5a215cb8dc12f892244e3a113fe05397ee23c5c4ca7a69cd6e69811755efc42d", "sha256:5a215cb8dc12f892244e3a113fe05397ee23c5c4ca7a69cd6e69811755efc42d",
@ -46,18 +53,18 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:7aaccacc199cd633b5ac14f0d544c9d592c53275a79cf4d230a9f66215331665", "sha256:33462a79d57c9c4a215e075472509537d03545f54566fc4f776fb0f4cfa616f6",
"sha256:ca36aabfa5e7fa9ee8f84f82c3faffb4ffe078923f935f572f70dc49154ef6a0" "sha256:34f9a04f529dc849f0e427782d6f3c6b62f7fb734d8f4859b17e5dee0855323e"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.11.5" "version": "==1.12.0"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:2538065f9f5023eae3607e78fc5d8c595c9173ec02bc92410652078617c44ac1", "sha256:055da4826f6c9158e4a61549d57a2ce449c27d44ce34ab4c96c7bb7b5c993efc",
"sha256:554231b1690c8521e05a41e50184e43d62941fdf9351e658aea894649b879985" "sha256:1f7cecfcd38c7cac17b5386014eb04626d1c7559ee8d8ec1526058cd23f6d1d4"
], ],
"version": "==1.14.15" "version": "==1.15.0"
}, },
"celery": { "celery": {
"hashes": [ "hashes": [
@ -164,11 +171,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:1226168be1b1c7efd0e66ee79b0e0b58b2caa7ed87717909cd8a57bb13a7079a", "sha256:2f1ba1db8648484dd5c238fb62504777b7ad090c81c5f1fd8d5eb5ec21b5f283",
"sha256:9a4635813e2d498a3c01b10c701fe4a515d76dd290aaa792ccb65ca4ccb6b038" "sha256:c91c91a7ad6ef67a874a4f76f58ba534f9208412692a840e1d125eb5c279cb0a"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.10" "version": "==3.0.3"
}, },
"django-cors-middleware": { "django-cors-middleware": {
"hashes": [ "hashes": [
@ -195,11 +202,11 @@
}, },
"django-guardian": { "django-guardian": {
"hashes": [ "hashes": [
"sha256:8cf4efd67a863eb32beafd4335a38ffb083630f8ab2045212d27f8f9c3abe5a6", "sha256:8cacf49ebcc1e545f0a8997971eec0fe109f5ed31fc2a569a7bf5615453696e2",
"sha256:e638c9a23eeac534bb68b133975539ed8782f733ab6f35c0b23b4c39cd06b1bb" "sha256:ac81e88372fdf1795d84ba065550e739b42e9c6d07cdf201cf5bbf9efa7f396c"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.1.0" "version": "==2.2.0"
}, },
"django-model-utils": { "django-model-utils": {
"hashes": [ "hashes": [
@ -225,27 +232,26 @@
}, },
"django-otp": { "django-otp": {
"hashes": [ "hashes": [
"sha256:1f16c2b93fe484706ff16ac6f5e64ecc73dd240318c333e0560384ba548d3837", "sha256:523a87f8d0c52ce9a9f0a5e248c59dab3e85cdda5bbcd106cf49138a8f3f3209",
"sha256:cd4975539be478417033561e9832a1a69a583189f680e92a649f412c661f90aa" "sha256:ad3206e4a6f461c8968a2366a7cccfb340b93294e604b278aa9ab8b26f1017a1"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.7.5" "version": "==0.8.1"
}, },
"django-prometheus": { "django-prometheus": {
"hashes": [ "hashes": [
"sha256:f0657d4b887309086b71b55f6aa4a95f967b35fe115128b501f95422c423b12c", "sha256:362ea45e5ee26bdba85ce978aeb370659ca6bbc0d6bac69868a055179e053bd1",
"sha256:f645016ae5270ac2025a70788cd2bd636244a0c5705b323cc086994bf828181e" "sha256:facaa677386899303ea26c45552371cc43f476e42a81c081011a49cb5564af0b"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.0.dev124" "version": "==2.1.0.dev5"
}, },
"django-recaptcha": { "django-recaptcha": {
"hashes": [ "hashes": [
"sha256:3b19b9d972ca802b683eed5fd51ed84b0798f2a52f8d057a912eb7d99cff3779", "sha256:567784963fd5400feaf92e8951d8dbbbdb4b4c48a76e225d4baa63a2c9d2cd8c"
"sha256:b6ce959cd7c0af7501698fab5f52ca1bcb6d9402cb3b8b42a4c4cb13d89cfbfa"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.5" "version": "==2.0.6"
}, },
"django-redis": { "django-redis": {
"hashes": [ "hashes": [
@ -264,11 +270,11 @@
}, },
"django-storages": { "django-storages": {
"hashes": [ "hashes": [
"sha256:0a9b7e620e969fb0797523695329ed223bf540bbfdf6cd163b061fc11dab2d1c", "sha256:3103991c2ee8cef8a2ff096709973ffe7106183d211a79f22cf855f33533d924",
"sha256:9322ab74ba6371e2e0fccc350c741686ade829e43085597b26b07ae8955a0a00" "sha256:a59e9923cbce7068792f75344ed7727021ee4ac20f227cf17297d0d03d141e91"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.8" "version": "==1.9.1"
}, },
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
@ -295,11 +301,11 @@
}, },
"drf-yasg": { "drf-yasg": {
"hashes": [ "hashes": [
"sha256:4cfec631880ae527a91ec7cd3241aea2f82189f59e2f089119aa687761afb227", "sha256:5572e9d5baab9f6b49318169df9789f7399d0e3c7bdac8fdb8dfccf1d5d2b1ca",
"sha256:504cce09035cf1bace63b84d9d778b772f86bb37d8a71ed6f723346362e633b2" "sha256:7d7af27ad16e18507e9392b2afd6b218fbffc432ec8dbea053099a2241e184ff"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.0" "version": "==1.17.1"
}, },
"eight": { "eight": {
"hashes": [ "hashes": [
@ -338,6 +344,7 @@
"sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484", "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484",
"sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668" "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668"
], ],
"index": "pypi",
"version": "==3.0.0a1" "version": "==3.0.0a1"
}, },
"jmespath": { "jmespath": {
@ -372,35 +379,36 @@
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
"sha256:00ac0d64949fef6b3693813fe636a2d56d97a5a49b5bbb86e4cc4cc50ebc9ea2", "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd",
"sha256:0571e607558665ed42e450d7bf0e2941d542c18e117b1ebbf0ba72f287ad841c", "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c",
"sha256:0e3f04a7615fdac0be5e18b2406529521d6dbdb0167d2a690ee328bef7807487", "sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081",
"sha256:13cf89be53348d1c17b453867da68704802966c433b2bb4fa1f970daadd2ef70", "sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f",
"sha256:217262fcf6a4c2e1c7cb1efa08bd9ebc432502abc6c255c4abab611e8be0d14d", "sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261",
"sha256:223e544828f1955daaf4cefbb4853bc416b2ec3fd56d4f4204a8b17007c21250", "sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a",
"sha256:277cb61fede2f95b9c61912fefb3d43fbd5f18bf18a14fae4911b67984486f5d", "sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9",
"sha256:3213f753e8ae86c396e0e066866e64c6b04618e85c723b32ecb0909885211f74", "sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a",
"sha256:4690984a4dee1033da0af6df0b7a6bde83f74e1c0c870623797cec77964de34d", "sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb",
"sha256:4fcc472ef87f45c429d3b923b925704aa581f875d65bac80f8ab0c3296a63f78", "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60",
"sha256:61409bd745a265a742f2693e4600e4dbd45cc1daebe1d5fad6fcb22912d44145", "sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128",
"sha256:678f1963f755c5d9f5f6968dded7b245dd1ece8cf53c1aa9d80e6734a8c7f41d", "sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a",
"sha256:6c6d03549d4e2734133badb9ab1c05d9f0ef4bcd31d83e5d2b4747c85cfa21da", "sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717",
"sha256:6e74d5f4d6ecd6942375c52ffcd35f4318a61a02328f6f1bd79fcb4ffedf969e", "sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89",
"sha256:7b4fc7b1ecc987ca7aaf3f4f0e71bbfbd81aaabf87002558f5bc95da3a865bcd", "sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72",
"sha256:7ed386a40e172ddf44c061ad74881d8622f791d9af0b6f5be20023029129bc85", "sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8",
"sha256:8f54f0924d12c47a382c600c880770b5ebfc96c9fd94cf6f6bdc21caf6163ea7", "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3",
"sha256:ad9b81351fdc236bda538efa6879315448411a81186c836d4b80d6ca8217cdb9", "sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7",
"sha256:bbd00e21ea17f7bcc58dccd13869d68441b32899e89cf6cfa90d624a9198ce85", "sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8",
"sha256:c3c289762cc09735e2a8f8a49571d0e8b4f57ea831ea11558247b5bdea0ac4db", "sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77",
"sha256:cf4650942de5e5685ad308e22bcafbccfe37c54aa7c0e30cd620c2ee5c93d336", "sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1",
"sha256:cfcbc33c9c59c93776aa41ab02e55c288a042211708b72fdb518221cc803abc8", "sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15",
"sha256:e301055deadfedbd80cf94f2f65ff23126b232b0d1fea28f332ce58137bcdb18", "sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679",
"sha256:ebbfe24df7f7b5c6c7620702496b6419f6a9aa2fd7f005eb731cc80d7b4692b9", "sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012",
"sha256:eff69ddbf3ad86375c344339371168640951c302450c5d3e9936e98d6459db06", "sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6",
"sha256:f6ed60a62c5f1c44e789d2cf14009423cb1646b44a43e40a9cf6a21f077678a1" "sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc",
"sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.4.2" "version": "==4.5.0"
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [
@ -450,11 +458,11 @@
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb", "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73",
"sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8" "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"
], ],
"index": "pypi", "index": "pypi",
"version": "==20.0" "version": "==20.1"
}, },
"prometheus-client": { "prometheus-client": {
"hashes": [ "hashes": [
@ -522,41 +530,39 @@
}, },
"pycryptodome": { "pycryptodome": {
"hashes": [ "hashes": [
"sha256:042ae873baadd0c33b4d699a5c5b976ade3233a979d972f98ca82314632d868c", "sha256:012ca77c2105600e3c6aef43188101ac1d95052c633a4ae8fbebffab20c25f8a",
"sha256:0502876279772b1384b660ccc91563d04490d562799d8e2e06b411e2d81128a9", "sha256:05b4d865710f9a6378d3ada28195ff78e52642d3ecffe6fa9d379d870b9bf29d",
"sha256:2de33ed0a95855735d5a0fc0c39603314df9e78ee8bbf0baa9692fb46b3b8bbb", "sha256:07daddb98f98f771ba027f8f835bdb675aeb84effe41ed5221f520b267429354",
"sha256:319e568baf86620b419d53063b18c216abf924875966efdfe06891b987196a45", "sha256:09bf05a489fe10f9280a5e0163f195e7b9630cafb15f7d72fb9c8f5eb2afa84f",
"sha256:4372ec7518727172e1605c0843cdc5375d4771e447b8148c787b860260aae151", "sha256:0a8d5f2dbb4bbe830ace54286b829bfa529f0853bedaab6225fcb2e6d1f7e356",
"sha256:48821950ffb9c836858d8fa09d7840b6df52eadd387a3c5acece55cb387743f9", "sha256:1259b8ca49662b8a941177357f08147d858595c0042e63ff81e9628e925b5c9d",
"sha256:4b9533d4166ca07abdd49ce9d516666b1df944997fe135d4b21ac376aa624aff", "sha256:238d8b6dd27bd1a04816a68aa90a739e6dd23b192fcd83b50f9360958bff192a",
"sha256:54456cf85130e01674d21fb1ab89ffccacb138a8ade88d72fa2b0ac898d2798b", "sha256:2a57daef18a2022a5e4b6f7376c9ddd0c2d946e4b1f1e59b837f5bf295be7380",
"sha256:56fdd0e425f1b8fd3a00b6d96351f86226674974814c50534864d0124d48871f", "sha256:39e5ca2f66d1eac7abcba5ce1a03370d123dc6085620f1cd532dfee27e650178",
"sha256:57b1b707363490c495ad0eeb38bd1b0e1697c497af25fad78d3a1ebf0477fd5b", "sha256:3d516df693c195b8da3795e381429bd420e87081b7e6c2871c62c9897c812cda",
"sha256:5c485ed6e9718ebcaa81138fa70ace9c563d202b56a8cee119b4085b023931f5", "sha256:3e486c5b7228e864665fc479e9f596b2547b5fe29c6f5c8ed3807784d06faed7",
"sha256:63c103a22cbe9752f6ea9f1a0de129995bad91c4d03a66c67cffcf6ee0c9f1e1", "sha256:5029c46b0d41dfb763c3981c0af68eab029f06fe2b94f2299112fc18cf9e8d6d",
"sha256:68fab8455efcbfe87c5d75015476f9b606227ffe244d57bfd66269451706e899", "sha256:5817c0b3c263025d851da96b90cbc7e95348008f88b990e90d10683dba376666",
"sha256:6c2720696b10ae356040e888bde1239b8957fe18885ccf5e7b4e8dec882f0856", "sha256:79320f1fc5c9ca682869087c565bb29ca6f334692e940d7365771e9a94382e12",
"sha256:72166c2ac520a5dbd2d90208b9c279161ec0861662a621892bd52fb6ca13ab91", "sha256:887d08beca6368d3d70dc75126607ad76317a9fd07fe61323d8c3cb42add12b6",
"sha256:7c52308ac5b834331b2f107a490b2c27de024a229b61df4cdc5c131d563dfe98", "sha256:9163fec630495c10c767991e3f8dab32f4427bfb2dfeaa59bb28fe3e52ba66f2",
"sha256:87d8d85b4792ca5e730fb7a519fbc3ed976c59dcf79c5204589c59afd56b9926", "sha256:95d324e603c5cec5d89e8595236bbf59ade5fe3a72d100ce61eebb323d598750",
"sha256:896e9b6fd0762aa07b203c993fbbee7a1f1a4674c6886afd7bfa86f3d1be98a8", "sha256:9927aa8a8cb4af681279b6f28a1dcb14e0eb556c1aea8413a1e27608a8516e0c",
"sha256:8a799bea3c6617736e914a2e77c409f52893d382f619f088f8a80e2e21f573c1", "sha256:9948c2d5c5c0ee45ed44cee0e2eba2ce60a03be006ed3074521f3da3be162e72",
"sha256:9d9945ac8375d5d8e60bd2a2e1df5882eaa315522eedf3ca868b1546dfa34eba", "sha256:a719bd708207fa219fcbf4c8ebbcbc52846045f78179d00445b429fdabdbc1c4",
"sha256:9ef966c727de942de3e41aa8462c4b7b4bca70f19af5a3f99e31376589c11aac", "sha256:bc22ced26ebc46546798fa0141f4418f1db116dec517f0aeaecec87cf7b2416c",
"sha256:a168e73879619b467072509a223282a02c8047d932a48b74fbd498f27224aa04", "sha256:c41b7e10b72cef00cd63410f31fe50e72dc3a40eafbd146e288384fbe4208064",
"sha256:a30f501bbb32e01a49ef9e09ca1260e5ab49bf33a257080ec553e08997acc487", "sha256:cdb0ad83a5d6bac986a37fcb7562bcbef0aabae8ea19505bab5cf83c4d18af12",
"sha256:a8ca2450394d3699c9f15ef25e8de9a24b401933716a1e39d37fa01f5fe3c58b", "sha256:d8e480f65ac7105cbc288eec2417dc61eaac6ed6e75595aa15b8c7c77c53a68b",
"sha256:aec4d42deb836b8fb3ba32f2ba1ef0d33dd3dc9d430b1479ee7a914490d15b5e", "sha256:da2d581da279bc7408d38e16ff77754f5448c4352f2acfe530a5d14d8fc6934a",
"sha256:b4af098f2a50f8d048ab12cabb59456585c0acf43d90ee79782d2d6d0ed59dba", "sha256:de61091dd68326b600422cf731eb4810c4c6363f18a65bccd6061784b7454f5b",
"sha256:b55c60c321ac91945c60a40ac9896ac7a3d432bb3e8c14006dfd82ad5871c331", "sha256:ec7d39589f9cfc2a8b83b1d2fc673441757c99d43283e97b2dd46e0e23730db8",
"sha256:c53348358408d94869059e16fba5ff3bef8c52c25b18421472aba272b9bb450f", "sha256:f3204006869ab037604b1d9f045c4e84882ddd365e4ee8caa5eb1ff47a59188e",
"sha256:cbfd97f9e060f0d30245cd29fa267a9a84de9da97559366fca0a3f7655acc63f", "sha256:f4d2174e168d0eabd1fffaf88b4f62c2b6f30a67b8816f31024b8e48be3e2d75",
"sha256:d3fe3f33ad52bf0c19ee6344b695ba44ffbfa16f3c29ca61116b48d97bd970fb", "sha256:fcff8c9d88d58880f7eda2139c7c444552a38f98a9e77ba5970b6e78f54ac358"
"sha256:e3a79a30d15d9c7c284a7734036ee8abdb5ca3a6f5774d293cdc9e1358c1dc10",
"sha256:eec0689509389f19875f66ae8dedd59f982240cdab31b9f78a8dc266011df93a"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.9.4" "version": "==3.9.6"
}, },
"pycryptodomex": { "pycryptodomex": {
"hashes": [ "hashes": [
@ -714,10 +720,10 @@
}, },
"ruamel.yaml": { "ruamel.yaml": {
"hashes": [ "hashes": [
"sha256:ee3264b83c3309b4ae7978afa185da6a1d278e3abc9fb942f1a0b57c622092f8", "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b",
"sha256:fd16843ff0ba45fa5e1ea9ea7038428b4a46f2c39deea9aa67f9eaa34823dc11" "sha256:099c644a778bf72ffa00524f78dd0b6476bca94a1da344130f4bf3381ce5b954"
], ],
"version": "==0.16.9" "version": "==0.16.10"
}, },
"ruamel.yaml.clib": { "ruamel.yaml.clib": {
"hashes": [ "hashes": [
@ -753,11 +759,11 @@
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
"sha256:8e2d38dc58dc992280487e553ec3d97a424e4d179f4fad802ef3b08f64ccf4d8", "sha256:b06dd27391fd11fb32f84fe054e6a64736c469514a718a99fb5ce1dff95d6b28",
"sha256:9b59e155229ea7d46a52b5c025d8c3c6d591e9dd9bb5f5f47310b2bb430038a8" "sha256:e023da07cfbead3868e1e2ba994160517885a32dfd994fc455b118e37989479b"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.14.0" "version": "==0.14.1"
}, },
"service-identity": { "service-identity": {
"hashes": [ "hashes": [
@ -791,11 +797,11 @@
}, },
"structlog": { "structlog": {
"hashes": [ "hashes": [
"sha256:4287058cf4ce1a59bc5dea290d6386d37f29a37529c9a51cdf7387e51710152b", "sha256:7a48375db6274ed1d0ae6123c486472aa1d0890b08d314d2b016f3aa7f35990b",
"sha256:6640e6690fc31d5949bc614c1a630464d3aaa625284aeb7c6e486c3010d73e12" "sha256:8a672be150547a93d90a7d74229a29e765be05bd156a35cdcc527ebf68e9af92"
], ],
"index": "pypi", "index": "pypi",
"version": "==19.2.0" "version": "==20.1.0"
}, },
"swagger-spec-validator": { "swagger-spec-validator": {
"hashes": [ "hashes": [
@ -817,12 +823,12 @@
"secure" "secure"
], ],
"hashes": [ "hashes": [
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
], ],
"index": "pypi", "index": "pypi",
"markers": null, "markers": null,
"version": "==1.25.7" "version": "==1.25.8"
}, },
"vine": { "vine": {
"hashes": [ "hashes": [
@ -863,10 +869,10 @@
}, },
"autopep8": { "autopep8": {
"hashes": [ "hashes": [
"sha256:4d8eec30cc81bc5617dbf1218201d770dc35629363547f17577c61683ccfb3ee" "sha256:0f592a0447acea0c2b0a9602be1e4e3d86db52badd2e3c84f0193bfd89fd3a43"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.4.4" "version": "==1.5"
}, },
"bandit": { "bandit": {
"hashes": [ "hashes": [
@ -946,40 +952,33 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:1226168be1b1c7efd0e66ee79b0e0b58b2caa7ed87717909cd8a57bb13a7079a", "sha256:2f1ba1db8648484dd5c238fb62504777b7ad090c81c5f1fd8d5eb5ec21b5f283",
"sha256:9a4635813e2d498a3c01b10c701fe4a515d76dd290aaa792ccb65ca4ccb6b038" "sha256:c91c91a7ad6ef67a874a4f76f58ba534f9208412692a840e1d125eb5c279cb0a"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.10" "version": "==3.0.3"
}, },
"django-debug-toolbar": { "django-debug-toolbar": {
"hashes": [ "hashes": [
"sha256:24c157bc6c0e1648e0a6587511ecb1b007a00a354ce716950bff2de12693e7a8", "sha256:eabbefe89881bbe4ca7c980ff102e3c35c8e8ad6eb725041f538988f2f39a943",
"sha256:77cfba1d6e91b9bc3d36dc7dc74a9bb80be351948db5f880f2562a0cbf20b6c5" "sha256:ff94725e7aae74b133d0599b9bf89bd4eb8f5d2c964106e61d11750228c8774c"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.1" "version": "==2.2"
},
"dodgy": {
"hashes": [
"sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a",
"sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"
],
"version": "==0.2.1"
}, },
"gitdb2": { "gitdb2": {
"hashes": [ "hashes": [
"sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", "sha256:0375d983fd887d03c8942e81b1b0abc6c320cfb500cd3fe0d9c0eac87fbf2b52",
"sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b" "sha256:b2b3a67090c17dc61f8407ca485e79ae811225ab5ebcd98ac5ee01448e8987b5"
], ],
"version": "==2.0.6" "version": "==3.0.2"
}, },
"gitpython": { "gitpython": {
"hashes": [ "hashes": [
"sha256:99c77677f31f255e130f3fed4c8e0eebb35f1a09df98ff965fff6774f71688cf", "sha256:620b3c729bbc143b498cfea77e302999deedc55faec5b1067086c9ef90e101bc",
"sha256:99cd0403cecd8a13b95d2e045b9fcaa7837137fcc5ec3105f2c413305d82c143" "sha256:a43a5d88a5bbc3cf32bb5223e4b4e68fd716db5e9996cad6e561bbfee6e5f4af"
], ],
"version": "==3.0.7" "version": "==3.0.8"
}, },
"isort": { "isort": {
"hashes": [ "hashes": [
@ -1035,40 +1034,12 @@
], ],
"version": "==5.4.4" "version": "==5.4.4"
}, },
"pep8-naming": {
"hashes": [
"sha256:1b419fa45b68b61cd8c5daf4e0c96d28915ad14d3d5f35fcc1e7e95324a33a2e",
"sha256:4eedfd4c4b05e48796f74f5d8628c068ff788b9c2b08471ad408007fc6450e5a"
],
"version": "==0.4.1"
},
"prospector": {
"hashes": [
"sha256:ea910794b53cfefcb5dfb6b4eb0323e42d1a88132e165b85b016cc7f0b6ae635"
],
"index": "pypi",
"version": "==1.2.0"
},
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
"sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
], ],
"version": "==2.4.0" "version": "==2.5.0"
},
"pydocstyle": {
"hashes": [
"sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586",
"sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"
],
"version": "==5.0.2"
},
"pyflakes": {
"hashes": [
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
],
"version": "==2.1.1"
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
@ -1078,25 +1049,13 @@
"index": "pypi", "index": "pypi",
"version": "==2.4.4" "version": "==2.4.4"
}, },
"pylint-celery": {
"hashes": [
"sha256:41e32094e7408d15c044178ea828dd524beedbdbe6f83f712c5e35bde1de4beb"
],
"version": "==0.3"
},
"pylint-django": { "pylint-django": {
"hashes": [ "hashes": [
"sha256:9bdb0e022b19881218a25ffb8ad05e83b83bc5cdbc58e5ee8ffbe99965193f6c", "sha256:440beb814464928aedd2e21196bb6e47a83b63e2cbe886a701ba0f4a64206bbb",
"sha256:9eea6a026eaa5ecfad5fed7a33faf77ef55a43cc78afbcaf2f6ddd071156b3f8" "sha256:d5d113605a64cf0e638b707d4cb42106e626f8851bc30a44d5b22bd698ad8483"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.12" "version": "==2.0.13"
},
"pylint-flask": {
"hashes": [
"sha256:f4d97de2216bf7bfce07c9c08b166e978fe9f2725de2a50a9845a97de7e31517"
],
"version": "==0.6"
}, },
"pylint-plugin-utils": { "pylint-plugin-utils": {
"hashes": [ "hashes": [
@ -1155,18 +1114,6 @@
], ],
"version": "==2020.1.8" "version": "==2020.1.8"
}, },
"requirements-detector": {
"hashes": [
"sha256:9fbc4b24e8b7c3663aff32e3eba34596848c6b91bd425079b386973bd8d08931"
],
"version": "==0.6"
},
"setoptconf": {
"hashes": [
"sha256:5b0b5d8e0077713f5d5152d4f63be6f048d9a1bb66be15d089a11c898c3cf49c"
],
"version": "==0.2.0"
},
"six": { "six": {
"hashes": [ "hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
@ -1181,13 +1128,6 @@
], ],
"version": "==2.0.5" "version": "==2.0.5"
}, },
"snowballstemmer": {
"hashes": [
"sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
"sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
],
"version": "==2.0.0"
},
"sqlparse": { "sqlparse": {
"hashes": [ "hashes": [
"sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
@ -1237,11 +1177,11 @@
}, },
"unittest-xml-reporting": { "unittest-xml-reporting": {
"hashes": [ "hashes": [
"sha256:358bbdaf24a26d904cc1c26ef3078bca7fc81541e0a54c8961693cc96a6f35e0", "sha256:6584562cde8226fc79fa29e38903c669a02799074a563bb0b70fcd3a8e87829c",
"sha256:9d28ddf6524cf0ff9293f61bd12e792de298f8561a5c945acea63fb437789e0e" "sha256:dd8046a64dc62f3d30301523a54992e0be75a945194491e0a3b718130cb429e0"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.5.2" "version": "==3.0.1"
}, },
"wrapt": { "wrapt": {
"hashes": [ "hashes": [

View File

@ -0,0 +1,32 @@
# Amazon Web Services Integration
## What is AWS
!!! note ""
Amazon Web Services (AWS) is the worlds most comprehensive and broadly adopted cloud platform, offering over 175 fully featured services from data centers globally. Millions of customers—including the fastest-growing startups, largest enterprises, and leading government agencies—are using AWS to lower costs, become more agile, and innovate faster.
## Preparation
The following placeholders will be used:
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
- ACS URL: `https://signin.aws.amazon.com/saml`
- Audience: `urn:amazon:webservices`
- Issuer: `passbook`
You can of course use a custom Signing Certificate, and adjust durations.
## AWS
Create a Role with the Permissions you desire, and note the ARN.
AWS requires two custom PropertyMappings; `Role` and `RoleSessionName`. Create them as following:
![](./property-mapping-role.png)
![](./property-mapping-role-session-name.png)
Afterwards export the Metadata from passbook, and create an Identity Provider [here](https://console.aws.amazon.com/iam/home#/providers).

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -4,9 +4,8 @@
From https://about.gitlab.com/what-is-gitlab/ From https://about.gitlab.com/what-is-gitlab/
``` !!! note ""
GitLab is a complete DevOps platform, delivered as a single application. This makes GitLab unique and makes Concurrent DevOps possible, unlocking your organization from the constraints of a pieced together toolchain. Join us for a live Q&A to learn how GitLab can give you unmatched visibility and higher levels of efficiency in a single application across the DevOps lifecycle. GitLab is a complete DevOps platform, delivered as a single application. This makes GitLab unique and makes Concurrent DevOps possible, unlocking your organization from the constraints of a pieced together toolchain. Join us for a live Q&A to learn how GitLab can give you unmatched visibility and higher levels of efficiency in a single application across the DevOps lifecycle.
```
## Preparation ## Preparation
@ -21,7 +20,7 @@ Create an application in passbook and note the slug, as this will be used later.
- Audience: `https://gitlab.company` - Audience: `https://gitlab.company`
- Issuer: `https://gitlab.company` - Issuer: `https://gitlab.company`
You can of course use a custom Signing Certificate, and adjust the Assertion Length. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php). You can of course use a custom Signing Certificate, and adjust durations. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
## GitLab Configuration ## GitLab Configuration

View File

@ -4,9 +4,8 @@
From https://goharbor.io From https://goharbor.io
``` !!! note ""
Harbor is an open source container image registry that secures images with role-based access control, scans images for vulnerabilities, and signs images as trusted. A CNCF Incubating project, Harbor delivers compliance, performance, and interoperability to help you consistently and securely manage images across cloud native compute platforms like Kubernetes and Docker. Harbor is an open source container image registry that secures images with role-based access control, scans images for vulnerabilities, and signs images as trusted. A CNCF Incubating project, Harbor delivers compliance, performance, and interoperability to help you consistently and securely manage images across cloud native compute platforms like Kubernetes and Docker.
```
## Preparation ## Preparation

View File

@ -4,10 +4,9 @@
From https://rancher.com/products/rancher From https://rancher.com/products/rancher
``` !!! note ""
An Enterprise Platform for Managing Kubernetes Everywhere An Enterprise Platform for Managing Kubernetes Everywhere
Rancher is a platform built to address the needs of the DevOps teams deploying applications with Kubernetes, and the IT staff responsible for delivering an enterprise-critical service. Rancher is a platform built to address the needs of the DevOps teams deploying applications with Kubernetes, and the IT staff responsible for delivering an enterprise-critical service.
```
## Preparation ## Preparation
@ -22,7 +21,7 @@ Create an application in passbook and note the slug, as this will be used later.
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata` - Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
- Issuer: `passbook` - Issuer: `passbook`
You can of course use a custom Signing Certificate, and adjust the Assertion Length. You can of course use a custom Signing Certificate, and adjust durations.
## Rancher ## Rancher

View File

@ -4,13 +4,12 @@
From https://sentry.io From https://sentry.io
``` !!! note ""
Sentry provides self-hosted and cloud-based error monitoring that helps all software Sentry provides self-hosted and cloud-based error monitoring that helps all software
teams discover, triage, and prioritize errors in real-time. teams discover, triage, and prioritize errors in real-time.
One million developers at over fifty thousand companies already ship One million developers at over fifty thousand companies already ship
better software faster with Sentry. Wont you join them? better software faster with Sentry. Wont you join them?
```
## Preparation ## Preparation

View File

@ -0,0 +1,74 @@
# Ansible Tower / AWX Integration
## What is Tower
From https://docs.ansible.com/ansible/2.5/reference_appendices/tower.html
!!! note ""
Ansible Tower (formerly AWX) is a web-based solution that makes Ansible even more easy to use for IT teams of all kinds. Its designed to be the hub for all of your automation tasks.
Tower allows you to control access to who can access what, even allowing sharing of SSH credentials without someone being able to transfer those credentials. Inventory can be graphically managed or synced with a wide variety of cloud sources. It logs all of your jobs, integrates well with LDAP, and has an amazing browsable REST API. Command line tools are available for easy integration with Jenkins as well. Provisioning callbacks provide great support for autoscaling topologies.
!!! note
AWX is the Open-Source version of Tower, and AWX will be used interchangeably throughout this document.
## Preparation
The following placeholders will be used:
- `awx.company` is the FQDN of the AWX/Tower Install
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
- ACS URL: `https://awx.company/sso/complete/saml/`
- Audience: `awx`
- Issuer: `https://awx.company/sso/metadata/saml/`
You can of course use a custom Signing Certificate, and adjust durations.
## AWX Configuration
Navigate to `https://awx.company/#/settings/auth` to configure SAML. Set the Field `SAML SERVICE PROVIDER ENTITY ID` to `awx`.
For the fields `SAML SERVICE PROVIDER PUBLIC CERTIFICATE` and `SAML SERVICE PROVIDER PRIVATE KEY`, you can either use custom Certificates, or use the self-signed Pair generated by Passbook.
Provide Metadata in the `SAML Service Provider Organization Info` Field:
```json
{
"en-US": {
"name": "passbook",
"url": "https://passbook.company",
"displayname": "passbook"
}
}
```
Provide Metadata in the `SAML Service Provider Technical Contact` and `SAML Service Provider Technical Contact` Fields:
```json
{
"givenName": "Admin Name",
"emailAddress": "admin@company"
}
```
In the `SAML Enabled Identity Providers` paste the following configuration:
```json
{
"passbook": {
"attr_username": "urn:oid:2.16.840.1.113730.3.1.241",
"attr_user_permanent_id": "urn:oid:0.9.2342.19200300.100.1.1",
"x509cert": "MIIDEjCCAfqgAwIBAgIRAJZ9pOZ1g0xjiHtQAAejsMEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlcGFzc2Jvb2sgU2VsZi1zaWduZWQgU0FNTCBDZXJ0aWZpY2F0ZTAeFw0xOTEyMjYyMDEwNDFaFw0yMDEyMjYyMDEwNDFaMFkxLjAsBgNVBAMMJXBhc3Nib29rIFNlbGYtc2lnbmVkIFNBTUwgQ2VydGlmaWNhdGUxETAPBgNVBAoMCHBhc3Nib29rMRQwEgYDVQQLDAtTZWxmLXNpZ25lZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO/ktBYZkY9xAijF4acvzX6Q1K8KoIZeyde8fVgcWBz4L5FgDQ4/dni4k2YAcPdwteGL4nKVzetUzjbRCBUNuO6lqU4J4WNNX4Xg4Ir7XLRoAQeo+omTPBdpJ1p02HjtN5jT01umN3bK2yto1e37CJhK6WJiaXqRewPxh4lI4aqdj3BhFkJ3I3r2qxaWOAXQ6X7fg3w/ny7QP53//ouZo7hSLY3GIcRKgvdjjVM3OW5C3WLpOq5Dez5GWVJ17aeFCfGQ8bwFKde6qfYqyGcU9xHB36TtVHB9hSFP/tUFhkiSOxtsrYwCgCyXm4UTSpP+wiNyjKfFw7qGLBvA2hGTNw8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh9PeAqPRQk1/SSygIFADZBi08O/DPCshFwEHvJATIcTzcDD8UGAjXh+H5OlkDyX7KyrcaNvYaafCUo63A+WprdtdY5Ty6SBEwTYyiQyQfwM9BfK+imCoif1Ai7xAelD7p9lNazWq7JU+H/Ep7U7Q7LvpxAbK0JArt+IWTb2NcMb3OWE1r0gFbs44O1l6W9UbJTbyLMzbGbe5i+NHlgnwPwuhtRMh0NUYabGHKcHbhwyFhfGAQv2dAp5KF1E5gu6ZzCiFePzc0FrqXQyb2zpFYcJHXquiqaOeG7cZxRHYcjrl10Vxzki64XVA9BpdELgKSnupDGUEJsRUt3WVOmvZuA==",
"url": "https://passbook.company/application/saml/awx/login/",
"attr_last_name": "User.LastName",
"entity_id": "https://awx.company/sso/metadata/saml/",
"attr_email": "urn:oid:0.9.2342.19200300.100.1.3",
"attr_first_name": "urn:oid:2.5.4.3"
}
}
```
`x509cert` is the Certificate configured in passbook. Remove the --BEGIN CERTIFICATE-- and --END CERTIFICATE-- headers, then enter the cert as one non-breaking string.

View File

@ -0,0 +1,19 @@
# Expression Policy
Expression Policies allows you to write custom Policy Logic using Jinja2 Templating language.
For a language reference, see [here](https://jinja.palletsprojects.com/en/2.11.x/templates/).
The following objects are passed into the variable:
- `request`: A PolicyRequest object, which has the following properties:
- `request.user`: The current User, which the Policy is applied against. ([ref](../../property-mappings/reference/user-object.md))
- `request.http_request`: The Django HTTP Request, as documented [here](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects).
- `request.obj`: A Django Model instance. This is only set if the Policy is ran against an object.
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external Provider.
- `pb_is_group_member(user, group_name)`: Function which checks if `user` is member of a Group with Name `gorup_name`.
There are also the following custom filters available:
- `regex_match(regex)`: Return True if value matches `regex`
- `regex_replace(regex, repl)`: Replace string matched by `regex` with `repl`

View File

@ -18,27 +18,9 @@ passbook keeps track of failed login attempts by Source IP and Attempted Usernam
This policy can be used to for example prompt Clients with a low score to pass a Captcha before they can continue. This policy can be used to for example prompt Clients with a low score to pass a Captcha before they can continue.
### Field matcher Policy ## Expression Policy
This policy allows you to evaluate arbitrary comparisons against the User instance. Currently supported fields are: See [Expression Policy](expression/index.md).
- Username
- E-Mail
- Name
- Is_active
- Date joined
Any of the following operations are supported:
- Starts with
- Ends with
- Contains
- Regexp (standard Python engine)
- Exact
### SSO Policy
This policy evaluates to True if the current Authentication Flow has been initiated through an external Source, like OAuth and SAML.
### Webhook Policy ### Webhook Policy

View File

@ -0,0 +1,20 @@
# Passbook User Object
The User object has the following attributes:
- `username`: User's Username
- `email` User's E-Mail
- `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
## Examples
List all the User's Group Names
```jinja2
[{% for group in user.groups.all() %}'{{ group.name }}',{% endfor %}]
```

View File

@ -13,11 +13,5 @@ The API exposes Username, E-Mail, Name and Groups in a GitHub-compatible format.
## SAML Provider ## SAML Provider
This provider allows you to integrate Enterprise Software using the SAML2 Protocol. It supports signed Requests. This Provider also has [Property Mappings](property-mappings.md#saml-property-mapping), which allows you to expose Vendor-specific Fields. This provider allows you to integrate Enterprise Software using the SAML2 Protocol. It supports signed Requests. This Provider uses [Property Mappings](property-mappings/index.md#saml-property-mapping) to determine which fields are exposed and what values they return. This makes it possible to expose Vendor-specific Fields.
Default fields are: Default fields are exposed through Auto-generated Property Mappings, which are prefixed with "Autogenerated..."
- `eduPersonPrincipalName`: User's E-Mail
- `cn`: User's Full Name
- `mail`: User's E-Mail
- `displayName`: User's Username
- `uid`: User Unique Identifier

View File

@ -36,4 +36,4 @@ This source allows you to import Users and Groups from an LDAP Server
- Object uniqueness field: Field which contains a unique Identifier. - Object uniqueness field: Field which contains a unique Identifier.
- Sync groups: Enable/disable Group synchronization. Groups are synced in the background every 5 minutes. - Sync groups: Enable/disable Group synchronization. Groups are synced in the background every 5 minutes.
- Sync parent group: Optionally set this Group as parent Group for all synced Groups (allows you to, for example, import AD Groups under a root `imported-from-ad` group.) - Sync parent group: Optionally set this Group as parent Group for all synced Groups (allows you to, for example, import AD Groups under a root `imported-from-ad` group.)
- Property mappings: Define which LDAP Properties map to which passbook Properties. The default set of Property Mappings is generated for Active Directory. See also [LDAP Property Mappings](property-mappings.md#ldap-property-mapping) - Property mappings: Define which LDAP Properties map to which passbook Properties. The default set of Property Mappings is generated for Active Directory. See also [LDAP Property Mappings](property-mappings/index.md#ldap-property-mapping)

View File

@ -1,6 +1,6 @@
apiVersion: v1 apiVersion: v1
appVersion: "0.7.14-beta" appVersion: "0.8.3-beta"
description: A Helm chart for passbook. description: A Helm chart for passbook.
name: passbook name: passbook
version: "0.7.14-beta" version: "0.8.3-beta"
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png

View File

@ -2,7 +2,7 @@
# This is a YAML-formatted file. # This is a YAML-formatted file.
# Declare variables to be passed into your templates. # Declare variables to be passed into your templates.
image: image:
tag: 0.7.14-beta tag: 0.8.3-beta
nameOverride: "" nameOverride: ""
@ -42,3 +42,5 @@ redis:
master: master:
persistence: persistence:
enabled: false enabled: false
# https://stackoverflow.com/a/59189742
disableCommands: []

View File

@ -10,15 +10,22 @@ nav:
- Kubernetes: installation/kubernetes.md - Kubernetes: installation/kubernetes.md
- Sources: sources.md - Sources: sources.md
- Providers: providers.md - Providers: providers.md
- Property Mappings: property-mappings.md - Property Mappings:
- Overview: property-mappings/index.md
- Reference:
- User Object: property-mappings/reference/user-object.md
- Factors: factors.md - Factors: factors.md
- Policies: policies.md - Policies:
- Overview: policies/index.md
- Expression: policies/expression/index.md
- Integrations: - Integrations:
- as Provider: - as Provider:
- Amazon Web Services: integrations/services/aws/index.md
- GitLab: integrations/services/gitlab/index.md - GitLab: integrations/services/gitlab/index.md
- Rancher: integrations/services/rancher/index.md - Rancher: integrations/services/rancher/index.md
- Harbor: integrations/services/harbor/index.md - Harbor: integrations/services/harbor/index.md
- Sentry: integrations/services/sentry/index.md - Sentry: integrations/services/sentry/index.md
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
repo_name: "BeryJu.org/passbook" repo_name: "BeryJu.org/passbook"
repo_url: https://github.com/BeryJu/passbook repo_url: https://github.com/BeryJu/passbook
@ -29,3 +36,4 @@ theme:
markdown_extensions: markdown_extensions:
- toc: - toc:
permalink: "¶" permalink: "¶"
- admonition

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = "0.7.14-beta" __version__ = "0.8.3-beta"

View File

@ -36,7 +36,7 @@
<tr> <tr>
<td>{{ source.name }}</td> <td>{{ source.name }}</td>
<td>{{ source|fieldtype }}</td> <td>{{ source|fieldtype }}</td>
<td>{{ source.additional_info|safe }}</td> <td>{{ source.ui_additional_info|safe|default:"" }}</td>
<td> <td>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

View File

@ -1,4 +1,4 @@
{% extends "generic/form.html" %} {% extends base_template|default:"generic/form.html" %}
{% load utils %} {% load utils %}
{% load i18n %} {% load i18n %}

View File

@ -20,6 +20,7 @@
<link rel="stylesheet" href="{% static 'codemirror/lib/codemirror.css' %}"> <link rel="stylesheet" href="{% static 'codemirror/lib/codemirror.css' %}">
<link rel="stylesheet" href="{% static 'codemirror/theme/monokai.css' %}"> <link rel="stylesheet" href="{% static 'codemirror/theme/monokai.css' %}">
<script src="{% static 'codemirror/mode/yaml/yaml.js' %}"></script> <script src="{% static 'codemirror/mode/yaml/yaml.js' %}"></script>
<script src="{% static 'codemirror/mode/jinja2/jinja2.js' %}"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -29,21 +30,33 @@
<div class=""> <div class="">
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% include 'partials/form.html' with form=form %} {% include 'partials/form.html' with form=form %}
{% block beneath_form %}
{% endblock %}
<a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a> <a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a>
<input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" /> <input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" />
</form> </form>
</div> </div>
{% block beneath_form %}
{% endblock %}
<script> <script>
let attributes = document.getElementsByName('attributes'); const attributes = document.getElementsByName('attributes');
if (attributes.length > 0) { if (attributes.length > 0) {
let myCodeMirror = CodeMirror.fromTextArea(attributes[0], { // https://github.com/codemirror/CodeMirror/issues/5092
attributes[0].removeAttribute("required");
const attributesCM = CodeMirror.fromTextArea(attributes[0], {
mode: 'yaml', mode: 'yaml',
theme: 'monokai', theme: 'monokai',
lineNumbers: true, lineNumbers: true,
}); });
} }
const expressions = document.getElementsByName('expression');
if (expressions.length > 0) {
// https://github.com/codemirror/CodeMirror/issues/5092
expressions[0].removeAttribute("required");
const expressionCM = CodeMirror.fromTextArea(expressions[0], {
mode: 'jinja2',
theme: 'monokai',
lineNumbers: true,
});
}
</script> </script>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "generic/form.html" %} {% extends base_template|default:"generic/form.html" %}
{% load utils %} {% load utils %}
{% load i18n %} {% load i18n %}

View File

@ -18,7 +18,7 @@ def get_links(model_instance):
links = {} links = {}
if not isinstance(model_instance, Model): if not isinstance(model_instance, Model):
LOGGER.warning("Model %s is not instance of Model", model_instance) LOGGER.warning("Model is not instance of Model", model_instance=model_instance)
return links return links
try: try:
@ -43,7 +43,7 @@ def get_htmls(context, model_instance):
htmls = [] htmls = []
if not isinstance(model_instance, Model): if not isinstance(model_instance, Model):
LOGGER.warning("Model %s is not instance of Model", model_instance) LOGGER.warning("Model is not instance of Model", model_instance=model_instance)
return htmls return htmls
try: try:

View File

@ -52,6 +52,13 @@ class PolicyCreateView(
success_url = reverse_lazy("passbook_admin:policies") success_url = reverse_lazy("passbook_admin:policies")
success_message = _("Successfully created Policy") success_message = _("Successfully created Policy")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self): def get_form_class(self):
policy_type = self.request.GET.get("type") policy_type = self.request.GET.get("type")
model = next(x for x in Policy.__subclasses__() if x.__name__ == policy_type) model = next(x for x in Policy.__subclasses__() if x.__name__ == policy_type)
@ -72,6 +79,13 @@ class PolicyUpdateView(
success_url = reverse_lazy("passbook_admin:policies") success_url = reverse_lazy("passbook_admin:policies")
success_message = _("Successfully updated Policy") success_message = _("Successfully updated Policy")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
form_class = path_to_class(form_class_path) form_class = path_to_class(form_class_path)

View File

@ -66,6 +66,9 @@ class PropertyMappingCreateView(
if x.__name__ == property_mapping_type if x.__name__ == property_mapping_type
) )
kwargs["type"] = model._meta.verbose_name kwargs["type"] = model._meta.verbose_name
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs return kwargs
def get_form_class(self): def get_form_class(self):
@ -92,6 +95,13 @@ class PropertyMappingUpdateView(
success_url = reverse_lazy("passbook_admin:property-mappings") success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _("Successfully updated Property Mapping") success_message = _("Successfully updated Property Mapping")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
form_class = path_to_class(form_class_path) form_class = path_to_class(form_class_path)

View File

@ -67,6 +67,8 @@ class UserDeleteView(
model = User model = User
permission_required = "passbook_core.delete_user" permission_required = "passbook_core.delete_user"
# By default the object's name is user which is used by other checks
context_object_name = "object"
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:users") success_url = reverse_lazy("passbook_admin:users")
success_message = _("Successfully deleted User") success_message = _("Successfully deleted User")

View File

@ -24,12 +24,10 @@ from passbook.factors.otp.api import OTPFactorViewSet
from passbook.factors.password.api import PasswordFactorViewSet from passbook.factors.password.api import PasswordFactorViewSet
from passbook.lib.utils.reflection import get_apps from passbook.lib.utils.reflection import get_apps
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
from passbook.policies.group.api import GroupMembershipPolicyViewSet from passbook.policies.expression.api import ExpressionPolicyViewSet
from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
from passbook.policies.matcher.api import FieldMatcherPolicyViewSet
from passbook.policies.password.api import PasswordPolicyViewSet from passbook.policies.password.api import PasswordPolicyViewSet
from passbook.policies.reputation.api import ReputationPolicyViewSet from passbook.policies.reputation.api import ReputationPolicyViewSet
from passbook.policies.sso.api import SSOLoginPolicyViewSet
from passbook.policies.webhook.api import WebhookPolicyViewSet from passbook.policies.webhook.api import WebhookPolicyViewSet
from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet
from passbook.providers.oauth.api import OAuth2ProviderViewSet from passbook.providers.oauth.api import OAuth2ProviderViewSet
@ -57,13 +55,11 @@ router.register("sources/ldap", LDAPSourceViewSet)
router.register("sources/oauth", OAuthSourceViewSet) router.register("sources/oauth", OAuthSourceViewSet)
router.register("policies/all", PolicyViewSet) router.register("policies/all", PolicyViewSet)
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet) router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
router.register("policies/groupmembership", GroupMembershipPolicyViewSet)
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet) router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
router.register("policies/fieldmatcher", FieldMatcherPolicyViewSet)
router.register("policies/password", PasswordPolicyViewSet) router.register("policies/password", PasswordPolicyViewSet)
router.register("policies/reputation", ReputationPolicyViewSet) router.register("policies/reputation", ReputationPolicyViewSet)
router.register("policies/ssologin", SSOLoginPolicyViewSet)
router.register("policies/webhook", WebhookPolicyViewSet) router.register("policies/webhook", WebhookPolicyViewSet)
router.register("policies/expression", ExpressionPolicyViewSet)
router.register("providers/all", ProviderViewSet) router.register("providers/all", ProviderViewSet)
router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet) router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet)
router.register("providers/oauth", OAuth2ProviderViewSet) router.register("providers/oauth", OAuth2ProviderViewSet)

View File

@ -100,7 +100,6 @@ class Event(UUIDModel):
app = getmodule(stack()[_inspect_offset][0]).__name__ app = getmodule(stack()[_inspect_offset][0]).__name__
cleaned_kwargs = sanitize_dict(kwargs) cleaned_kwargs = sanitize_dict(kwargs)
event = Event(action=action.value, app=app, context=cleaned_kwargs) event = Event(action=action.value, app=app, context=cleaned_kwargs)
LOGGER.debug("Created Audit event", action=action, context=cleaned_kwargs)
return event return event
def from_http( def from_http(
@ -129,6 +128,12 @@ class Event(UUIDModel):
raise ValidationError( raise ValidationError(
"you may not edit an existing %s" % self._meta.model_name "you may not edit an existing %s" % self._meta.model_name
) )
LOGGER.debug(
"Created Audit event",
action=self.action,
context=self.context,
client_ip=self.client_ip,
)
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
class Meta: class Meta:

View File

@ -15,11 +15,13 @@ class ApplicationSerializer(ModelSerializer):
"pk", "pk",
"name", "name",
"slug", "slug",
"launch_url",
"icon_url",
"provider",
"policies",
"skip_authorization", "skip_authorization",
"provider",
"meta_launch_url",
"meta_icon_url",
"meta_description",
"meta_publisher",
"policies",
] ]

View File

@ -0,0 +1,5 @@
"""passbook core exceptions"""
class PropertyMappingExpressionException(Exception):
"""Error when a PropertyMapping Exception expression could not be parsed or evaluated."""

View File

@ -19,19 +19,25 @@ class ApplicationForm(forms.ModelForm):
fields = [ fields = [
"name", "name",
"slug", "slug",
"launch_url",
"icon_url",
"provider",
"policies",
"skip_authorization", "skip_authorization",
"provider",
"meta_launch_url",
"meta_icon_url",
"meta_description",
"meta_publisher",
"policies",
] ]
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),
"launch_url": forms.TextInput(), "meta_launch_url": forms.TextInput(),
"icon_url": forms.TextInput(), "meta_icon_url": forms.TextInput(),
"meta_publisher": forms.TextInput(),
"policies": FilteredSelectMultiple(_("policies"), False), "policies": FilteredSelectMultiple(_("policies"), False),
} }
labels = { labels = {
"launch_url": _("Launch URL"), "meta_launch_url": _("Launch URL"),
"icon_url": _("Icon URL"), "meta_icon_url": _("Icon URL"),
"meta_description": _("Description"),
"meta_publisher": _("Publisher"),
} }
help_texts = {"policies": _("Policies required to access this Application.")}

View File

@ -70,7 +70,7 @@ class SignUpForm(forms.Form):
"""Check if username is used already""" """Check if username is used already"""
username = self.cleaned_data.get("username") username = self.cleaned_data.get("username")
if User.objects.filter(username=username).exists(): if User.objects.filter(username=username).exists():
LOGGER.warning("Username %s already exists", username) LOGGER.warning("username already exists", username=username)
raise ValidationError(_("Username already exists")) raise ValidationError(_("Username already exists"))
return username return username
@ -79,7 +79,7 @@ class SignUpForm(forms.Form):
email = self.cleaned_data.get("email") email = self.cleaned_data.get("email")
# Check if user exists already, error early # Check if user exists already, error early
if User.objects.filter(email=email).exists(): if User.objects.filter(email=email).exists():
LOGGER.debug("email %s exists in django", email) LOGGER.debug("email already exists", email=email)
raise ValidationError(_("Email already exists")) raise ValidationError(_("Email already exists"))
return email return email

View File

@ -0,0 +1,19 @@
# Generated by Django 3.0.3 on 2020-02-17 16:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0005_merge_20191025_2022"),
]
operations = [
migrations.AddField(
model_name="propertymapping",
name="template",
field=models.TextField(default=""),
preserve_default=False,
),
]

View File

@ -0,0 +1,16 @@
# Generated by Django 3.0.3 on 2020-02-17 19:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0006_propertymapping_template"),
]
operations = [
migrations.RenameField(
model_name="propertymapping", old_name="template", new_name="expression",
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.0.3 on 2020-02-20 12:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0007_auto_20200217_1934"),
]
operations = [
migrations.RenameField(
model_name="application", old_name="icon_url", new_name="meta_icon_url",
),
migrations.RenameField(
model_name="application", old_name="launch_url", new_name="meta_launch_url",
),
migrations.AddField(
model_name="application",
name="meta_description",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name="application",
name="meta_publisher",
field=models.TextField(blank=True, null=True),
),
]

View File

@ -2,26 +2,34 @@
from datetime import timedelta from datetime import timedelta
from random import SystemRandom from random import SystemRandom
from time import sleep from time import sleep
from typing import Optional from typing import Any, Optional
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.http import HttpRequest
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from django_prometheus.models import ExportModelOperationsMixin from django_prometheus.models import ExportModelOperationsMixin
from guardian.mixins import GuardianUserMixin from guardian.mixins import GuardianUserMixin
from jinja2 import Undefined
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
from jinja2.nativetypes import NativeEnvironment
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from structlog import get_logger from structlog import get_logger
from passbook.core.types import UIUserSettings, UILoginButton
from passbook.core.exceptions import PropertyMappingExpressionException
from passbook.core.signals import password_changed from passbook.core.signals import password_changed
from passbook.lib.models import CreatedUpdatedModel, UUIDModel from passbook.lib.models import CreatedUpdatedModel, UUIDModel
from passbook.policies.exceptions import PolicyException from passbook.policies.exceptions import PolicyException
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()
NATIVE_ENVIRONMENT = NativeEnvironment()
def default_nonce_duration(): def default_nonce_duration():
@ -95,19 +103,6 @@ class PolicyModel(UUIDModel, CreatedUpdatedModel):
policies = models.ManyToManyField("Policy", blank=True) policies = models.ManyToManyField("Policy", blank=True)
class UserSettings:
"""Dataclass for Factor and Source's user_settings"""
name: str
icon: str
view_name: str
def __init__(self, name: str, icon: str, view_name: str):
self.name = name
self.icon = icon
self.view_name = view_name
class Factor(ExportModelOperationsMixin("factor"), PolicyModel): class Factor(ExportModelOperationsMixin("factor"), PolicyModel):
"""Authentication factor, multiple instances of the same Factor can be used""" """Authentication factor, multiple instances of the same Factor can be used"""
@ -120,9 +115,10 @@ class Factor(ExportModelOperationsMixin("factor"), PolicyModel):
type = "" type = ""
form = "" form = ""
def user_settings(self) -> Optional[UserSettings]: @property
def ui_user_settings(self) -> Optional[UIUserSettings]:
"""Entrypoint to integrate with User settings. Can either return None if no """Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or an instanace of UserSettings.""" user settings are available, or an instanace of UIUserSettings."""
return None return None
def __str__(self): def __str__(self):
@ -136,16 +132,19 @@ class Application(ExportModelOperationsMixin("application"), PolicyModel):
name = models.TextField() name = models.TextField()
slug = models.SlugField() slug = models.SlugField()
launch_url = models.URLField(null=True, blank=True) skip_authorization = models.BooleanField(default=False)
icon_url = models.TextField(null=True, blank=True)
provider = models.OneToOneField( provider = models.OneToOneField(
"Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT "Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT
) )
skip_authorization = models.BooleanField(default=False)
meta_launch_url = models.URLField(null=True, blank=True)
meta_icon_url = models.TextField(null=True, blank=True)
meta_description = models.TextField(null=True, blank=True)
meta_publisher = models.TextField(null=True, blank=True)
objects = InheritanceManager() objects = InheritanceManager()
def get_provider(self): def get_provider(self) -> Optional[Provider]:
"""Get casted provider instance""" """Get casted provider instance"""
if not self.provider: if not self.provider:
return None return None
@ -160,6 +159,7 @@ class Source(ExportModelOperationsMixin("source"), PolicyModel):
name = models.TextField() name = models.TextField()
slug = models.SlugField() slug = models.SlugField()
enabled = models.BooleanField(default=True) enabled = models.BooleanField(default=True)
property_mappings = models.ManyToManyField( property_mappings = models.ManyToManyField(
"PropertyMapping", default=None, blank=True "PropertyMapping", default=None, blank=True
@ -170,19 +170,20 @@ class Source(ExportModelOperationsMixin("source"), PolicyModel):
objects = InheritanceManager() objects = InheritanceManager()
@property @property
def login_button(self): def ui_login_button(self) -> Optional[UILoginButton]:
"""Return a tuple of URL, Icon name and Name """If source uses a http-based flow, return UI Information about the login
if Source should get a link on the login page""" button. If source doesn't use http-based flow, return None."""
return None return None
@property @property
def additional_info(self): def ui_additional_info(self) -> Optional[str]:
"""Return additional Info, such as a callback URL. Show in the administration interface.""" """Return additional Info, such as a callback URL. Show in the administration interface."""
return None return None
def user_settings(self) -> Optional[UserSettings]: @property
def ui_user_settings(self) -> Optional[UIUserSettings]:
"""Entrypoint to integrate with User settings. Can either return None if no """Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or an instanace of UserSettings.""" user settings are available, or an instanace of UIUserSettings."""
return None return None
def __str__(self): def __str__(self):
@ -293,10 +294,34 @@ class PropertyMapping(UUIDModel):
"""User-defined key -> x mapping which can be used by providers to expose extra data.""" """User-defined key -> x mapping which can be used by providers to expose extra data."""
name = models.TextField() name = models.TextField()
expression = models.TextField()
form = "" form = ""
objects = InheritanceManager() objects = InheritanceManager()
def evaluate(
self, user: Optional[User], request: Optional[HttpRequest], **kwargs
) -> Any:
"""Evaluate `self.expression` using `**kwargs` as Context."""
try:
expression = NATIVE_ENVIRONMENT.from_string(self.expression)
except TemplateSyntaxError as exc:
raise PropertyMappingExpressionException from exc
try:
response = expression.render(user=user, request=request, **kwargs)
if isinstance(response, Undefined):
raise PropertyMappingExpressionException("Response was 'Undefined'")
return response
except UndefinedError as exc:
raise PropertyMappingExpressionException from exc
def save(self, *args, **kwargs):
try:
NATIVE_ENVIRONMENT.from_string(self.expression)
except TemplateSyntaxError as exc:
raise ValidationError("Expression Syntax Error") from exc
return super().save(*args, **kwargs)
def __str__(self): def __str__(self):
return f"Property Mapping {self.name}" return f"Property Mapping {self.name}"

View File

@ -1 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><path d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -7,7 +7,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title> <title>
{% block title %} {% block title %}
{% title %} {% title %}

View File

@ -19,6 +19,9 @@
<h1>{% trans 'Bad Request' %}</h1> <h1>{% trans 'Bad Request' %}</h1>
</header> </header>
<form> <form>
{% if message %}
<h3>{% trans message %}</h3>
{% endif %}
{% if 'back' in request.GET %} {% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a> <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %} {% endif %}

View File

@ -48,10 +48,16 @@
<!--login-pf-section--> <!--login-pf-section-->
<section class="login-pf-social-section" role="contentinfo" aria-label="Log in with third party account"> <section class="login-pf-social-section" role="contentinfo" aria-label="Log in with third party account">
<ul class="login-pf-social login-pf-social-double-col list-unstyled"> <ul class="login-pf-social login-pf-social-double-col list-unstyled">
{% for url, icon, name in sources %} {% for source in sources %}
<li class="login-pf-social-link"> <li class="login-pf-social-link">
<a href="{{ url }}"> <a href="{{ source.url }}">
<img src="{% static 'img/logos/' %}{{ icon }}.svg" alt="{{ name }}"> {{ name }} {% if source.icon_path %}
<img src="{% static source.icon_path %}" alt="{{ source.name }}">
{% endif %}
{% if source.icon_url %}
<img src="icon_url" alt="{{ source.name }}">
{% endif %}
{{ source.name }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -2,42 +2,44 @@
{% load i18n %} {% load i18n %}
{% block head %}
{{ block.super }}
<style>
img.app-icon {
max-height: 72px;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row row-cards-pf"> <div class="row row-cards-pf">
{% for app in applications %} {% for app in applications %}
<div class="col-xs-12 col-sm-6 col-md-3"> <div class="col-xs-12 col-sm-6 col-md-3">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
<h2 class="card-pf-title"> <div class="card-pf-body">
<span class="fa fa-shield"></span> {{ app.name }} <div class="card-pf-top-element">
</h2> <a href="{{ app.meta_launch_url }}">
<div class="card-pf-body"> {% if not app.meta_icon_url %}
<p class="card-pf-aggregate-status-notifications"> <span class="pficon pficon-arrow card-pf-icon-circle"></span>
<span class="card-pf-aggregate-status-notification"> {% else %}
<a href="{{ app.launch_url }}" class="add" data-toggle="tooltip" data-placement="top" title="{% trans 'Open App...' %}"> <img class="app-icon card-pf-icon-circle" src="{{ app.meta_icon_url }}" alt="{% trans 'Application Icon' %}">
{% if not app.icon_url %} {% endif %}
<span class="pficon pficon-arrow"></span> </a>
{% else %} </div>
<img class="app-icon" src="{{ app.icon_url }}" alt="{% trans 'Application Icon' %}"> <h2 class="card-pf-title text-center">
{% endif %} <a href="{{ app.meta_launch_url }}">
</a> {{ app.name }}
</span> </a>
</p> </h2>
{% if app.meta_publisher %}
<div class="card-pf-items text-center">
<small>{{ app.meta_publisher }}</small>
</div>
{% endif %}
{% if app.meta_description %}
<div class="card-pf-items text-center">
<p>{{ app.meta_description }}</p>
</div>
{% endif %}
</div>
<div class="card-pf-view-checkbox">
<a href="{{ app.meta_launch_url }}"></a>
</div>
</div>
</div> </div>
</div> {% endfor %}
</div> </div>
{% empty %}
<h1>{% trans 'No Applications available.' %}</h1>
{% endfor %}
</div><!-- /row -->
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,46 +1,53 @@
"""passbook user settings template tags""" """passbook user settings template tags"""
from typing import List from typing import List, Iterable
from django import template from django import template
from django.template.context import RequestContext from django.template.context import RequestContext
from passbook.core.models import Factor, Source, UserSettings from passbook.core.types import UIUserSettings
from passbook.core.models import Factor, Source
from passbook.policies.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
register = template.Library() register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def user_factors(context: RequestContext) -> List[UserSettings]: def user_factors(context: RequestContext) -> List[UIUserSettings]:
"""Return list of all factors which apply to user""" """Return list of all factors which apply to user"""
user = context.get("request").user user = context.get("request").user
_all_factors = ( _all_factors: Iterable[Factor] = (
Factor.objects.filter(enabled=True).order_by("order").select_subclasses() Factor.objects.filter(enabled=True).order_by("order").select_subclasses()
) )
matching_factors: List[UserSettings] = [] matching_factors: List[UIUserSettings] = []
for factor in _all_factors: for factor in _all_factors:
user_settings = factor.user_settings() user_settings = factor.ui_user_settings
if not user_settings:
continue
policy_engine = PolicyEngine( policy_engine = PolicyEngine(
factor.policies.all(), user, context.get("request") factor.policies.all(), user, context.get("request")
) )
policy_engine.build() policy_engine.build()
if policy_engine.passing and user_settings: if policy_engine.passing:
matching_factors.append(user_settings) matching_factors.append(user_settings)
return matching_factors return matching_factors
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def user_sources(context: RequestContext) -> List[UserSettings]: def user_sources(context: RequestContext) -> List[UIUserSettings]:
"""Return a list of all sources which are enabled for the user""" """Return a list of all sources which are enabled for the user"""
user = context.get("request").user user = context.get("request").user
_all_sources = Source.objects.filter(enabled=True).select_subclasses() _all_sources: Iterable[Source] = (
matching_sources: List[UserSettings] = [] Source.objects.filter(enabled=True).select_subclasses()
)
matching_sources: List[UIUserSettings] = []
for factor in _all_sources: for factor in _all_sources:
user_settings = factor.user_settings() user_settings = factor.ui_user_settings
if not user_settings:
continue
policy_engine = PolicyEngine( policy_engine = PolicyEngine(
factor.policies.all(), user, context.get("request") factor.policies.all(), user, context.get("request")
) )
policy_engine.build() policy_engine.build()
if policy_engine.passing and user_settings: if policy_engine.passing:
matching_sources.append(user_settings) matching_sources.append(user_settings)
return matching_sources return matching_sources

29
passbook/core/types.py Normal file
View File

@ -0,0 +1,29 @@
"""passbook core dataclasses"""
from typing import Optional
from dataclasses import dataclass
@dataclass
class UIUserSettings:
"""Dataclass for Factor and Source's user_settings"""
name: str
icon: str
view_name: str
@dataclass
class UILoginButton:
"""Dataclass for Source's ui_ui_login_button"""
# Name, ran through i18n
name: str
# URL Which Button points to
url: str
# Icon name, ran through django's static
icon_path: Optional[str] = None
# Icon URL, used as-is
icon_url: Optional[str] = None

View File

@ -47,9 +47,9 @@ class LoginView(UserPassesTestMixin, FormView):
kwargs["sources"] = [] kwargs["sources"] = []
sources = Source.objects.filter(enabled=True).select_subclasses() sources = Source.objects.filter(enabled=True).select_subclasses()
for source in sources: for source in sources:
login_button = source.login_button ui_login_button = source.ui_login_button
if login_button: if ui_login_button:
kwargs["sources"].append(login_button) kwargs["sources"].append(ui_login_button)
if kwargs["sources"]: if kwargs["sources"]:
self.template_name = "login/with_sources.html" self.template_name = "login/with_sources.html"
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
@ -72,7 +72,6 @@ class LoginView(UserPassesTestMixin, FormView):
if not pre_user: if not pre_user:
# No user found # No user found
return self.invalid_login(self.request) return self.invalid_login(self.request)
# self.request.session.flush()
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
return _redirect_with_qs("passbook_core:auth-process", self.request.GET) return _redirect_with_qs("passbook_core:auth-process", self.request.GET)
@ -156,26 +155,9 @@ class SignUpView(UserPassesTestMixin, FormView):
for error in exc.messages: for error in exc.messages:
errors.append(error) errors.append(error)
return self.form_invalid(form) return self.form_invalid(form)
# needs_confirmation = True
# if self._invitation and not self._invitation.needs_confirmation:
# needs_confirmation = False
# if needs_confirmation:
# nonce = Nonce.objects.create(user=self._user)
# LOGGER.debug(str(nonce.uuid))
# # Send email to user
# send_email.delay(self._user.email, _('Confirm your account.'),
# 'email/account_confirm.html', {
# 'url': self.request.build_absolute_uri(
# reverse('passbook_core:auth-sign-up-confirm', kwargs={
# 'nonce': nonce.uuid
# })
# )
# })
# self._user.is_active = False
# self._user.save()
self.consume_invitation() self.consume_invitation()
messages.success(self.request, _("Successfully signed up!")) messages.success(self.request, _("Successfully signed up!"))
LOGGER.debug("Successfully signed up %s", form.cleaned_data.get("email")) LOGGER.debug("Successfully signed up", email=form.cleaned_data.get("email"))
return redirect(reverse("passbook_core:auth-login")) return redirect(reverse("passbook_core:auth-login"))
def consume_invitation(self): def consume_invitation(self):

View File

@ -15,7 +15,7 @@ class OverviewView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs["applications"] = [] kwargs["applications"] = []
for application in Application.objects.all(): for application in Application.objects.all().order_by("name"):
engine = PolicyEngine( engine = PolicyEngine(
application.policies.all(), self.request.user, self.request application.policies.all(), self.request.user, self.request
) )

View File

@ -1,9 +1,9 @@
"""OTP Factor""" """OTP Factor"""
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import Factor, UserSettings from passbook.core.types import UIUserSettings
from passbook.core.models import Factor
class OTPFactor(Factor): class OTPFactor(Factor):
@ -17,9 +17,12 @@ class OTPFactor(Factor):
type = "passbook.factors.otp.factors.OTPFactor" type = "passbook.factors.otp.factors.OTPFactor"
form = "passbook.factors.otp.forms.OTPFactorForm" form = "passbook.factors.otp.forms.OTPFactorForm"
def user_settings(self) -> UserSettings: @property
return UserSettings( def ui_user_settings(self) -> UIUserSettings:
_("OTP"), "pficon-locked", "passbook_factors_otp:otp-user-settings" return UIUserSettings(
name="OTP",
icon="pficon-locked",
view_name="passbook_factors_otp:otp-user-settings",
) )
def __str__(self): def __str__(self):

View File

@ -7,8 +7,10 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, HttpRequest, HttpResponse from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views import View from django.views import View
from django.views.decorators.cache import never_cache
from django.views.generic import FormView, TemplateView from django.views.generic import FormView, TemplateView
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
@ -19,7 +21,6 @@ from structlog import get_logger
from passbook.audit.models import Event, EventAction from passbook.audit.models import Event, EventAction
from passbook.factors.otp.forms import OTPSetupForm from passbook.factors.otp.forms import OTPSetupForm
from passbook.factors.otp.utils import otpauth_url from passbook.factors.otp.utils import otpauth_url
from passbook.lib.boilerplate import NeverCacheMixin
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
OTP_SESSION_KEY = "passbook_factors_otp_key" OTP_SESSION_KEY = "passbook_factors_otp_key"
@ -146,7 +147,8 @@ class EnableView(LoginRequiredMixin, FormView):
return redirect("passbook_factors_otp:otp-user-settings") return redirect("passbook_factors_otp:otp-user-settings")
class QRView(NeverCacheMixin, View): @method_decorator(never_cache, name="dispatch")
class QRView(View):
"""View returns an SVG image with the OTP token information""" """View returns an SVG image with the OTP token information"""
def get(self, request: HttpRequest) -> HttpResponse: def get(self, request: HttpRequest) -> HttpResponse:

View File

@ -3,7 +3,8 @@ from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from passbook.core.models import Factor, Policy, User, UserSettings from passbook.core.types import UIUserSettings
from passbook.core.models import Factor, Policy, User
class PasswordFactor(Factor): class PasswordFactor(Factor):
@ -18,9 +19,12 @@ class PasswordFactor(Factor):
type = "passbook.factors.password.factor.PasswordFactor" type = "passbook.factors.password.factor.PasswordFactor"
form = "passbook.factors.password.forms.PasswordFactorForm" form = "passbook.factors.password.forms.PasswordFactorForm"
def user_settings(self): @property
return UserSettings( def ui_user_settings(self) -> UIUserSettings:
_("Change Password"), "pficon-key", "passbook_core:user-change-password" return UIUserSettings(
name="Change Password",
icon="pficon-key",
view_name="passbook_core:user-change-password",
) )
def password_passes(self, user: User) -> bool: def password_passes(self, user: User) -> bool:

View File

@ -38,9 +38,8 @@ class TestFactorAuthentication(TestCase):
def test_unauthenticated_raw(self): def test_unauthenticated_raw(self):
"""test direct call to AuthenticationView""" """test direct call to AuthenticationView"""
response = self.client.get(reverse("passbook_core:auth-process")) response = self.client.get(reverse("passbook_core:auth-process"))
# Response should be 302 since no pending user is set # Response should be 400 since no pending user is set
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 400)
self.assertEqual(response.url, reverse("passbook_core:auth-login"))
def test_unauthenticated_prepared(self): def test_unauthenticated_prepared(self):
"""test direct call but with pending_uesr in session""" """test direct call but with pending_uesr in session"""
@ -71,9 +70,8 @@ class TestFactorAuthentication(TestCase):
"""Test with already logged in user""" """Test with already logged in user"""
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("passbook_core:auth-process")) response = self.client.get(reverse("passbook_core:auth-process"))
# Response should be 302 since no pending user is set # Response should be 400 since no pending user is set
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 400)
self.assertEqual(response.url, reverse("passbook_core:overview"))
self.client.logout() self.client.logout()
def test_unauthenticated_post(self): def test_unauthenticated_post(self):

View File

@ -1,15 +1,17 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from typing import List, Tuple from typing import List, Optional, Tuple
from django.contrib.auth import login from django.contrib.auth import login
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import get_object_or_404, redirect, reverse from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from django.views.generic import View from django.views.generic import View
from structlog import get_logger from structlog import get_logger
from passbook.core.models import Factor, User from passbook.core.models import Factor, User
from passbook.core.views.utils import PermissionDeniedView from passbook.core.views.utils import PermissionDeniedView
from passbook.lib.config import CONFIG
from passbook.lib.utils.reflection import class_to_path, path_to_class from passbook.lib.utils.reflection import class_to_path, path_to_class
from passbook.lib.utils.urls import is_url_absolute from passbook.lib.utils.urls import is_url_absolute
from passbook.policies.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
@ -44,10 +46,28 @@ class AuthenticationView(UserPassesTestMixin, View):
current_factor: Factor current_factor: Factor
# Allow only not authenticated users to login # Allow only not authenticated users to login
def test_func(self): def test_func(self) -> bool:
return AuthenticationView.SESSION_PENDING_USER in self.request.session return AuthenticationView.SESSION_PENDING_USER in self.request.session
def handle_no_permission(self): def _check_config_domain(self) -> Optional[HttpResponse]:
"""Checks if current request's domain matches configured Domain, and
adds a warning if not."""
current_domain = self.request.get_host()
if ":" in current_domain:
current_domain, _ = current_domain.split(":")
config_domain = CONFIG.y("domain")
if current_domain != config_domain:
message = (
f"Current domain of '{current_domain}' doesn't "
f"match configured domain of '{config_domain}'."
)
LOGGER.warning(message)
return render(
self.request, "error/400.html", context={"message": message}, status=400
)
return None
def handle_no_permission(self) -> HttpResponse:
# Function from UserPassesTestMixin # Function from UserPassesTestMixin
if NEXT_ARG_NAME in self.request.GET: if NEXT_ARG_NAME in self.request.GET:
return redirect(self.request.GET.get(NEXT_ARG_NAME)) return redirect(self.request.GET.get(NEXT_ARG_NAME))
@ -55,7 +75,7 @@ class AuthenticationView(UserPassesTestMixin, View):
return _redirect_with_qs("passbook_core:overview", self.request.GET) return _redirect_with_qs("passbook_core:overview", self.request.GET)
return _redirect_with_qs("passbook_core:auth-login", self.request.GET) return _redirect_with_qs("passbook_core:auth-login", self.request.GET)
def get_pending_factors(self): def get_pending_factors(self) -> List[Tuple[str, str]]:
"""Loading pending factors from Database or load from session variable""" """Loading pending factors from Database or load from session variable"""
# Write pending factors to session # Write pending factors to session
if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session: if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session:
@ -67,6 +87,7 @@ class AuthenticationView(UserPassesTestMixin, View):
) )
pending_factors = [] pending_factors = []
for factor in _all_factors: for factor in _all_factors:
factor: Factor
LOGGER.debug( LOGGER.debug(
"Checking if factor applies to user", "Checking if factor applies to user",
factor=factor, factor=factor,
@ -81,10 +102,13 @@ class AuthenticationView(UserPassesTestMixin, View):
LOGGER.debug("Factor applies", factor=factor, user=self.pending_user) LOGGER.debug("Factor applies", factor=factor, user=self.pending_user)
return pending_factors return pending_factors
def dispatch(self, request, *args, **kwargs): def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if user passes test (i.e. SESSION_PENDING_USER is set) # Check if user passes test (i.e. SESSION_PENDING_USER is set)
user_test_result = self.get_test_func()() user_test_result = self.get_test_func()()
if not user_test_result: if not user_test_result:
incorrect_domain_message = self._check_config_domain()
if incorrect_domain_message:
return incorrect_domain_message
return self.handle_no_permission() return self.handle_no_permission()
# Extract pending user from session (only remember uid) # Extract pending user from session (only remember uid)
self.pending_user = get_object_or_404( self.pending_user = get_object_or_404(
@ -117,7 +141,7 @@ class AuthenticationView(UserPassesTestMixin, View):
self._current_factor_class.request = request self._current_factor_class.request = request
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""pass get request to current factor""" """pass get request to current factor"""
LOGGER.debug( LOGGER.debug(
"Passing GET", "Passing GET",
@ -125,7 +149,7 @@ class AuthenticationView(UserPassesTestMixin, View):
) )
return self._current_factor_class.get(request, *args, **kwargs) return self._current_factor_class.get(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""pass post request to current factor""" """pass post request to current factor"""
LOGGER.debug( LOGGER.debug(
"Passing POST", "Passing POST",
@ -133,7 +157,7 @@ class AuthenticationView(UserPassesTestMixin, View):
) )
return self._current_factor_class.post(request, *args, **kwargs) return self._current_factor_class.post(request, *args, **kwargs)
def user_ok(self): def user_ok(self) -> HttpResponse:
"""Redirect to next Factor""" """Redirect to next Factor"""
LOGGER.debug( LOGGER.debug(
"Factor passed", "Factor passed",
@ -160,14 +184,14 @@ class AuthenticationView(UserPassesTestMixin, View):
LOGGER.debug("User passed all factors, logging in", user=self.pending_user) LOGGER.debug("User passed all factors, logging in", user=self.pending_user)
return self._user_passed() return self._user_passed()
def user_invalid(self): def user_invalid(self) -> HttpResponse:
"""Show error message, user cannot login. """Show error message, user cannot login.
This should only be shown if user authenticated successfully, but is disabled/locked/etc""" This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
LOGGER.debug("User invalid") LOGGER.debug("User invalid")
self.cleanup() self.cleanup()
return _redirect_with_qs("passbook_core:auth-denied", self.request.GET) return _redirect_with_qs("passbook_core:auth-denied", self.request.GET)
def _user_passed(self): def _user_passed(self) -> HttpResponse:
"""User Successfully passed all factors""" """User Successfully passed all factors"""
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND] backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
login(self.request, self.pending_user, backend=backend) login(self.request, self.pending_user, backend=backend)

View File

@ -1,12 +0,0 @@
"""passbook django boilerplate code"""
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
class NeverCacheMixin:
"""Use never_cache as mixin for CBV"""
@method_decorator(never_cache)
def dispatch(self, *args, **kwargs):
"""Use never_cache as mixin for CBV"""
return super().dispatch(*args, **kwargs)

View File

@ -1,12 +0,0 @@
"""passbook util mixins"""
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
class CSRFExemptMixin:
"""wrapper to apply @csrf_exempt to CBV"""
@method_decorator(csrf_exempt)
def dispatch(self, *args, **kwargs):
"""wrapper to apply @csrf_exempt to CBV"""
return super().dispatch(*args, **kwargs)

View File

@ -8,10 +8,12 @@ def before_send(event, hint):
"""Check if error is database error, and ignore if so""" """Check if error is database error, and ignore if so"""
from django_redis.exceptions import ConnectionInterrupted from django_redis.exceptions import ConnectionInterrupted
from django.db import OperationalError, InternalError from django.db import OperationalError, InternalError
from django.core.exceptions import ValidationError
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from billiard.exceptions import WorkerLostError from billiard.exceptions import WorkerLostError
from django.core.exceptions import DisallowedHost from django.core.exceptions import DisallowedHost
from botocore.client import ClientError from botocore.client import ClientError
from redis.exceptions import RedisError
ignored_classes = ( ignored_classes = (
OperationalError, OperationalError,
@ -24,6 +26,9 @@ def before_send(event, hint):
ConnectionResetError, ConnectionResetError,
KeyboardInterrupt, KeyboardInterrupt,
ClientError, ClientError,
ValidationError,
OSError,
RedisError,
) )
if "exc_info" in hint: if "exc_info" in hint:
_exc_type, exc_value, _ = hint["exc_info"] _exc_type, exc_value, _ = hint["exc_info"]

View File

@ -1,5 +1,6 @@
"""passbook helper views""" """passbook helper views"""
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.views.generic import CreateView from django.views.generic import CreateView
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
@ -22,3 +23,8 @@ class CreateAssignPermView(CreateView):
) )
assign_perm(full_permission, self.request.user, self.object) assign_perm(full_permission, self.request.user, self.object)
return response return response
def bad_request_message(request: HttpRequest, message: str) -> HttpResponse:
"""Return generic error page with message, with status code set to 400"""
return render(request, "error/400.html", {"message": message}, status=400)

View File

@ -9,7 +9,7 @@ from structlog import get_logger
from passbook.core.models import Policy, User from passbook.core.models import Policy, User
from passbook.policies.process import PolicyProcess, cache_key from passbook.policies.process import PolicyProcess, cache_key
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()
# This is only really needed for macOS, because Python 3.8 changed the default to spawn # This is only really needed for macOS, because Python 3.8 changed the default to spawn
@ -63,13 +63,13 @@ class PolicyEngine:
for policy in self._select_subclasses(): for policy in self._select_subclasses():
cached_policy = cache.get(cache_key(policy, self.request.user), None) cached_policy = cache.get(cache_key(policy, self.request.user), None)
if cached_policy and self.use_cache: if cached_policy and self.use_cache:
LOGGER.debug("Taking result from cache", policy=policy) LOGGER.debug("P_ENG: Taking result from cache", policy=policy)
self.__cached_policies.append(cached_policy) self.__cached_policies.append(cached_policy)
continue continue
LOGGER.debug("Evaluating policy", policy=policy) LOGGER.debug("P_ENG: Evaluating policy", policy=policy)
our_end, task_end = Pipe(False) our_end, task_end = Pipe(False)
task = PolicyProcess(policy, self.request, task_end) task = PolicyProcess(policy, self.request, task_end)
LOGGER.debug("Starting Process", policy=policy) LOGGER.debug("P_ENG: Starting Process", policy=policy)
task.start() task.start()
self.__processes.append( self.__processes.append(
PolicyProcessInfo(process=task, connection=our_end, policy=policy) PolicyProcessInfo(process=task, connection=our_end, policy=policy)
@ -90,7 +90,7 @@ class PolicyEngine:
x.result for x in self.__processes if x.result x.result for x in self.__processes if x.result
] ]
for result in process_results + self.__cached_policies: for result in process_results + self.__cached_policies:
LOGGER.debug("result", passing=result.passing) LOGGER.debug("P_ENG: result", passing=result.passing)
if result.messages: if result.messages:
messages += result.messages messages += result.messages
if not result.passing: if not result.passing:

View File

@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
from structlog import get_logger from structlog import get_logger
from passbook.core.models import Policy from passbook.core.models import Policy
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -0,0 +1,5 @@
"""Passbook passbook expression policy Admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister("passbook_policies_expression")

View File

@ -0,0 +1,21 @@
"""Expression Policy API"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS
class ExpressionPolicySerializer(ModelSerializer):
"""Group Membership Policy Serializer"""
class Meta:
model = ExpressionPolicy
fields = GENERAL_SERIALIZER_FIELDS + ["expression"]
class ExpressionPolicyViewSet(ModelViewSet):
"""Source Viewset"""
queryset = ExpressionPolicy.objects.all()
serializer_class = ExpressionPolicySerializer

View File

@ -0,0 +1,11 @@
"""Passbook policy_expression app config"""
from django.apps import AppConfig
class PassbookPolicyExpressionConfig(AppConfig):
"""Passbook policy_expression app config"""
name = "passbook.policies.expression"
label = "passbook_policies_expression"
verbose_name = "passbook Policies.Expression"

View File

@ -0,0 +1,94 @@
"""passbook expression policy evaluator"""
import re
from typing import TYPE_CHECKING, Any, Dict
from django.core.exceptions import ValidationError
from jinja2 import Undefined
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
from jinja2.nativetypes import NativeEnvironment
from structlog import get_logger
from passbook.factors.view import AuthenticationView
from passbook.policies.types import PolicyRequest, PolicyResult
if TYPE_CHECKING:
from passbook.core.models import User
LOGGER = get_logger()
class Evaluator:
"""Validate and evaulate jinja2-based expressions"""
_env: NativeEnvironment
def __init__(self):
self._env = NativeEnvironment()
# update passbook/policies/expression/templates/policy/expression/form.html
# update docs/policies/expression/index.md
self._env.filters["regex_match"] = Evaluator.jinja2_filter_regex_match
self._env.filters["regex_replace"] = Evaluator.jinja2_filter_regex_replace
@staticmethod
def jinja2_filter_regex_match(value: Any, regex: str) -> bool:
"""Jinja2 Filter to run re.search"""
return re.search(regex, value) is None
@staticmethod
def jinja2_filter_regex_replace(value: Any, regex: str, repl: str) -> str:
"""Jinja2 Filter to run re.sub"""
return re.sub(regex, repl, value)
@staticmethod
def jinja2_func_is_group_member(user: "User", group_name: str) -> bool:
"""Check if `user` is member of group with name `group_name`"""
return user.groups.filter(name=group_name).exists()
def _get_expression_context(
self, request: PolicyRequest, **kwargs
) -> Dict[str, Any]:
"""Return dictionary with additional global variables passed to expression"""
# update passbook/policies/expression/templates/policy/expression/form.html
# update docs/policies/expression/index.md
kwargs["pb_is_sso_flow"] = request.http_request.session.get(
AuthenticationView.SESSION_IS_SSO_LOGIN, False
)
kwargs["pb_is_group_member"] = Evaluator.jinja2_func_is_group_member
kwargs["pb_logger"] = get_logger()
return kwargs
def evaluate(self, expression_source: str, request: PolicyRequest) -> PolicyResult:
"""Parse and evaluate expression.
If the Expression evaluates to a list with 2 items, the first is used as passing bool and
the second as messages.
If the Expression evaluates to a truthy-object, it is used as passing bool."""
try:
expression = self._env.from_string(expression_source)
except TemplateSyntaxError as exc:
return PolicyResult(False, str(exc))
try:
result = expression.render(
request=request, **self._get_expression_context(request)
)
if isinstance(result, Undefined):
LOGGER.warning(
"Expression policy returned undefined",
src=expression_source,
req=request,
)
return PolicyResult(False)
if isinstance(result, list) and len(result) == 2:
return PolicyResult(*result)
if result:
return PolicyResult(result)
return PolicyResult(False)
except UndefinedError as exc:
return PolicyResult(False, str(exc))
def validate(self, expression: str):
"""Validate expression's syntax, raise ValidationError if Syntax is invalid"""
try:
self._env.from_string(expression)
return True
except TemplateSyntaxError as exc:
raise ValidationError("Expression Syntax Error") from exc

View File

@ -0,0 +1,22 @@
"""passbook Expression Policy forms"""
from django import forms
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.forms import GENERAL_FIELDS
class ExpressionPolicyForm(forms.ModelForm):
"""ExpressionPolicy Form"""
template_name = "policy/expression/form.html"
class Meta:
model = ExpressionPolicy
fields = GENERAL_FIELDS + [
"expression",
]
widgets = {
"name": forms.TextInput(),
}

View File

@ -1,4 +1,4 @@
# Generated by Django 2.2.6 on 2019-10-07 14:07 # Generated by Django 3.0.3 on 2020-02-18 14:00
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -9,12 +9,12 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("passbook_core", "0001_initial"), ("passbook_core", "0007_auto_20200217_1934"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="SSOLoginPolicy", name="ExpressionPolicy",
fields=[ fields=[
( (
"policy_ptr", "policy_ptr",
@ -27,10 +27,11 @@ class Migration(migrations.Migration):
to="passbook_core.Policy", to="passbook_core.Policy",
), ),
), ),
("expression", models.TextField()),
], ],
options={ options={
"verbose_name": "SSO Login Policy", "verbose_name": "Expression Policy",
"verbose_name_plural": "SSO Login Policies", "verbose_name_plural": "Expression Policies",
}, },
bases=("passbook_core.policy",), bases=("passbook_core.policy",),
), ),

View File

@ -0,0 +1,28 @@
"""passbook expression Policy Models"""
from django.db import models
from django.utils.translation import gettext as _
from passbook.core.models import Policy
from passbook.policies.expression.evaluator import Evaluator
from passbook.policies.types import PolicyRequest, PolicyResult
class ExpressionPolicy(Policy):
"""Jinja2-based Expression policy that allows Admins to write their own logic"""
expression = models.TextField()
form = "passbook.policies.expression.forms.ExpressionPolicyForm"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
return Evaluator().evaluate(self.expression, request)
def save(self, *args, **kwargs):
Evaluator().validate(self.expression)
return super().save(*args, **kwargs)
class Meta:
verbose_name = _("Expression Policy")
verbose_name_plural = _("Expression Policies")

View File

@ -0,0 +1,27 @@
{% extends "generic/form.html" %}
{% load i18n %}
{% block beneath_form %}
<div class="form-group ">
<label class="col-sm-2 control-label" for="friendly_name-2">
</label>
<div class="col-sm-10">
<p>
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
</p>
<ul>
<li><code>request.user</code>: Passbook User Object (<a href="https://beryju.github.io/passbook/property-mappings/reference/user-object/">Reference</a>)</li>
<li><code>request.http_request</code>: Django HTTP Request Object (<a href="https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects">Reference</a>) </li>
<li><code>request.obj</code>: Model the Policy is run against. </li>
<li><code>pb_is_sso_flow</code>: Boolean which is true if request was initiated by authenticating through an external Provider.</li>
<li><code>pb_is_group_member(user, group_name)</code>: Function which checks if <code>user</code> is member of a Group with Name <code>group_name</code>.</li>
</ul>
<p>Custom Filters:</p>
<ul>
<li><code>regex_match(regex)</code>: Checks if value matches <code>regex</code></li>
<li><code>regex_replace(regex, repl)</code>: Replace string matched by <code>regex</code> with <code>repl</code></li>
</ul>
</div>
</div>
{% endblock %}

View File

@ -1,4 +0,0 @@
"""autodiscover admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister("passbook_policies_group")

View File

@ -1,21 +0,0 @@
"""Source API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS
from passbook.policies.group.models import GroupMembershipPolicy
class GroupMembershipPolicySerializer(ModelSerializer):
"""Group Membership Policy Serializer"""
class Meta:
model = GroupMembershipPolicy
fields = GENERAL_SERIALIZER_FIELDS + ["group"]
class GroupMembershipPolicyViewSet(ModelViewSet):
"""Source Viewset"""
queryset = GroupMembershipPolicy.objects.all()
serializer_class = GroupMembershipPolicySerializer

View File

@ -1,11 +0,0 @@
"""passbook Group policy app config"""
from django.apps import AppConfig
class PassbookPoliciesGroupConfig(AppConfig):
"""passbook Group policy app config"""
name = "passbook.policies.group"
label = "passbook_policies_group"
verbose_name = "passbook Policies.Group"

View File

@ -1,21 +0,0 @@
"""passbook Policy forms"""
from django import forms
from passbook.policies.forms import GENERAL_FIELDS
from passbook.policies.group.models import GroupMembershipPolicy
class GroupMembershipPolicyForm(forms.ModelForm):
"""GroupMembershipPolicy Form"""
class Meta:
model = GroupMembershipPolicy
fields = GENERAL_FIELDS + [
"group",
]
widgets = {
"name": forms.TextInput(),
"order": forms.NumberInput(),
}

View File

@ -1,44 +0,0 @@
# Generated by Django 2.2.6 on 2019-10-07 14:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("passbook_core", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="GroupMembershipPolicy",
fields=[
(
"policy_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="passbook_core.Policy",
),
),
(
"group",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="passbook_core.Group",
),
),
],
options={
"verbose_name": "Group Membership Policy",
"verbose_name_plural": "Group Membership Policies",
},
bases=("passbook_core.policy",),
),
]

View File

@ -1,22 +0,0 @@
"""passbook group models models"""
from django.db import models
from django.utils.translation import gettext as _
from passbook.core.models import Group, Policy
from passbook.policies.struct import PolicyRequest, PolicyResult
class GroupMembershipPolicy(Policy):
"""Policy to check if the user is member in a certain group"""
group = models.ForeignKey(Group, on_delete=models.CASCADE)
form = "passbook.policies.group.forms.GroupMembershipPolicyForm"
def passes(self, request: PolicyRequest) -> PolicyResult:
return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists())
class Meta:
verbose_name = _("Group Membership Policy")
verbose_name_plural = _("Group Membership Policies")

View File

@ -35,7 +35,7 @@ class HaveIBeenPwendPolicy(Policy):
full_hash, count = line.split(":") full_hash, count = line.split(":")
if pw_hash[5:] == full_hash.lower(): if pw_hash[5:] == full_hash.lower():
final_count = int(count) final_count = int(count)
LOGGER.debug("Got count %d for hash %s", final_count, pw_hash[:5]) LOGGER.debug("got hibp result", count=final_count, hash=pw_hash[:5])
if final_count > self.allowed_count: if final_count > self.allowed_count:
message = _( message = _(
"Password exists on %(count)d online lists." % {"count": final_count} "Password exists on %(count)d online lists." % {"count": final_count}

View File

@ -1,4 +0,0 @@
"""autodiscover admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister("passbook_policies_matcher")

View File

@ -1,25 +0,0 @@
"""Source API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS
from passbook.policies.matcher.models import FieldMatcherPolicy
class FieldMatcherPolicySerializer(ModelSerializer):
"""Field Matcher Policy Serializer"""
class Meta:
model = FieldMatcherPolicy
fields = GENERAL_SERIALIZER_FIELDS + [
"user_field",
"match_action",
"value",
]
class FieldMatcherPolicyViewSet(ModelViewSet):
"""Source Viewset"""
queryset = FieldMatcherPolicy.objects.all()
serializer_class = FieldMatcherPolicySerializer

View File

@ -1,11 +0,0 @@
"""passbook Matcher policy app config"""
from django.apps import AppConfig
class PassbookPoliciesMatcherConfig(AppConfig):
"""passbook Matcher policy app config"""
name = "passbook.policies.matcher"
label = "passbook_policies_matcher"
verbose_name = "passbook Policies.Matcher"

View File

@ -1,23 +0,0 @@
"""passbook Policy forms"""
from django import forms
from passbook.policies.forms import GENERAL_FIELDS
from passbook.policies.matcher.models import FieldMatcherPolicy
class FieldMatcherPolicyForm(forms.ModelForm):
"""FieldMatcherPolicy Form"""
class Meta:
model = FieldMatcherPolicy
fields = GENERAL_FIELDS + [
"user_field",
"match_action",
"value",
]
widgets = {
"name": forms.TextInput(),
"value": forms.TextInput(),
}

View File

@ -1,64 +0,0 @@
# Generated by Django 2.2.6 on 2019-10-07 14:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("passbook_core", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="FieldMatcherPolicy",
fields=[
(
"policy_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="passbook_core.Policy",
),
),
(
"user_field",
models.TextField(
choices=[
("username", "Username"),
("name", "Name"),
("email", "E-Mail"),
("is_staff", "Is staff"),
("is_active", "Is active"),
("data_joined", "Date joined"),
]
),
),
(
"match_action",
models.CharField(
choices=[
("startswith", "Starts with"),
("endswith", "Ends with"),
("contains", "Contains"),
("regexp", "Regexp"),
("exact", "Exact"),
],
max_length=50,
),
),
("value", models.TextField()),
],
options={
"verbose_name": "Field matcher Policy",
"verbose_name_plural": "Field matcher Policies",
},
bases=("passbook_core.policy",),
),
]

View File

@ -1,83 +0,0 @@
"""user field matcher models"""
import re
from django.db import models
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Policy
from passbook.policies.struct import PolicyRequest, PolicyResult
LOGGER = get_logger()
class FieldMatcherPolicy(Policy):
"""Policy which checks if a field of the User model matches/doesn't match a
certain pattern"""
MATCH_STARTSWITH = "startswith"
MATCH_ENDSWITH = "endswith"
MATCH_CONTAINS = "contains"
MATCH_REGEXP = "regexp"
MATCH_EXACT = "exact"
MATCHES = (
(MATCH_STARTSWITH, _("Starts with")),
(MATCH_ENDSWITH, _("Ends with")),
(MATCH_CONTAINS, _("Contains")),
(MATCH_REGEXP, _("Regexp")),
(MATCH_EXACT, _("Exact")),
)
USER_FIELDS = (
("username", _("Username"),),
("name", _("Name"),),
("email", _("E-Mail"),),
("is_staff", _("Is staff"),),
("is_active", _("Is active"),),
("data_joined", _("Date joined"),),
)
user_field = models.TextField(choices=USER_FIELDS)
match_action = models.CharField(max_length=50, choices=MATCHES)
value = models.TextField()
form = "passbook.policies.matcher.forms.FieldMatcherPolicyForm"
def __str__(self):
description = (
f"{self.name}, user.{self.user_field} {self.match_action} '{self.value}'"
)
if self.name:
description = f"{self.name}: {description}"
return description
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if user instance passes this role"""
if not hasattr(request.user, self.user_field):
raise ValueError("Field does not exist")
user_field_value = getattr(request.user, self.user_field, None)
LOGGER.debug(
"Checking field",
value=user_field_value,
action=self.match_action,
should_be=self.value,
)
passes = False
if self.match_action == FieldMatcherPolicy.MATCH_STARTSWITH:
passes = user_field_value.startswith(self.value)
if self.match_action == FieldMatcherPolicy.MATCH_ENDSWITH:
passes = user_field_value.endswith(self.value)
if self.match_action == FieldMatcherPolicy.MATCH_CONTAINS:
passes = self.value in user_field_value
if self.match_action == FieldMatcherPolicy.MATCH_REGEXP:
pattern = re.compile(self.value)
passes = bool(pattern.match(user_field_value))
if self.match_action == FieldMatcherPolicy.MATCH_EXACT:
passes = user_field_value == self.value
return PolicyResult(passes)
class Meta:
verbose_name = _("Field matcher Policy")
verbose_name_plural = _("Field matcher Policies")

View File

@ -6,7 +6,7 @@ from django.utils.translation import gettext as _
from structlog import get_logger from structlog import get_logger
from passbook.core.models import Policy from passbook.core.models import Policy
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -7,7 +7,7 @@ from structlog import get_logger
from passbook.core.models import Policy from passbook.core.models import Policy
from passbook.policies.exceptions import PolicyException from passbook.policies.exceptions import PolicyException
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
from passbook.core.models import Policy, User from passbook.core.models import Policy, User
from passbook.lib.utils.http import get_client_ip from passbook.lib.utils.http import get_client_ip
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
class ReputationPolicy(Policy): class ReputationPolicy(Policy):

View File

@ -1,4 +0,0 @@
"""autodiscover admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister("passbook_policies_sso")

View File

@ -1,21 +0,0 @@
"""Source API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS
from passbook.policies.sso.models import SSOLoginPolicy
class SSOLoginPolicySerializer(ModelSerializer):
"""SSO Login Policy Serializer"""
class Meta:
model = SSOLoginPolicy
fields = GENERAL_SERIALIZER_FIELDS
class SSOLoginPolicyViewSet(ModelViewSet):
"""Source Viewset"""
queryset = SSOLoginPolicy.objects.all()
serializer_class = SSOLoginPolicySerializer

View File

@ -1,11 +0,0 @@
"""passbook sso policy app config"""
from django.apps import AppConfig
class PassbookPoliciesSSOConfig(AppConfig):
"""passbook sso policy app config"""
name = "passbook.policies.sso"
label = "passbook_policies_sso"
verbose_name = "passbook Policies.SSO"

View File

@ -1,19 +0,0 @@
"""passbook Policy forms"""
from django import forms
from passbook.policies.forms import GENERAL_FIELDS
from passbook.policies.sso.models import SSOLoginPolicy
class SSOLoginPolicyForm(forms.ModelForm):
"""Edit SSOLoginPolicy instances"""
class Meta:
model = SSOLoginPolicy
fields = GENERAL_FIELDS
widgets = {
"name": forms.TextInput(),
"order": forms.NumberInput(),
}

View File

@ -1,25 +0,0 @@
"""sso models"""
from django.utils.translation import gettext as _
from passbook.core.models import Policy
from passbook.policies.struct import PolicyRequest, PolicyResult
class SSOLoginPolicy(Policy):
"""Policy that applies to users that have authenticated themselves through SSO"""
form = "passbook.policies.sso.forms.SSOLoginPolicyForm"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if user instance passes this policy"""
from passbook.factors.view import AuthenticationView
is_sso_login = request.user.session.get(
AuthenticationView.SESSION_IS_SSO_LOGIN, False
)
return PolicyResult(is_sso_login)
class Meta:
verbose_name = _("SSO Login Policy")
verbose_name_plural = _("SSO Login Policies")

View File

@ -1,7 +1,7 @@
"""policy structures""" """policy structures"""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, List from typing import TYPE_CHECKING, Tuple
from django.db.models import Model from django.db.models import Model
from django.http import HttpRequest from django.http import HttpRequest
@ -27,8 +27,8 @@ class PolicyRequest:
class PolicyResult: class PolicyResult:
"""Small data-class to hold policy results""" """Small data-class to hold policy results"""
passing: bool = False passing: bool
messages: List[str] = [] messages: Tuple[str]
def __init__(self, passing: bool, *messages: str): def __init__(self, passing: bool, *messages: str):
self.passing = passing self.passing = passing

View File

@ -3,7 +3,7 @@ from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import Policy from passbook.core.models import Policy
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
class WebhookPolicy(Policy): class WebhookPolicy(Policy):

View File

@ -15,24 +15,32 @@ from passbook.policies.engine import PolicyEngine
LOGGER = get_logger() LOGGER = get_logger()
def client_related_provider(client: Client) -> Optional[Provider]:
"""Lookup related Application from Client"""
# because oidc_provider is also used by app_gw, we can't be
# sure an OpenIDPRovider instance exists. hence we look through all related models
# and choose the one that inherits from Provider, which is guaranteed to
# have the application property
collector = Collector(using="default")
collector.collect([client])
for _, related in collector.data.items():
related_object = next(iter(related))
if isinstance(related_object, Provider):
return related_object
return None
def check_permissions( def check_permissions(
request: HttpRequest, user: User, client: Client request: HttpRequest, user: User, client: Client
) -> Optional[HttpResponse]: ) -> Optional[HttpResponse]:
"""Check permissions, used for """Check permissions, used for
https://django-oidc-provider.readthedocs.io/en/latest/ https://django-oidc-provider.readthedocs.io/en/latest/
sections/settings.html#oidc-after-userlogin-hook""" sections/settings.html#oidc-after-userlogin-hook"""
provider = client_related_provider(client)
if not provider:
return redirect("passbook_providers_oauth:oauth2-permission-denied")
try: try:
# because oidc_provider is also used by app_gw, we can't be application = provider.application
# sure an OpenIDPRovider instance exists. hence we look through all related models
# and choose the one that inherits from Provider, which is guaranteed to
# have the application property
collector = Collector(using="default")
collector.collect([client])
for _, related in collector.data.items():
related_object = next(iter(related))
if isinstance(related_object, Provider):
application = related_object.application
break
except Application.DoesNotExist: except Application.DoesNotExist:
return redirect("passbook_providers_oauth:oauth2-permission-denied") return redirect("passbook_providers_oauth:oauth2-permission-denied")
LOGGER.debug( LOGGER.debug(

View File

@ -14,12 +14,16 @@ class SAMLProviderSerializer(ModelSerializer):
fields = [ fields = [
"pk", "pk",
"name", "name",
"property_mappings", "processor_path",
"acs_url", "acs_url",
"audience", "audience",
"processor_path",
"issuer", "issuer",
"assertion_valid_for", "assertion_valid_not_before",
"assertion_valid_not_on_or_after",
"session_valid_not_on_or_after",
"property_mappings",
"digest_algorithm",
"signature_algorithm",
"signing", "signing",
"signing_cert", "signing_cert",
"signing_key", "signing_key",
@ -39,7 +43,7 @@ class SAMLPropertyMappingSerializer(ModelSerializer):
class Meta: class Meta:
model = SAMLPropertyMapping model = SAMLPropertyMapping
fields = ["pk", "name", "saml_name", "friendly_name", "values"] fields = ["pk", "name", "saml_name", "friendly_name", "expression"]
class SAMLPropertyMappingViewSet(ModelViewSet): class SAMLPropertyMappingViewSet(ModelViewSet):

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