Compare commits
473 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
5265064b10 | |||
ee8313142f | |||
45dd004d00 | |||
c4ebc5c085 | |||
31c42f1104 | |||
8b7f665c82 | |||
4915205678 | |||
07da5f7122 | |||
f91e02a0ec | |||
c62794c738 | |||
7664b428e7 | |||
9f3f34389e | |||
30ca926b38 | |||
eeb62f543f | |||
6a7ffa855e | |||
0c5aff21bc | |||
b0f426e51a | |||
ed0094eba2 | |||
4c1b6d5f48 | |||
41a7e1ef07 | |||
4ff41be74a | |||
7067d1f236 | |||
73116b9d1a | |||
147212a5f9 | |||
5502c319d4 | |||
d18b76a47d | |||
90ce704def | |||
790139f8bc | |||
f4be007803 | |||
448ca62661 | |||
4824e5c8ba | |||
c4facd53b4 | |||
ef913abc7a | |||
55fc5a6068 | |||
f22c89c998 | |||
2a78d2d0a0 | |||
b0ddc6a8c0 | |||
8080b0380e | |||
ddfa2abbaa | |||
5fc5e54f47 | |||
7462d56182 | |||
3f92d1c420 | |||
d4fa60f509 | |||
052bf88c3d | |||
07d047c887 | |||
23193314f1 | |||
1912b29dc5 | |||
46410428d9 | |||
1774e33c24 | |||
c961327d27 | |||
fe1ff7fc76 | |||
82d12ecfdf | |||
6ed822fa38 | |||
4d1658b35e | |||
08302a04bf | |||
8b4558fcd0 | |||
80a36a3441 | |||
b1b63fbb7f | |||
fe6bfb1620 | |||
e6d64bf5b3 | |||
270739a45a | |||
df8995deed | |||
fdfc6472d2 | |||
bc495828e7 | |||
fa138a273f | |||
5555dd4dde | |||
084ff2994f | |||
ee8a61d164 | |||
60a363e4bf | |||
e2804b9755 | |||
9330a03273 | |||
beabba2890 | |||
eeeb14a045 | |||
ff3f126832 | |||
73225917cd | |||
8b7a285f4a | |||
3a4c6a5d67 | |||
0664f0b6b2 | |||
fab9f1cb1b | |||
b3c54308b7 | |||
c1a72a0474 | |||
4377d1e86e | |||
cafe2f1e1f | |||
c329a724e8 | |||
969da05437 | |||
c61c84e9f3 | |||
e08c5ff875 | |||
d1fd616b8d | |||
cc4bbc049b | |||
24a3e787dd | |||
13a20478fd | |||
f58ee7fb52 | |||
4d45dc31a9 | |||
f6b5fbc2cf | |||
db69c3e38d | |||
5ba55356a9 | |||
6104b7c9ba | |||
bcef8369ff | |||
0fe555a76e | |||
c903c81bd5 | |||
2c43bfb13f | |||
24ecab0878 | |||
6285c79341 | |||
5f7c56fab4 | |||
6338b14a5b | |||
62ff3667f9 | |||
3b97eef641 | |||
b05d66466b | |||
220144c919 | |||
3b9402420d | |||
1324143146 | |||
595144cdd9 | |||
b3e714a741 | |||
2b3ae1e2d5 | |||
886845306a | |||
de6aaec088 | |||
137e0854ab | |||
b6a0e895bc | |||
05c791b1c0 | |||
669f3051e8 | |||
d945b56561 | |||
27333bbff1 | |||
80c800f233 | |||
5d17d3dd31 | |||
8c64fcd17f | |||
753110583e | |||
4206a8c43e | |||
7cc9ae45bc | |||
09ef58350c | |||
0b70007926 | |||
2c5d6e8187 | |||
10d3f9ac2f | |||
a6301055f0 | |||
9a1b8eb7c8 | |||
2ee45f388c | |||
9a9feea5ff | |||
8540e787d3 | |||
db26ce808e | |||
802d568273 | |||
7b6524357f | |||
83013f063d | |||
333ee3eed4 | |||
e68352b09c | |||
df1cb88abc | |||
5596caedbc | |||
7bd65120b9 | |||
227966e727 | |||
406f69080b | |||
2ce31dfaa5 | |||
b23afcfc3b | |||
7ed3ceb960 | |||
615cd7870d | |||
b907105f4a | |||
023423c6e7 | |||
a5319fc2fe | |||
8cfd3f9a2b | |||
814c797c64 | |||
776ad3cfbf | |||
74ddf70cb7 | |||
a7a839a29c | |||
9859c5db0a | |||
fe503c8de0 | |||
43a583e2d2 | |||
f289025d8e | |||
19cb310446 | |||
47f6d0ac59 | |||
922cbf932d | |||
c104eeebe6 | |||
80c3246333 | |||
461fed5567 | |||
d5f6714ed7 | |||
c42ed6bc99 | |||
57fed2b92b | |||
e45b33c6c2 | |||
137e90355b | |||
7500e622f6 | |||
d49c58f326 | |||
9814d3be03 | |||
fc9f86cccc | |||
6fd19c0a37 | |||
10cb412532 | |||
e12780f78f | |||
9dec13c225 | |||
69120da45c | |||
5b2bf7519a | |||
631cf77f89 | |||
6676e95011 | |||
3219cffb52 | |||
d4f149bc02 | |||
206cf4967d | |||
a67c53f46a | |||
e989c61793 | |||
99bab03cce | |||
a7567ad8c6 | |||
2ffa2fc6b8 | |||
8de87d9acb | |||
1d03b36750 | |||
8dc3c49a2f | |||
f6461b08d7 | |||
a3a3dde1c8 | |||
f111604b70 | |||
4315d1a03c | |||
9def45c8d7 | |||
358922b09b | |||
fbc3ac6b30 | |||
4f785da452 | |||
ff4bd1c91f | |||
7a96f9e894 | |||
c27d257146 | |||
9bccf9bb0a | |||
c0b05a62f4 | |||
c140c39d07 | |||
9a700e506b | |||
8e488670ad | |||
fd5b2298e5 | |||
caeaf8d5a9 | |||
c46f0781fc | |||
0aad0604d8 | |||
131c3fdb32 | |||
8a6009c278 | |||
3456527f10 | |||
28b913136d | |||
f700899640 | |||
5ba45d3037 | |||
212e966dd4 | |||
08c0eb2ec6 | |||
872ecd93a6 | |||
f8af9d6ce0 | |||
c3e43a7c2f | |||
273af0f1cb | |||
2a85e5ae87 | |||
114bb1b0bd | |||
97b5d120f8 | |||
e1f0fe45cb | |||
bc0f4973d8 | |||
5400882d78 | |||
8de66b27ad | |||
179f0097c0 | |||
b40bffdf38 | |||
2e9496bb74 | |||
d9d765c6cd | |||
10cc6856a4 | |||
813dd2894f | |||
80d90b91e8 | |||
fff05e35ac | |||
75bb59a22a | |||
3ce69bb391 | |||
4eb7c5f94b | |||
aac7e6be90 | |||
c77f4204c0 | |||
5f4452470b | |||
9a1270c693 | |||
7b9d1a1159 | |||
cdbe1f6161 | |||
e43db2e065 | |||
d1c74d2160 | |||
f2119ce567 | |||
2c4dcb9cf0 | |||
93b8266821 | |||
443797d9b0 | |||
a4365ca02c | |||
3750083667 | |||
66ef067ecf | |||
b489b0e691 | |||
f2154d9875 | |||
80a50f9bdb | |||
dc8b89a6b9 | |||
8df55f22aa | |||
f6c322be27 | |||
a144552059 | |||
535d529193 | |||
6ed2e137a2 | |||
45bd63c720 | |||
736e13fc35 | |||
966fff008c | |||
64f15eadbd | |||
81b66ecdcd | |||
53e5cf7826 | |||
82654b3fd9 | |||
9b72c604dd | |||
5fb1b8044c | |||
b8daab4377 | |||
c5b91bdae8 | |||
39a208c55f | |||
a5bfef9b6b | |||
f1f4cbef9b | |||
8388120b06 | |||
2bf96828f1 | |||
22838e66fe | |||
484dd6de09 | |||
b743736c26 | |||
af91e2079b | |||
cad1c17f14 | |||
120d32e4dc | |||
238b489e07 | |||
4daa70c894 | |||
f8599438df | |||
155c9a4c3f | |||
8433b5e583 | |||
dc5ba144f1 | |||
521a8b5356 | |||
3453077d7b | |||
70ede8581a | |||
6e9d297f02 | |||
6a7545fd43 | |||
a8926cbd07 | |||
64d7b009ab | |||
2b5fddb7bf | |||
b99d23c119 | |||
03905b74ff | |||
6b8a59cfbd | |||
d6fdcd3ef9 | |||
53ebc551d2 | |||
3d4f43d6e3 | |||
074cde7cd5 | |||
382e563590 | |||
ca61a7cc21 | |||
fa2870afe0 | |||
0f46207ea4 | |||
1e7d912144 | |||
f4a676e2fb | |||
b2c10e2387 | |||
8c329dca7d | |||
83da175749 | |||
995c87938f | |||
40678b2f84 | |||
8dbbe9102b | |||
2f51f354de | |||
04b815a33e | |||
2a4d68911b | |||
4d5a2d61ff | |||
efd88c27ad | |||
80d361ccd2 | |||
6ed4501615 | |||
8d34faa28e | |||
a3ae827839 | |||
88c1ad4c1c | |||
1147c4901b | |||
063181d7a7 | |||
1285ba6fbb | |||
a09a1793ec | |||
50caa3ac3e | |||
9440d24358 | |||
26bf6fd22f | |||
e2f836feae | |||
b6326f399c | |||
ea6a1422f7 | |||
8fd86a28ff | |||
d88283a7a9 | |||
32a15f84c0 | |||
93ba4b7f62 | |||
187780dab2 | |||
d988f37afc | |||
295c0bae3f | |||
38a22ddf13 | |||
d06f1abb89 | |||
027a64fad2 | |||
84fc54ddaa | |||
0b5caa85f5 | |||
14e0a17dbc | |||
3c04afa31f | |||
40a2a26904 | |||
c8b3c6e51a | |||
e0272a6422 | |||
b290bbf6d7 | |||
8d875cb01d | |||
36b1f8ba36 | |||
6c889eff27 | |||
9d8675e54b | |||
22ae986c0b | |||
2bef5f3911 | |||
3c2b8e5ee1 | |||
c96571bdba | |||
2dfd93afb1 | |||
f1d77d475c | |||
1d22e30c70 | |||
07b7951390 | |||
995615d0a0 | |||
ac273aab75 | |||
44cd03654d | |||
3e2375f970 | |||
38ad8e5fd3 | |||
c481558a46 | |||
e27a05a7fc | |||
e4886f0c6f | |||
8b2ce5476a | |||
1b82283a20 | |||
7f3d0113c2 | |||
0f6dd33a6b | |||
5b79b3fd22 | |||
d68c72f1fa | |||
9267d0c1dd | |||
865abc005a | |||
a2725d5b82 | |||
4a05bc6e02 | |||
4e8238603a | |||
ff25c1c057 | |||
78cddca0d7 | |||
4742ee1d93 | |||
0c2dc309e7 | |||
144935d10f | |||
74ad1b6759 | |||
591d2f89a1 | |||
7c353f9297 | |||
cd1af15c56 | |||
878169ea2e | |||
38dfb03668 | |||
e2631cec0e | |||
5dad853f8a | |||
9f00843441 | |||
f31cd7dec6 | |||
1c1afca31f | |||
fbd4bdef33 | |||
5b22f9b6c3 | |||
083e317028 | |||
95416623b3 | |||
813b2676de | |||
aeca66a288 | |||
04a5428148 | |||
73b173b92a | |||
7cbf20a71c | |||
7a98e6d92b | |||
49e915f98b | |||
3aa2f1e892 | |||
bc4b7ef44d | |||
9400b01a55 | |||
e57da71dcf | |||
7268afaaf9 | |||
205183445c | |||
a08bdfdbcd | |||
e6c47fee26 | |||
a5629c5155 | |||
41689fe3ce | |||
8e84208e2c | |||
32a48fa07a | |||
773a9c0692 | |||
8808e3afe0 | |||
ecea85f8ca | |||
5dfa141e35 | |||
447e81d0b8 | |||
e138076e1d | |||
721d133dc3 | |||
75b687ecbe | |||
bdd1863177 | |||
e5b85e8e6a | |||
d7481c9de7 | |||
571373866e | |||
e36d7928e4 | |||
2be026dd44 | |||
d5b9de3569 | |||
e22620b0ec | |||
ba74a3213d | |||
d9ecb7070d | |||
fc4a46bd9c | |||
78301b7bab | |||
7bf7bde856 | |||
9bdff14403 | |||
f124314eab | |||
684e4ffdcf | |||
d9ff5c69c8 | |||
8142e3df45 | |||
73920899de | |||
13666965a7 | |||
86f16e2781 | |||
2ed8e72c62 | |||
edeed18ae8 | |||
d24133d8a2 | |||
b9733e56aa | |||
cd34413914 | |||
c3a4a76d43 | |||
a59a29b256 | |||
dce1edbe53 | |||
264d43827a |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.7.9-beta
|
||||
current_version = 0.9.0-pre1
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
|
@ -3,3 +3,4 @@ helm
|
||||
passbook-ui
|
||||
static
|
||||
*.env.yml
|
||||
node_modules/
|
||||
|
20
.fossa.yml
Executable file
@ -0,0 +1,20 @@
|
||||
# Generated by FOSSA CLI (https://github.com/fossas/fossa-cli)
|
||||
# Visit https://fossa.com to learn more
|
||||
|
||||
version: 2
|
||||
cli:
|
||||
server: https://app.fossa.com
|
||||
fetcher: custom
|
||||
project: git@github.com:BeryJu/passbook.git
|
||||
analyze:
|
||||
modules:
|
||||
- name: static
|
||||
type: npm
|
||||
target: passbook/static/static
|
||||
path: passbook/static/static
|
||||
- name: .
|
||||
type: pip
|
||||
target: .
|
||||
path: .
|
||||
options:
|
||||
strategy: pipenv
|
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
custom: ["https://www.paypal.me/octocat"]
|
38
.github/workflows/ci-cleanup.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: passbook-ci-cleanup
|
||||
on:
|
||||
- delete
|
||||
|
||||
jobs:
|
||||
delete-server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete docker tag
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: curl
|
||||
-u $DOCKER_USERNAME:$DOCKER_PASSWORD
|
||||
-X "DELETE"
|
||||
"https://hub.docker.com/v2/repositories/$DOCKER_USERNAME/passbook/tags/${GITHUB_REF##*/}/"
|
||||
delete-gatekeeper:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete docker tag
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: curl
|
||||
-u $DOCKER_USERNAME:$DOCKER_PASSWORD
|
||||
-X "DELETE"
|
||||
"https://hub.docker.com/v2/repositories/$DOCKER_USERNAME/passbook-gatekeeper/tags/${GITHUB_REF##*/}/"
|
||||
delete-static:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete docker tag
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: curl
|
||||
-u $DOCKER_USERNAME:$DOCKER_PASSWORD
|
||||
-X "DELETE"
|
||||
"https://hub.docker.com/v2/repositories/$DOCKER_USERNAME/passbook-static/tags/${GITHUB_REF##*/}/"
|
151
.github/workflows/ci.yml
vendored
@ -14,15 +14,9 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip pipenv && pipenv install --dev
|
||||
run: sudo pip install -U wheel pipenv && pipenv install --dev
|
||||
- name: Lint with pylint
|
||||
run: pipenv run pylint passbook
|
||||
black:
|
||||
@ -31,15 +25,9 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip pipenv && pipenv install --dev
|
||||
run: sudo pip install -U wheel pipenv && pipenv install --dev
|
||||
- name: Lint with black
|
||||
run: pipenv run black --check passbook
|
||||
prospector:
|
||||
@ -48,15 +36,9 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip pipenv && pipenv install --dev
|
||||
run: sudo pip install -U wheel pipenv && pipenv install --dev && pipenv install --dev prospector --skip-lock
|
||||
- name: Lint with prospector
|
||||
run: pipenv run prospector
|
||||
bandit:
|
||||
@ -65,17 +47,27 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip pipenv && pipenv install --dev
|
||||
run: sudo pip install -U wheel pipenv && pipenv install --dev
|
||||
- name: Lint with bandit
|
||||
run: pipenv run bandit -r passbook
|
||||
pyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Install pyright
|
||||
run: npm install -g pyright
|
||||
- name: Show pyright version
|
||||
run: pyright --version
|
||||
- name: Install dependencies
|
||||
run: sudo pip install -U wheel pipenv && pipenv install --dev
|
||||
- name: Lint with pyright
|
||||
run: pipenv run pyright
|
||||
# Actual CI tests
|
||||
migrations:
|
||||
needs:
|
||||
@ -100,15 +92,9 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip pipenv && pipenv install --dev
|
||||
run: sudo pip install -U wheel pipenv && pipenv install --dev
|
||||
- name: Run migrations
|
||||
run: pipenv run ./manage.py migrate
|
||||
coverage:
|
||||
@ -134,14 +120,83 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip pipenv && pipenv install --dev
|
||||
run: sudo pip install -U wheel pipenv && pipenv install --dev
|
||||
- name: Run coverage
|
||||
run: pipenv run ./scripts/coverage.sh
|
||||
- name: Create XML Report
|
||||
run: pipenv run coverage xml
|
||||
- uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# Build
|
||||
build-server:
|
||||
needs:
|
||||
- migrations
|
||||
- coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Docker Login Registry
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:${GITHUB_REF##*/}
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry
|
||||
run: docker push beryju/passbook:${GITHUB_REF##*/}
|
||||
build-gatekeeper:
|
||||
needs:
|
||||
- migrations
|
||||
- coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Docker Login Registry
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||
- name: Building Docker Image
|
||||
run: |
|
||||
cd gatekeeper
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-gatekeeper:${GITHUB_REF##*/} \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry
|
||||
run: docker push beryju/passbook-gatekeeper:${GITHUB_REF##*/}
|
||||
build-static:
|
||||
needs:
|
||||
- migrations
|
||||
- coverage
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_DB: passbook
|
||||
POSTGRES_USER: passbook
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
redis:
|
||||
image: redis:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Docker Login Registry
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:${GITHUB_REF##*/}
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry
|
||||
run: docker push beryju/passbook-static:${GITHUB_REF##*/}
|
||||
|
33
.github/workflows/release.yml
vendored
@ -1,8 +1,6 @@
|
||||
name: passbook-release
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
release
|
||||
|
||||
jobs:
|
||||
# Build
|
||||
@ -18,13 +16,34 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.7.9-beta
|
||||
-t beryju/passbook:0.9.0-pre1
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.7.9-beta
|
||||
run: docker push beryju/passbook:0.9.0-pre1
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-gatekeeper:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Docker Login Registry
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||
- name: Building Docker Image
|
||||
run: |
|
||||
cd gatekeeper
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-gatekeeper:0.9.0-pre1 \
|
||||
-t beryju/passbook-gatekeeper:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-gatekeeper:0.9.0-pre1
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-gatekeeper:latest
|
||||
build-static:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
@ -47,11 +66,11 @@ jobs:
|
||||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:0.7.9-beta
|
||||
-t beryju/passbook-static:0.9.0-pre1
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.7.9-beta
|
||||
run: docker push beryju/passbook-static:0.9.0-pre1
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
|
11
.github/workflows/tag.yml
vendored
@ -31,6 +31,13 @@ jobs:
|
||||
helm dependency update helm/
|
||||
helm package helm/
|
||||
mv passbook-*.tgz passbook-chart.tgz
|
||||
- name: Extract verison number
|
||||
id: get_version
|
||||
uses: actions/github-script@0.2.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1.0.0
|
||||
@ -38,10 +45,10 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
release_name: Release ${{ steps.get_version.outputs.result }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
- name: Create Release from Tag
|
||||
- name: Upload packaged Helm Chart
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
env:
|
||||
|
6
.isort.cfg
Normal file
@ -0,0 +1,6 @@
|
||||
[settings]
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
@ -8,4 +8,4 @@ ignore-paths:
|
||||
- node_modules
|
||||
|
||||
uses:
|
||||
- django
|
||||
- django
|
||||
|
10
.pylintrc
@ -1,13 +1,9 @@
|
||||
[MASTER]
|
||||
|
||||
disable=redefined-outer-name,arguments-differ,no-self-use,cyclic-import,fixme,locally-disabled,unpacking-non-sequence,too-many-ancestors,too-many-branches,too-few-public-methods,import-outside-toplevel,bad-continuation
|
||||
disable=redefined-outer-name,arguments-differ,no-self-use,cyclic-import,fixme,locally-disabled,too-many-ancestors,too-few-public-methods,import-outside-toplevel,bad-continuation,signature-differs
|
||||
load-plugins=pylint_django,pylint.extensions.bad_builtin
|
||||
extension-pkg-whitelist=lxml
|
||||
const-rgx=[a-zA-Z0-9_]{1,40}$
|
||||
ignored-modules=django-otp
|
||||
jobs=4
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=20
|
||||
jobs=12
|
||||
ignore=migrations
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.7-slim-buster as locker
|
||||
FROM python:3.8-slim-buster as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
@ -9,7 +9,7 @@ RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
|
||||
FROM python:3.7-slim-buster
|
||||
FROM python:3.8-slim-buster
|
||||
|
||||
COPY --from=locker /app/requirements.txt /app/
|
||||
COPY --from=locker /app/requirements-dev.txt /app/
|
||||
@ -25,7 +25,11 @@ RUN apt-get update && \
|
||||
COPY ./passbook/ /app/passbook
|
||||
COPY ./manage.py /app/
|
||||
COPY ./docker/uwsgi.ini /app/
|
||||
COPY ./docker/bootstrap.sh /bootstrap.sh
|
||||
COPY ./docker/wait_for_db.py /app/wait_for_db.py
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
USER passbook
|
||||
|
||||
ENTRYPOINT [ "/bootstrap.sh" ]
|
||||
|
3
Pipfile
@ -42,7 +42,7 @@ swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
python_version = "3.8"
|
||||
|
||||
[dev-packages]
|
||||
autopep8 = "*"
|
||||
@ -51,7 +51,6 @@ bumpversion = "*"
|
||||
colorama = "*"
|
||||
coverage = "*"
|
||||
django-debug-toolbar = "*"
|
||||
prospector = "*"
|
||||
pylint = "*"
|
||||
pylint-django = "*"
|
||||
unittest-xml-reporting = "*"
|
||||
|
1092
Pipfile.lock
generated
87
README.md
@ -1,13 +1,92 @@
|
||||
# passbook
|
||||
<img src="passbook/static/static/passbook/logo.svg" height="50" alt="passbook logo"><img src="passbook/static/static/passbook/brand_inverted.svg" height="50" alt="passbook">
|
||||
|
||||
## Quick instance
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## What is passbook?
|
||||
|
||||
passbook is an open-source Identity Provider focused on flexibility and versatility. You can use passbook in an existing environment to add support for new protocols. passbook is also a great solution for implementing signup/recovery/etc in your application, so you don't have to deal with it.
|
||||
|
||||
## Installation
|
||||
|
||||
For small/test setups it is recommended to use docker-compose.
|
||||
|
||||
```
|
||||
export PASSBOOK_DOMAIN=domain.tld
|
||||
wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
||||
# Optionally enable Error-reporting
|
||||
# export PASSBOOK_ERROR_REPORTING=true
|
||||
# Optionally deploy a different version
|
||||
# export PASSBOOK_TAG=0.8.15-beta
|
||||
# If this is a productive installation, set a different PostgreSQL Password
|
||||
# export PG_PASS=$(pwgen 40 1)
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker-compose exec server ./manage.py migrate
|
||||
docker-compose exec server ./manage.py createsuperuser
|
||||
```
|
||||
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org//installation/kubernetes/)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
## Development
|
||||
|
||||
To develop on passbook, you need a system with Python 3.7+ (3.8 is recommended). passbook uses [pipenv](https://pipenv.pypa.io/en/latest/) for managing dependencies.
|
||||
|
||||
To get started, run
|
||||
|
||||
```
|
||||
python3 -m pip install pipenv
|
||||
git clone https://github.com/BeryJu/passbook.git
|
||||
cd passbook
|
||||
pipenv shell
|
||||
pipenv sync -d
|
||||
```
|
||||
|
||||
Since passbook uses PostgreSQL-specific fields, you also need a local PostgreSQL instance to develop. passbook also uses redis for caching and message queueing.
|
||||
For these databases you can use [Postgres.app](https://postgresapp.com/) and [Redis.app](https://jpadilla.github.io/redisapp/) on macOS or use it via docker-comppose:
|
||||
|
||||
```yaml
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
container_name: postgres
|
||||
image: postgres:11
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 127.0.0.1:5432:5432
|
||||
restart: always
|
||||
redis:
|
||||
container_name: redis
|
||||
image: redis
|
||||
ports:
|
||||
- 127.0.0.1:6379:6379
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
driver: local
|
||||
```
|
||||
|
||||
To tell passbook about these databases, create a file in the project root called `local.env.yml` with the following contents:
|
||||
|
||||
```yaml
|
||||
debug: true
|
||||
postgresql:
|
||||
user: postgres
|
||||
|
||||
log_level: debug
|
||||
error_reporting: false
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
See [SECURITY.md](SECURITY.md)
|
||||
|
13
SECURITY.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As passbook is currently in a pre-stable, only the latest "stable" version is supported. After passbook 1.0, this will change.
|
||||
|
||||
| Version | Supported |
|
||||
| -------- | ------------------ |
|
||||
| 0.8.15 | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, send am email to [security@beryju.org](mailto:security@beryju.org)
|
@ -21,12 +21,11 @@ services:
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
server:
|
||||
image: beryju/passbook:${SERVER_TAG:-latest}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-latest}
|
||||
command:
|
||||
- uwsgi
|
||||
- uwsgi.ini
|
||||
environment:
|
||||
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
|
||||
- PASSBOOK_REDIS__HOST=redis
|
||||
- PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false}
|
||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||
@ -40,7 +39,7 @@ services:
|
||||
- traefik.docker.network=internal
|
||||
- traefik.frontend.rule=PathPrefix:/
|
||||
worker:
|
||||
image: beryju/passbook:${SERVER_TAG:-latest}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-latest}
|
||||
command:
|
||||
- celery
|
||||
- worker
|
||||
@ -54,7 +53,6 @@ services:
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
environment:
|
||||
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
|
||||
- PASSBOOK_REDIS__HOST=redis
|
||||
- PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false}
|
||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||
@ -65,7 +63,7 @@ services:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.frontend.rule=PathPrefix:/static, /robots.txt
|
||||
- traefik.port=8080
|
||||
- traefik.port=80
|
||||
- traefik.docker.network=internal
|
||||
traefik:
|
||||
image: traefik:1.7
|
||||
|
3
docker/bootstrap.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash -ex
|
||||
/app/wait_for_db.py
|
||||
"$@"
|
@ -7,4 +7,4 @@ threads = 2
|
||||
enable-threads = true
|
||||
uid = passbook
|
||||
gid = passbook
|
||||
disable-logging=True
|
||||
disable-logging = True
|
||||
|
41
docker/wait_for_db.py
Executable file
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python
|
||||
"""This file needs to be run from the root of the project to correctly
|
||||
import passbook. This is done by the dockerfile."""
|
||||
from time import sleep
|
||||
|
||||
from psycopg2 import OperationalError, connect
|
||||
from redis import Redis
|
||||
from redis.exceptions import RedisError
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
while True:
|
||||
try:
|
||||
conn = connect(
|
||||
dbname=CONFIG.y("postgresql.name"),
|
||||
user=CONFIG.y("postgresql.user"),
|
||||
password=CONFIG.y("postgresql.password"),
|
||||
host=CONFIG.y("postgresql.host"),
|
||||
)
|
||||
conn.cursor()
|
||||
break
|
||||
except OperationalError:
|
||||
sleep(1)
|
||||
LOGGER.warning("PostgreSQL Connection failed, retrying...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
redis = Redis(
|
||||
host=CONFIG.y("redis.host"),
|
||||
port=6379,
|
||||
db=CONFIG.y("redis.message_queue_db"),
|
||||
password=CONFIG.y("redis.password"),
|
||||
)
|
||||
redis.ping()
|
||||
break
|
||||
except RedisError:
|
||||
sleep(1)
|
||||
LOGGER.warning("Redis Connection failed, retrying...")
|
@ -1,14 +0,0 @@
|
||||
FROM python:3.7-slim-buster as builder
|
||||
|
||||
WORKDIR /mkdocs
|
||||
|
||||
RUN pip install mkdocs mkdocs-material
|
||||
|
||||
COPY docs/ docs
|
||||
COPY mkdocs.yml .
|
||||
|
||||
RUN mkdocs build
|
||||
|
||||
FROM nginx
|
||||
|
||||
COPY --from=builder /mkdocs/site /usr/share/nginx/html
|
3
docs/build.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash -x
|
||||
pip install -U mkdocs mkdocs-material
|
||||
mkdocs gh-deploy
|
55
docs/expressions/index.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Expressions
|
||||
|
||||
Expressions allow you to write custom Logic using Python code.
|
||||
|
||||
Expressions are used in different places throughout passbook, and can do different things.
|
||||
|
||||
!!! info
|
||||
These functions/objects are available wherever expressions are used. For more specific information, see [Expression Policies](../policies/expression.md) and [Property Mappings](../property-mappings/expression.md)
|
||||
|
||||
## Global objects
|
||||
|
||||
- `pb_logger`: structlog BoundLogger. ([ref](https://www.structlog.org/en/stable/api.html#structlog.BoundLogger))
|
||||
- `requests`: requests Session object. ([ref](https://requests.readthedocs.io/en/master/user/advanced/))
|
||||
|
||||
## Generally available functions
|
||||
|
||||
### `regex_match(value: Any, regex: str) -> bool`
|
||||
|
||||
Check if `value` matches Regular Expression `regex`.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
return regex_match(request.user.username, '.*admin.*')
|
||||
```
|
||||
|
||||
### `regex_replace(value: Any, regex: str, repl: str) -> str`
|
||||
|
||||
Replace anything matching `regex` within `value` with `repl` and return it.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
user_email_local = regex_replace(request.user.email, '(.+)@.+', '')
|
||||
```
|
||||
|
||||
### `pb_is_group_member(user: User, **group_filters) -> bool`
|
||||
|
||||
Check if `user` is member of a group matching `**group_filters`.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
return pb_is_group_member(request.user, name="test_group")
|
||||
```
|
||||
|
||||
### `pb_user_by(**filters) -> Optional[User]`
|
||||
|
||||
Fetch a user matching `**filters`. Returns None if no user was found.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
other_user = pb_user_by(username="other_user")
|
||||
```
|
21
docs/expressions/reference/user-object.md
Normal file
@ -0,0 +1,21 @@
|
||||
# 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
|
||||
|
||||
```python
|
||||
for group in user.groups.all():
|
||||
yield group.name
|
||||
```
|
@ -1,23 +0,0 @@
|
||||
# Factors
|
||||
|
||||
A factor represents a single authenticating factor for a user. Common examples of this would be a password or an OTP. These factors can be combined in any order, and can be dynamically enabled using policies.
|
||||
|
||||
## Password Factor
|
||||
|
||||
This is the standard Password Factor. It allows you to select which Backend the password is checked with. here you can also specify which Policies are used to check the password. You can also specify which Factors a User has to pass to recover their account.
|
||||
|
||||
## Dummy Factor
|
||||
|
||||
This factor waits a random amount of time. Mostly used for debugging.
|
||||
|
||||
## E-Mail Factor
|
||||
|
||||
This factor is mostly for recovery, and used in conjunction with the Password Factor.
|
||||
|
||||
## OTP Factor
|
||||
|
||||
This is your typical One-Time Password implementation, compatible with Authy and Google Authenticator. You can enfore this Factor so that every user has to configure it, or leave it optional.
|
||||
|
||||
## Captcha Factor
|
||||
|
||||
While this factor doesn't really authenticate a user, it is part of the Authentication Flow. passbook uses Google's reCaptcha implementation.
|
36
docs/flow/examples/login.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Login Flow
|
||||
|
||||
This document describes how a simple authentication flow can be created.
|
||||
|
||||
This flow is created automatically when passbook is installed.
|
||||
|
||||
1. Create an **Identification** stage
|
||||
|
||||
> Here you can select whichever fields the user can identify themselves with
|
||||
> Select the Template **Default Login**, as this template shows the (optional) Flows
|
||||
> Here you can also link optional enrollment and recovery flows.
|
||||
|
||||
2. Create a **Password** stage
|
||||
|
||||
> Select the Backend you want the password to be checked against. Select "passbook-internal Userdatabase".
|
||||
|
||||
3. Create a **User Login** stage
|
||||
|
||||
> This stage doesn't have any options.
|
||||
|
||||
4. Create a flow
|
||||
|
||||
> Create a flow with the delegation of **Authentication**
|
||||
> Assign a name and a slug. The slug is used in the URL when the flow is executed.
|
||||
|
||||
5. Bind the stages to the flow
|
||||
|
||||
> Bind the **Identification** Stage with an order of 0
|
||||
> Bind the **Password** Stage with an order of 1
|
||||
> Bind the **User Login** Stage with an order of 2
|
||||
|
||||

|
||||
|
||||
!!! notice
|
||||
|
||||
This flow can used by any user, authenticated and un-authenticated. This means any authenticated user that visits this flow can login again.
|
BIN
docs/flow/examples/login.png
Normal file
After Width: | Height: | Size: 110 KiB |
45
docs/flow/flows.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Flows
|
||||
|
||||
Flows are a method of describing a sequence of stages. A stage represents a single verification or logic step. They are used to authenticate users, enroll them, and more.
|
||||
|
||||
Upon Flow execution, a plan is generated, which contains all stages. This means upon execution, all attached policies are evaluated. This behaviour can be altered by enabling the **Re-evaluate Policies** option on the binding.
|
||||
|
||||
To determine which flow is linked, passbook searches all Flows with the required designation and chooses the first instance the current user has access to.
|
||||
|
||||
## Permissions
|
||||
|
||||
Flows can have policies assigned to them, which determines if the current user is allowed to see and use this flow.
|
||||
|
||||
## Designation
|
||||
|
||||
Flows are designated for a single Purpose. This designation changes when a Flow is used. The following designations are available:
|
||||
|
||||
### Authentication
|
||||
|
||||
This is designates a flow to be used for authentication.
|
||||
|
||||
The authentication flow should always contain a [**User Login**](stages/user_login.md) stage, which attaches the staged user to the current session.
|
||||
|
||||
### Invalidation
|
||||
|
||||
This designates a flow to be used for the invalidation of a session.
|
||||
|
||||
This stage should always contain a [**User Logout**](stages/user_logout.md) stage, which resets the current session.
|
||||
|
||||
### Enrollment
|
||||
|
||||
This designates a flow for enrollment. This flow can contain any amount of Prompt stages, E-Mail verification or Captchas. At the end to create the user, you can use the [**User Write**](stages/user_write.md) stage, which either updates the currently staged user, or if none exists, creates a new one.
|
||||
|
||||
### Unenrollment
|
||||
|
||||
This designates a flow for unenrollment. This flow can contain any amount of verification, like [**E-Mail**](stages/email/index.md) or [**Captcha**](stages/captcha/index.md). To finally delete the account, use the [**User Delete**](stages/user_delete.md) stage.
|
||||
|
||||
### Recovery
|
||||
|
||||
This designates a flow for recovery. This flow normally contains an [**Identification**](stages/identification/index.md) stage to find the user. Then it can contain any amount of verification, like [**E-Mail**](stages/email/index.md) or [**Captcha**](stages/captcha/index.md).
|
||||
Afterwards, use the [**Prompt**](stages/prompt/index.md) stage to ask the user for a new password and use [**User Write**](stages/user_write.md) to update the password.
|
||||
|
||||
### Change Password
|
||||
|
||||
This designates a flow for password changing. This flow can contain any amount of verification, like [**E-Mail**](stages/email/index.md) or [**Captcha**](stages/captcha/index.md).
|
||||
Afterwards, use the [**Prompt**](stages/prompt/index.md) stage to ask the user for a new password and use [**User Write**](stages/user_write.md) to update the password.
|
BIN
docs/flow/stages/captcha/captcha-admin.png
Normal file
After Width: | Height: | Size: 140 KiB |
7
docs/flow/stages/captcha/index.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Captcha stage
|
||||
|
||||
This stage adds a form of verification using [Google's ReCaptcha](https://www.google.com/recaptcha/intro/v3.html).
|
||||
|
||||
This stage has two required fields. You need a Public and a Private key, both of which you can acquire at https://www.google.com/recaptcha/admin.
|
||||
|
||||

|
BIN
docs/flow/stages/dummy/dummy.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
5
docs/flow/stages/dummy/index.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Dummy stage
|
||||
|
||||
This stage is used for development, and has no function. It presents the User with a form, that requires a single confirmation.
|
||||
|
||||

|
BIN
docs/flow/stages/email/email-recovery.png
Normal file
After Width: | Height: | Size: 51 KiB |
5
docs/flow/stages/email/index.md
Normal file
@ -0,0 +1,5 @@
|
||||
# E-Mail
|
||||
|
||||
This stage can be used for E-Mail verification. passbook's background worker will send an E-Mail using the specified connection details. When an E-Mail can't be delivered, it is automatically periodically retried.
|
||||
|
||||

|
25
docs/flow/stages/identification/index.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Identification
|
||||
|
||||
This stage provides a ready-to-go form for users to identify themselves.
|
||||
|
||||
## Options
|
||||
|
||||
### User Fields
|
||||
|
||||
Select which fields the user can use to identify themselves. Multiple fields can be specified and separated with a comma.
|
||||
Valid choices:
|
||||
|
||||
- email
|
||||
- username
|
||||
|
||||
### Template
|
||||
|
||||
This specifies which template is rendered. Currently there are two templates.
|
||||
|
||||
The `Login` template shows configured Sources below the login form, as well as linking to the defined Enrollment and Recovery flows.
|
||||
|
||||
The `Recovery` template shows only the form.
|
||||
|
||||
### Enrollment/Recovery Flow
|
||||
|
||||
These fields specify if and which flows are linked on the form. The enrollment flow is linked as `Need an account? Sign up.`, and the recovery flow is linked as `Forgot username or password?`.
|
7
docs/flow/stages/invitation/index.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Invitation Stage
|
||||
|
||||
This stage can be used to invite users. You can use this enroll users with preset values.
|
||||
|
||||
If the option `Continue Flow without Invitation`, this stage will continue when no invitation token is present.
|
||||
|
||||
If you want to check if a user has used an invitation within a policy, you can check `request.context.invitation_in_effect`.
|
7
docs/flow/stages/otp/index.md
Normal file
@ -0,0 +1,7 @@
|
||||
# OTP Stage
|
||||
|
||||
This stage offers a generic Time-based One-time Password authentication step.
|
||||
|
||||
You can optionally enforce this step, which will force every user without OTP setup to configure it.
|
||||
|
||||
This stage uses a 6-digit Code with a 30 second time-drift. This is currently not changeable.
|
3
docs/flow/stages/password/index.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Password Stage
|
||||
|
||||
This is a generic password prompt, which authenticates the currently `pending_user`. This stage allows the selection of the Backend the user is authenticated against.
|
42
docs/flow/stages/prompt/index.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Prompt Stage
|
||||
|
||||
This stage is used to show the user arbitrary prompts.
|
||||
|
||||
## Prompt
|
||||
|
||||
The prompt can be any of the following types:
|
||||
|
||||
| | |
|
||||
|----------|------------------------------------------------------------------|
|
||||
| text | Arbitrary text, no client-side validation is done. |
|
||||
| email | E-Mail input, requires a valid E-Mail adress |
|
||||
| password | Password Input |
|
||||
| number | Number Input, any number is allowed |
|
||||
| checkbox | Simple Checkbox |
|
||||
| hidden | Hidden Input field, allows for the pre-setting of default values |
|
||||
|
||||
A Prompt has the following attributes:
|
||||
|
||||
### `field_key`
|
||||
|
||||
HTML name used for the prompt. This key is also used to later retrieve the data in expression policies:
|
||||
|
||||
```python
|
||||
request.context.get('prompt_data').get('<field_key>')
|
||||
```
|
||||
|
||||
### `label`
|
||||
|
||||
Label used to describe the Field. This might not be shown depending on the template selected.
|
||||
|
||||
### `required`
|
||||
|
||||
Flag that decides whether or not this field is required.
|
||||
|
||||
### `placeholder`
|
||||
|
||||
Field placeholder, shown within the input field. This field is also used by the `hidden` type as the actual value.
|
||||
|
||||
### `order`
|
||||
|
||||
Numerical index of the prompt. This applies to all stages this prompt is a part of.
|
16
docs/flow/stages/prompt/validation.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Prompt Validation
|
||||
|
||||
Further validation of prompts can be done using policies.
|
||||
|
||||
To validate that two password fields are identical, create the following expression policy:
|
||||
|
||||
```python
|
||||
if request.context.get('prompt_data').get('password') == request.context.get('prompt_data').get('password_repeat'):
|
||||
return True
|
||||
|
||||
pb_message("Passwords don't match.")
|
||||
return False
|
||||
```
|
||||
This policy expects you two have two password fields with `field_key` set to `password` and `password_repeat`.
|
||||
|
||||
Afterwards bind this policy to the prompt stage you want to validate.
|
8
docs/flow/stages/user_delete.md
Normal file
@ -0,0 +1,8 @@
|
||||
# User Delete Stage
|
||||
|
||||
!!! danger
|
||||
This stage deletes the `pending_user` without any confirmation. You have to make sure the user is aware of this.
|
||||
|
||||
This stage is intended for an unenrollment flow. It deletes the currently pending user.
|
||||
|
||||
The pending user is also removed from the current session.
|
5
docs/flow/stages/user_login.md
Normal file
@ -0,0 +1,5 @@
|
||||
# User Login Stage
|
||||
|
||||
This stage attaches a currently pending user to the current session.
|
||||
|
||||
It can be used after `user_write` during an enrollment flow, or after a `password` stage during an authentication flow.
|
3
docs/flow/stages/user_logout.md
Normal file
@ -0,0 +1,3 @@
|
||||
# User Logout Stage
|
||||
|
||||
Opposite stage of [User Login Stages](user_login.md). It removes the user from the current session.
|
3
docs/flow/stages/user_write.md
Normal file
@ -0,0 +1,3 @@
|
||||
# User Write Stage
|
||||
|
||||
This stages writes data from the current context to the current pending user. If no user is pending, a new one is created.
|
@ -1,2 +1,2 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="270px" height="20px" viewBox="0 0 270 10" enable-background="new 0 0 270 10" xml:space="preserve"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#fff;}</style></defs><g class="cls-1"><path class="cls-2" d="M1.65,11V2.45H2.87V3a2.81,2.81,0,0,1,.47-.45A1.13,1.13,0,0,1,4,2.38,1.11,1.11,0,0,1,5.1,3a1.55,1.55,0,0,1,.16.5,5.61,5.61,0,0,1,0,.81V6.58c0,.45,0,.77,0,1a1.17,1.17,0,0,1-.55.9,1.23,1.23,0,0,1-.7.16,1.35,1.35,0,0,1-.64-.16A1.53,1.53,0,0,1,2.89,8h0v3ZM4.08,4.43a1.21,1.21,0,0,0-.14-.6.51.51,0,0,0-.46-.22A.54.54,0,0,0,3,3.82a.8.8,0,0,0-.17.54V6.73A.68.68,0,0,0,3,7.2a.6.6,0,0,0,.44.18A.53.53,0,0,0,4,7.17a1,1,0,0,0,.12-.5Z"/><path class="cls-2" d="M8.63,8.54V7.91h0a2.24,2.24,0,0,1-.48.52,1.13,1.13,0,0,1-.69.18A1.39,1.39,0,0,1,7,8.54a1.09,1.09,0,0,1-.43-.24,1.32,1.32,0,0,1-.33-.49A2.33,2.33,0,0,1,6.11,7a4.89,4.89,0,0,1,.08-.91,1.51,1.51,0,0,1,.31-.65,1.44,1.44,0,0,1,.59-.38A3.19,3.19,0,0,1,8,4.93h.59V4.33a1,1,0,0,0-.13-.52A.52.52,0,0,0,8,3.61a.71.71,0,0,0-.44.15.78.78,0,0,0-.26.46H6.13A2,2,0,0,1,6.69,2.9a1.73,1.73,0,0,1,.57-.38A2,2,0,0,1,8,2.38a2.18,2.18,0,0,1,.72.12,1.71,1.71,0,0,1,.59.36,2,2,0,0,1,.38.6,2.18,2.18,0,0,1,.14.84V8.54Zm0-2.62-.34,0a1.2,1.2,0,0,0-.67.18.76.76,0,0,0-.29.68.89.89,0,0,0,.17.56A.55.55,0,0,0,8,7.53a.63.63,0,0,0,.49-.2.91.91,0,0,0,.17-.58Z"/><path class="cls-2" d="M13,4.16a.59.59,0,0,0-.2-.47.65.65,0,0,0-.42-.16.59.59,0,0,0-.45.19.66.66,0,0,0-.15.43.8.8,0,0,0,.08.33.85.85,0,0,0,.44.29l.71.29a1.73,1.73,0,0,1,.95.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.56,1.56,0,0,1-.58.39,1.88,1.88,0,0,1-2-.32,1.58,1.58,0,0,1-.4-.57,1.81,1.81,0,0,1-.17-.8h1.15a1.11,1.11,0,0,0,.17.47.56.56,0,0,0,.49.22.71.71,0,0,0,.47-.18A.59.59,0,0,0,13,6.8a.69.69,0,0,0-.13-.43,1.08,1.08,0,0,0-.48-.32l-.59-.21a2.08,2.08,0,0,1-.9-.64,1.66,1.66,0,0,1-.33-1,1.89,1.89,0,0,1,.14-.72,1.78,1.78,0,0,1,.4-.57,1.5,1.5,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.6,1.6,0,0,1,.54.38,1.85,1.85,0,0,1,.36.57,1.82,1.82,0,0,1,.13.7Z"/><path class="cls-2" d="M17.2,4.16a.63.63,0,0,0-.2-.47.69.69,0,0,0-.43-.16.55.55,0,0,0-.44.19.62.62,0,0,0-.16.43.68.68,0,0,0,.09.33.81.81,0,0,0,.43.29l.72.29a1.7,1.7,0,0,1,.94.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.61,1.61,0,0,1-.57.39,1.81,1.81,0,0,1-.74.15,1.76,1.76,0,0,1-1.24-.47,1.61,1.61,0,0,1-.41-.57,2,2,0,0,1-.17-.8h1.15a1.12,1.12,0,0,0,.18.47.53.53,0,0,0,.48.22.72.72,0,0,0,.48-.18.59.59,0,0,0,.21-.48.69.69,0,0,0-.14-.43,1,1,0,0,0-.48-.32l-.58-.21a2.06,2.06,0,0,1-.91-.64,1.66,1.66,0,0,1-.33-1A1.89,1.89,0,0,1,15,3.44a1.78,1.78,0,0,1,.4-.57,1.58,1.58,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.75,1.75,0,0,1,.55.38,1.85,1.85,0,0,1,.36.57,2,2,0,0,1,.13.7Z"/><path class="cls-2" d="M19.2,8.54V0h1.22V3h0a1.53,1.53,0,0,1,.48-.47,1.39,1.39,0,0,1,.65-.16,1.26,1.26,0,0,1,.69.16,1.35,1.35,0,0,1,.4.39,1.18,1.18,0,0,1,.15.51,7.72,7.72,0,0,1,0,1V6.73a5.56,5.56,0,0,1-.05.8,1.56,1.56,0,0,1-.15.5,1.12,1.12,0,0,1-1.07.58,1.15,1.15,0,0,1-.7-.18A3.79,3.79,0,0,1,20.42,8v.55Zm2.44-4.21a1,1,0,0,0-.13-.51A.5.5,0,0,0,21,3.61a.57.57,0,0,0-.44.18.66.66,0,0,0-.18.48V6.63a.83.83,0,0,0,.17.54.52.52,0,0,0,.45.21.49.49,0,0,0,.45-.22,1.11,1.11,0,0,0,.15-.6Z"/><path class="cls-2" d="M23.76,4.49a4.83,4.83,0,0,1,0-.68A1.55,1.55,0,0,1,24,3.26a1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24A1.59,1.59,0,0,1,24,7.73a1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1,0-.68ZM25,6.69a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17A.55.55,0,0,0,26,7.21a.72.72,0,0,0,.16-.52V4.3A.74.74,0,0,0,26,3.78a.55.55,0,0,0-.44-.17.53.53,0,0,0-.43.17A.74.74,0,0,0,25,4.3Z"/><path class="cls-2" d="M28.2,4.49a4.83,4.83,0,0,1,.05-.68,1.55,1.55,0,0,1,.18-.55,1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24,1.59,1.59,0,0,1-.62-.64,1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1-.05-.68Zm1.22,2.2a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17.55.55,0,0,0,.44-.17.72.72,0,0,0,.16-.52V4.3a.74.74,0,0,0-.16-.52A.55.55,0,0,0,30,3.61a.53.53,0,0,0-.43.17.74.74,0,0,0-.17.52Z"/><path class="cls-2" d="M32.75,8.54V0H34V5.11h0l1.47-2.66H36.7L35.24,4.93,37,8.54H35.66l-1.1-2.63L34,6.83V8.54Z"/></g></svg>
|
||||
width="120px" height="20px" viewBox="15 0 10 10" enable-background="new 0 0 270 10" xml:space="preserve"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#fff;}</style></defs><g class="cls-1"><path class="cls-2" d="M1.65,11V2.45H2.87V3a2.81,2.81,0,0,1,.47-.45A1.13,1.13,0,0,1,4,2.38,1.11,1.11,0,0,1,5.1,3a1.55,1.55,0,0,1,.16.5,5.61,5.61,0,0,1,0,.81V6.58c0,.45,0,.77,0,1a1.17,1.17,0,0,1-.55.9,1.23,1.23,0,0,1-.7.16,1.35,1.35,0,0,1-.64-.16A1.53,1.53,0,0,1,2.89,8h0v3ZM4.08,4.43a1.21,1.21,0,0,0-.14-.6.51.51,0,0,0-.46-.22A.54.54,0,0,0,3,3.82a.8.8,0,0,0-.17.54V6.73A.68.68,0,0,0,3,7.2a.6.6,0,0,0,.44.18A.53.53,0,0,0,4,7.17a1,1,0,0,0,.12-.5Z"/><path class="cls-2" d="M8.63,8.54V7.91h0a2.24,2.24,0,0,1-.48.52,1.13,1.13,0,0,1-.69.18A1.39,1.39,0,0,1,7,8.54a1.09,1.09,0,0,1-.43-.24,1.32,1.32,0,0,1-.33-.49A2.33,2.33,0,0,1,6.11,7a4.89,4.89,0,0,1,.08-.91,1.51,1.51,0,0,1,.31-.65,1.44,1.44,0,0,1,.59-.38A3.19,3.19,0,0,1,8,4.93h.59V4.33a1,1,0,0,0-.13-.52A.52.52,0,0,0,8,3.61a.71.71,0,0,0-.44.15.78.78,0,0,0-.26.46H6.13A2,2,0,0,1,6.69,2.9a1.73,1.73,0,0,1,.57-.38A2,2,0,0,1,8,2.38a2.18,2.18,0,0,1,.72.12,1.71,1.71,0,0,1,.59.36,2,2,0,0,1,.38.6,2.18,2.18,0,0,1,.14.84V8.54Zm0-2.62-.34,0a1.2,1.2,0,0,0-.67.18.76.76,0,0,0-.29.68.89.89,0,0,0,.17.56A.55.55,0,0,0,8,7.53a.63.63,0,0,0,.49-.2.91.91,0,0,0,.17-.58Z"/><path class="cls-2" d="M13,4.16a.59.59,0,0,0-.2-.47.65.65,0,0,0-.42-.16.59.59,0,0,0-.45.19.66.66,0,0,0-.15.43.8.8,0,0,0,.08.33.85.85,0,0,0,.44.29l.71.29a1.73,1.73,0,0,1,.95.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.56,1.56,0,0,1-.58.39,1.88,1.88,0,0,1-2-.32,1.58,1.58,0,0,1-.4-.57,1.81,1.81,0,0,1-.17-.8h1.15a1.11,1.11,0,0,0,.17.47.56.56,0,0,0,.49.22.71.71,0,0,0,.47-.18A.59.59,0,0,0,13,6.8a.69.69,0,0,0-.13-.43,1.08,1.08,0,0,0-.48-.32l-.59-.21a2.08,2.08,0,0,1-.9-.64,1.66,1.66,0,0,1-.33-1,1.89,1.89,0,0,1,.14-.72,1.78,1.78,0,0,1,.4-.57,1.5,1.5,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.6,1.6,0,0,1,.54.38,1.85,1.85,0,0,1,.36.57,1.82,1.82,0,0,1,.13.7Z"/><path class="cls-2" d="M17.2,4.16a.63.63,0,0,0-.2-.47.69.69,0,0,0-.43-.16.55.55,0,0,0-.44.19.62.62,0,0,0-.16.43.68.68,0,0,0,.09.33.81.81,0,0,0,.43.29l.72.29a1.7,1.7,0,0,1,.94.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.61,1.61,0,0,1-.57.39,1.81,1.81,0,0,1-.74.15,1.76,1.76,0,0,1-1.24-.47,1.61,1.61,0,0,1-.41-.57,2,2,0,0,1-.17-.8h1.15a1.12,1.12,0,0,0,.18.47.53.53,0,0,0,.48.22.72.72,0,0,0,.48-.18.59.59,0,0,0,.21-.48.69.69,0,0,0-.14-.43,1,1,0,0,0-.48-.32l-.58-.21a2.06,2.06,0,0,1-.91-.64,1.66,1.66,0,0,1-.33-1A1.89,1.89,0,0,1,15,3.44a1.78,1.78,0,0,1,.4-.57,1.58,1.58,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.75,1.75,0,0,1,.55.38,1.85,1.85,0,0,1,.36.57,2,2,0,0,1,.13.7Z"/><path class="cls-2" d="M19.2,8.54V0h1.22V3h0a1.53,1.53,0,0,1,.48-.47,1.39,1.39,0,0,1,.65-.16,1.26,1.26,0,0,1,.69.16,1.35,1.35,0,0,1,.4.39,1.18,1.18,0,0,1,.15.51,7.72,7.72,0,0,1,0,1V6.73a5.56,5.56,0,0,1-.05.8,1.56,1.56,0,0,1-.15.5,1.12,1.12,0,0,1-1.07.58,1.15,1.15,0,0,1-.7-.18A3.79,3.79,0,0,1,20.42,8v.55Zm2.44-4.21a1,1,0,0,0-.13-.51A.5.5,0,0,0,21,3.61a.57.57,0,0,0-.44.18.66.66,0,0,0-.18.48V6.63a.83.83,0,0,0,.17.54.52.52,0,0,0,.45.21.49.49,0,0,0,.45-.22,1.11,1.11,0,0,0,.15-.6Z"/><path class="cls-2" d="M23.76,4.49a4.83,4.83,0,0,1,0-.68A1.55,1.55,0,0,1,24,3.26a1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24A1.59,1.59,0,0,1,24,7.73a1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1,0-.68ZM25,6.69a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17A.55.55,0,0,0,26,7.21a.72.72,0,0,0,.16-.52V4.3A.74.74,0,0,0,26,3.78a.55.55,0,0,0-.44-.17.53.53,0,0,0-.43.17A.74.74,0,0,0,25,4.3Z"/><path class="cls-2" d="M28.2,4.49a4.83,4.83,0,0,1,.05-.68,1.55,1.55,0,0,1,.18-.55,1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24,1.59,1.59,0,0,1-.62-.64,1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1-.05-.68Zm1.22,2.2a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17.55.55,0,0,0,.44-.17.72.72,0,0,0,.16-.52V4.3a.74.74,0,0,0-.16-.52A.55.55,0,0,0,30,3.61a.53.53,0,0,0-.43.17.74.74,0,0,0-.17.52Z"/><path class="cls-2" d="M32.75,8.54V0H34V5.11h0l1.47-2.66H36.7L35.24,4.93,37,8.54H35.66l-1.1-2.63L34,6.83V8.54Z"/></g></svg>
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
2
docs/images/brand_inverted.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="120px" height="20px" viewBox="15 0 10 10" enable-background="new 0 0 270 10" xml:space="preserve"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#000;}</style></defs><g class="cls-1"><path class="cls-2" d="M1.65,11V2.45H2.87V3a2.81,2.81,0,0,1,.47-.45A1.13,1.13,0,0,1,4,2.38,1.11,1.11,0,0,1,5.1,3a1.55,1.55,0,0,1,.16.5,5.61,5.61,0,0,1,0,.81V6.58c0,.45,0,.77,0,1a1.17,1.17,0,0,1-.55.9,1.23,1.23,0,0,1-.7.16,1.35,1.35,0,0,1-.64-.16A1.53,1.53,0,0,1,2.89,8h0v3ZM4.08,4.43a1.21,1.21,0,0,0-.14-.6.51.51,0,0,0-.46-.22A.54.54,0,0,0,3,3.82a.8.8,0,0,0-.17.54V6.73A.68.68,0,0,0,3,7.2a.6.6,0,0,0,.44.18A.53.53,0,0,0,4,7.17a1,1,0,0,0,.12-.5Z"/><path class="cls-2" d="M8.63,8.54V7.91h0a2.24,2.24,0,0,1-.48.52,1.13,1.13,0,0,1-.69.18A1.39,1.39,0,0,1,7,8.54a1.09,1.09,0,0,1-.43-.24,1.32,1.32,0,0,1-.33-.49A2.33,2.33,0,0,1,6.11,7a4.89,4.89,0,0,1,.08-.91,1.51,1.51,0,0,1,.31-.65,1.44,1.44,0,0,1,.59-.38A3.19,3.19,0,0,1,8,4.93h.59V4.33a1,1,0,0,0-.13-.52A.52.52,0,0,0,8,3.61a.71.71,0,0,0-.44.15.78.78,0,0,0-.26.46H6.13A2,2,0,0,1,6.69,2.9a1.73,1.73,0,0,1,.57-.38A2,2,0,0,1,8,2.38a2.18,2.18,0,0,1,.72.12,1.71,1.71,0,0,1,.59.36,2,2,0,0,1,.38.6,2.18,2.18,0,0,1,.14.84V8.54Zm0-2.62-.34,0a1.2,1.2,0,0,0-.67.18.76.76,0,0,0-.29.68.89.89,0,0,0,.17.56A.55.55,0,0,0,8,7.53a.63.63,0,0,0,.49-.2.91.91,0,0,0,.17-.58Z"/><path class="cls-2" d="M13,4.16a.59.59,0,0,0-.2-.47.65.65,0,0,0-.42-.16.59.59,0,0,0-.45.19.66.66,0,0,0-.15.43.8.8,0,0,0,.08.33.85.85,0,0,0,.44.29l.71.29a1.73,1.73,0,0,1,.95.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.56,1.56,0,0,1-.58.39,1.88,1.88,0,0,1-2-.32,1.58,1.58,0,0,1-.4-.57,1.81,1.81,0,0,1-.17-.8h1.15a1.11,1.11,0,0,0,.17.47.56.56,0,0,0,.49.22.71.71,0,0,0,.47-.18A.59.59,0,0,0,13,6.8a.69.69,0,0,0-.13-.43,1.08,1.08,0,0,0-.48-.32l-.59-.21a2.08,2.08,0,0,1-.9-.64,1.66,1.66,0,0,1-.33-1,1.89,1.89,0,0,1,.14-.72,1.78,1.78,0,0,1,.4-.57,1.5,1.5,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.6,1.6,0,0,1,.54.38,1.85,1.85,0,0,1,.36.57,1.82,1.82,0,0,1,.13.7Z"/><path class="cls-2" d="M17.2,4.16a.63.63,0,0,0-.2-.47.69.69,0,0,0-.43-.16.55.55,0,0,0-.44.19.62.62,0,0,0-.16.43.68.68,0,0,0,.09.33.81.81,0,0,0,.43.29l.72.29a1.7,1.7,0,0,1,.94.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.61,1.61,0,0,1-.57.39,1.81,1.81,0,0,1-.74.15,1.76,1.76,0,0,1-1.24-.47,1.61,1.61,0,0,1-.41-.57,2,2,0,0,1-.17-.8h1.15a1.12,1.12,0,0,0,.18.47.53.53,0,0,0,.48.22.72.72,0,0,0,.48-.18.59.59,0,0,0,.21-.48.69.69,0,0,0-.14-.43,1,1,0,0,0-.48-.32l-.58-.21a2.06,2.06,0,0,1-.91-.64,1.66,1.66,0,0,1-.33-1A1.89,1.89,0,0,1,15,3.44a1.78,1.78,0,0,1,.4-.57,1.58,1.58,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.75,1.75,0,0,1,.55.38,1.85,1.85,0,0,1,.36.57,2,2,0,0,1,.13.7Z"/><path class="cls-2" d="M19.2,8.54V0h1.22V3h0a1.53,1.53,0,0,1,.48-.47,1.39,1.39,0,0,1,.65-.16,1.26,1.26,0,0,1,.69.16,1.35,1.35,0,0,1,.4.39,1.18,1.18,0,0,1,.15.51,7.72,7.72,0,0,1,0,1V6.73a5.56,5.56,0,0,1-.05.8,1.56,1.56,0,0,1-.15.5,1.12,1.12,0,0,1-1.07.58,1.15,1.15,0,0,1-.7-.18A3.79,3.79,0,0,1,20.42,8v.55Zm2.44-4.21a1,1,0,0,0-.13-.51A.5.5,0,0,0,21,3.61a.57.57,0,0,0-.44.18.66.66,0,0,0-.18.48V6.63a.83.83,0,0,0,.17.54.52.52,0,0,0,.45.21.49.49,0,0,0,.45-.22,1.11,1.11,0,0,0,.15-.6Z"/><path class="cls-2" d="M23.76,4.49a4.83,4.83,0,0,1,0-.68A1.55,1.55,0,0,1,24,3.26a1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24A1.59,1.59,0,0,1,24,7.73a1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1,0-.68ZM25,6.69a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17A.55.55,0,0,0,26,7.21a.72.72,0,0,0,.16-.52V4.3A.74.74,0,0,0,26,3.78a.55.55,0,0,0-.44-.17.53.53,0,0,0-.43.17A.74.74,0,0,0,25,4.3Z"/><path class="cls-2" d="M28.2,4.49a4.83,4.83,0,0,1,.05-.68,1.55,1.55,0,0,1,.18-.55,1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24,1.59,1.59,0,0,1-.62-.64,1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1-.05-.68Zm1.22,2.2a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17.55.55,0,0,0,.44-.17.72.72,0,0,0,.16-.52V4.3a.74.74,0,0,0-.16-.52A.55.55,0,0,0,30,3.61a.53.53,0,0,0-.43.17.74.74,0,0,0-.17.52Z"/><path class="cls-2" d="M32.75,8.54V0H34V5.11h0l1.47-2.66H36.7L35.24,4.93,37,8.54H35.66l-1.1-2.63L34,6.83V8.54Z"/></g></svg>
|
After Width: | Height: | Size: 4.5 KiB |
BIN
docs/images/screen_admin.png
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
docs/images/screen_apps.png
Normal file
After Width: | Height: | Size: 160 KiB |
@ -1,31 +1,16 @@
|
||||
# Welcome
|
||||
#
|
||||
{: style="height:50px"}
|
||||
{: style="height:50px"}
|
||||
|
||||
Welcome to the passbook Documentation. passbook is an open-source Identity Provider and Usermanagement software. It can be used as a central directory for users or customers and it can integrate with your existing Directory.
|
||||
## What is passbook?
|
||||
|
||||
passbook can also be used as part of an Application to facilitate User Enrollment, Password recovery and Social Login.
|
||||
passbook is an open-source Identity Provider focused on flexibility and versatility. You can use passbook in an existing environment to add support for new protocols. passbook is also a great solution for implementing signup/recovery/etc in your application, so you don't have to deal with it.
|
||||
|
||||
passbook uses the following Terminology:
|
||||
## Installation
|
||||
|
||||
### Policy
|
||||
See [Docker-compose](installation/docker-compose.md) or [Kubernetes](installation/kubernetes.md)
|
||||
|
||||
A Policy is at a base level a yes/no gate. It will either evaluate to True or False depending on the Policy Kind and settings. For example, a "Group Membership Policy" evaluates to True if the User is member of the specified Group and False if not. This can be used to conditionally apply Factors and grant/deny access.
|
||||
## Screenshots
|
||||
|
||||
### Provider
|
||||
|
||||
A Provider is a way for other Applications to authenticate against passbook. Common Providers are OpenID Connect (OIDC) and SAML.
|
||||
|
||||
### Source
|
||||
|
||||
Sources are ways to get users into passbook. This might be an LDAP Connection to import Users from Active Directory, or an OAuth2 Connection to allow Social Logins.
|
||||
|
||||
### Application
|
||||
|
||||
An application links together Policies with a Provider, allowing you to control access. It also holds Information like UI Name, Icon and more.
|
||||
|
||||
### Factors
|
||||
|
||||
Factors represent Authentication Factors, like a Password or OTP. These Factors can be dynamically enabled using policies. This allows you to, for example, force users from a certain IP ranges to complete a Captcha to authenticate.
|
||||
|
||||
### Property Mappings
|
||||
|
||||
Property Mappings allow you to make Information available for external Applications. For example, if you want to login to AWS with passbook, you'd use Property Mappings to set the User's Roles based on their Groups.
|
||||

|
||||

|
||||
|
@ -1,6 +0,0 @@
|
||||
# Installation
|
||||
|
||||
There are two supported ways to install passbook:
|
||||
|
||||
- [docker-compose](docker-compose.md) for test- or small productive setups
|
||||
- [Kubernetes](./kubernetes.md) for larger Productive setups
|
@ -1,3 +1,61 @@
|
||||
# Kubernetes
|
||||
|
||||
For a mid to high-load Installation, Kubernetes is recommended. passbook is installed using a helm-chart.
|
||||
|
||||
```
|
||||
# Default values for passbook.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
# passbook version to use. Defaults to latest stable version
|
||||
# image:
|
||||
# tag:
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
error_reporting: false
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
log_level: warning
|
||||
|
||||
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
|
||||
# This requires the CoreOS Prometheus Operator.
|
||||
monitoring:
|
||||
enabled: false
|
||||
|
||||
# Enable Database Backups to S3
|
||||
# backup:
|
||||
# access_key: access-key
|
||||
# secret_key: secret-key
|
||||
# bucket: s3-bucket
|
||||
# host: s3-host
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
|
||||
# These settings configure the packaged PostgreSQL and Redis chart.
|
||||
postgresql:
|
||||
postgresqlDatabase: passbook
|
||||
|
||||
redis:
|
||||
cluster:
|
||||
enabled: false
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
```
|
||||
|
32
docs/integrations/services/aws/index.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Amazon Web Services Integration
|
||||
|
||||
## What is AWS
|
||||
|
||||
!!! note ""
|
||||
Amazon Web Services (AWS) is the world’s 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:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Afterwards export the Metadata from passbook, and create an Identity Provider [here](https://console.aws.amazon.com/iam/home#/providers).
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/integrations/services/aws/property-mapping-role.png
Normal file
After Width: | Height: | Size: 66 KiB |
@ -4,9 +4,8 @@
|
||||
|
||||
From https://about.gitlab.com/what-is-gitlab/
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
!!! 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.
|
||||
|
||||
## Preparation
|
||||
|
||||
@ -21,7 +20,7 @@ Create an application in passbook and note the slug, as this will be used later.
|
||||
- Audience: `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
|
||||
|
||||
|
@ -4,9 +4,8 @@
|
||||
|
||||
From https://goharbor.io
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
!!! 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.
|
||||
|
||||
## Preparation
|
||||
|
||||
|
@ -4,10 +4,9 @@
|
||||
|
||||
From https://rancher.com/products/rancher
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
!!! note ""
|
||||
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.
|
||||
|
||||
## 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`
|
||||
- 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
|
||||
|
||||
|
@ -4,13 +4,12 @@
|
||||
|
||||
From https://sentry.io
|
||||
|
||||
```
|
||||
Sentry provides self-hosted and cloud-based error monitoring that helps all software
|
||||
teams discover, triage, and prioritize errors in real-time.
|
||||
!!! note ""
|
||||
Sentry provides self-hosted and cloud-based error monitoring that helps all software
|
||||
teams discover, triage, and prioritize errors in real-time.
|
||||
|
||||
One million developers at over fifty thousand companies already ship
|
||||
better software faster with Sentry. Won’t you join them?
|
||||
```
|
||||
One million developers at over fifty thousand companies already ship
|
||||
better software faster with Sentry. Won’t you join them?
|
||||
|
||||
## Preparation
|
||||
|
||||
|
74
docs/integrations/services/tower-awx/index.md
Normal 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. It’s designed to be the hub for all of your automation tasks.
|
||||
|
||||
Tower allows you to control access to who can access what, even allowing sharing of SSH credentials without someone being able to transfer those credentials. Inventory can be graphically managed or synced with a wide variety of cloud sources. It logs all of your jobs, integrates well with LDAP, and has an amazing browsable REST API. Command line tools are available for easy integration with Jenkins as well. Provisioning callbacks provide great support for autoscaling topologies.
|
||||
|
||||
!!! note
|
||||
AWX is the Open-Source version of Tower, 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.
|
@ -1,33 +0,0 @@
|
||||
---
|
||||
apiVersion: apps/v1beta2
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: passbook-docs
|
||||
namespace: prod-passbook-docs
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
app.kubernetes.io/managed-by: passbook-docs
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
spec:
|
||||
containers:
|
||||
- name: passbook-docs
|
||||
image: "beryju/passbook-docs:latest"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
@ -1,21 +0,0 @@
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
name: passbook-docs
|
||||
namespace: prod-passbook-docs
|
||||
spec:
|
||||
rules:
|
||||
- host: docs.passbook.beryju.org
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: passbook-docs-http
|
||||
servicePort: http
|
||||
path: /
|
||||
tls:
|
||||
- hosts:
|
||||
- docs.passbook.beryju.org
|
||||
secretName: passbook-docs-acme
|
@ -1,17 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: passbook-docs-http
|
||||
namespace: prod-passbook-docs
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app.kubernetes.io/name: passbook-docs
|
30
docs/policies/expression.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Expression Policies
|
||||
|
||||
!!! notice
|
||||
These variables are available in addition to the common variables/functions defined in [**Expressions**](../expressions/index.md)
|
||||
|
||||
The passing of the policy is determined by the return value of the code. Use `return True` to pass a policy and `return False` to fail it.
|
||||
|
||||
### Available Functions
|
||||
|
||||
#### `pb_message(message: str)`
|
||||
|
||||
Add a message, visible by the end user. This can be used to show the reason why they were denied.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
pb_message("Access denied")
|
||||
return False
|
||||
```
|
||||
|
||||
### Context variables
|
||||
|
||||
- `request`: A PolicyRequest object, which has the following properties:
|
||||
- `request.user`: The current User, which the Policy is applied against. ([ref](../expressions/reference/user-object.md))
|
||||
- `request.http_request`: The Django HTTP Request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects))
|
||||
- `request.obj`: A Django Model instance. This is only set if the Policy is ran against an object.
|
||||
- `request.context`: A dictionary with dynamic data. This depends on the origin of the execution.
|
||||
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external Provider.
|
||||
- `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted.
|
||||
- `pb_flow_plan`: Current Plan if Policy is called from the Flow Planner.
|
@ -8,41 +8,15 @@ There are two different Kind of policies, a Standard Policy and a Password Polic
|
||||
|
||||
---
|
||||
|
||||
### Group-Membership Policy
|
||||
|
||||
This policy evaluates to True if the current user is a Member of the selected group.
|
||||
|
||||
### Reputation Policy
|
||||
|
||||
passbook keeps track of failed login attempts by Source IP and Attempted Username. These values are saved as scores. Each failed login decreases the Score for the Client IP as well as the targeted Username by one.
|
||||
|
||||
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:
|
||||
|
||||
- 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
|
||||
|
||||
This policy allows you to send an arbitrary HTTP Request to any URL. You can then use JSONPath to extract the result you need.
|
||||
See [Expression Policy](expression.md).
|
||||
|
||||
## Password Policies
|
||||
|
||||
@ -53,11 +27,11 @@ This policy allows you to send an arbitrary HTTP Request to any URL. You can the
|
||||
This Policy allows you to specify Password rules, like Length and required Characters.
|
||||
The following rules can be set:
|
||||
|
||||
- Minimum amount of Uppercase Characters
|
||||
- Minimum amount of Lowercase Characters
|
||||
- Minimum amount of Symbols Characters
|
||||
- Minimum Length
|
||||
- Symbol charset (define which characters are counted as symbols)
|
||||
- Minimum amount of Uppercase Characters
|
||||
- Minimum amount of Lowercase Characters
|
||||
- Minimum amount of Symbols Characters
|
||||
- Minimum Length
|
||||
- Symbol charset (define which characters are counted as symbols)
|
||||
|
||||
### Have I Been Pwned Policy
|
||||
|
12
docs/property-mappings/expression.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Property Mapping Expressions
|
||||
|
||||
The property mapping should return a value that is expected by the Provider/Source. What types are supported, is documented in the individual Provider/Source. Returning `None` is always accepted, this simply skips this mapping.
|
||||
|
||||
!!! notice
|
||||
These variables are available in addition to the common variables/functions defined in [**Expressions**](../expressions/index.md)
|
||||
|
||||
### Context Variables
|
||||
|
||||
- `user`: The current user, this might be `None` if there is no contextual user. ([ref](../expressions/reference/user-object.md))
|
||||
- `request`: The current request, this might be `None` if there is no contextual request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects))
|
||||
- Arbitrary other arguments given by the provider, this is documented on the Provider/Source.
|
@ -12,10 +12,10 @@ You can find examples [here](integrations/)
|
||||
|
||||
LDAP Property Mappings are used when you define a LDAP Source. These Mappings define which LDAP Property maps to which passbook Property. By default, these mappings are created:
|
||||
|
||||
- Autogenerated LDAP Mapping: givenName -> first_name
|
||||
- Autogenerated LDAP Mapping: mail -> email
|
||||
- Autogenerated LDAP Mapping: name -> name
|
||||
- Autogenerated LDAP Mapping: sAMAccountName -> username
|
||||
- Autogenerated LDAP Mapping: sn -> last_name
|
||||
- Autogenerated LDAP Mapping: givenName -> first_name
|
||||
- Autogenerated LDAP Mapping: mail -> email
|
||||
- Autogenerated LDAP Mapping: name -> name
|
||||
- Autogenerated LDAP Mapping: sAMAccountName -> username
|
||||
- Autogenerated LDAP Mapping: sn -> last_name
|
||||
|
||||
These are configured for the most common LDAP Setups.
|
@ -13,11 +13,5 @@ The API exposes Username, E-Mail, Name and Groups in a GitHub-compatible format.
|
||||
|
||||
## 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.
|
||||
Default fields are:
|
||||
|
||||
- `eduPersonPrincipalName`: User's E-Mail
|
||||
- `cn`: User's Full Name
|
||||
- `mail`: User's E-Mail
|
||||
- `displayName`: User's Username
|
||||
- `uid`: User Unique Identifier
|
||||
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 exposed through Auto-generated Property Mappings, which are prefixed with "Autogenerated..."
|
||||
|
2
docs/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
mkdocs
|
||||
mkdocs-material
|
1
docs/runtime.txt
Normal file
@ -0,0 +1 @@
|
||||
3.7
|
@ -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.
|
||||
- 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.)
|
||||
- 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)
|
||||
|
27
docs/terminology.md
Normal file
@ -0,0 +1,27 @@
|
||||
### Policy
|
||||
|
||||
A Policy is at a base level a yes/no gate. It will either evaluate to True or False depending on the Policy Kind and settings. For example, a "Group Membership Policy" evaluates to True if the User is member of the specified Group and False if not. This can be used to conditionally apply Stages, grant/deny access to various objects and is also used for other custom logic.
|
||||
|
||||
### Provider
|
||||
|
||||
A Provider is a way for other Applications to authenticate against passbook. Common Providers are OpenID Connect (OIDC) and SAML.
|
||||
|
||||
### Source
|
||||
|
||||
Sources are ways to get users into passbook. This might be an LDAP Connection to import Users from Active Directory, or an OAuth2 Connection to allow Social Logins.
|
||||
|
||||
### Application
|
||||
|
||||
An application links together Policies with a Provider, allowing you to control access. It also holds Information like UI Name, Icon and more.
|
||||
|
||||
### Flows
|
||||
|
||||
Flows are a method of describing a sequence of stages. These flows can be used to defined how a user authenticates, enrolls, etc.
|
||||
|
||||
### Stages
|
||||
|
||||
A stage represents a single verification or logic step. They are used to authenticate users, enroll them, and more. These stages can optionally be applied to a flow via policies.
|
||||
|
||||
### Property Mappings
|
||||
|
||||
Property Mappings allow you to make Information available for external Applications. For example, if you want to login to AWS with passbook, you'd use Property Mappings to set the User's Roles based on their Groups.
|
33
docs/upgrading-from-0.8.x.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Upgrading from 0.8.x
|
||||
|
||||
Due to some database changes that had to be rather sooner than later, there is no possibility to directly upgrade. You must extract the data before hand and import it again. It is recommended to spin up a second instance of passbook to do this.
|
||||
|
||||
To export data from your old instance, run this command:
|
||||
|
||||
(with docker-compose)
|
||||
```
|
||||
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
|
||||
docker cp passbook_server_1:/tmp/passbook_dump.json passbook_dump.json
|
||||
```
|
||||
|
||||
(with kubernetes)
|
||||
```
|
||||
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
|
||||
kubectl cp passbook-web-...:/tmp/passbook_dump.json passbook_dump.json
|
||||
```
|
||||
|
||||
After that, create a new passbook instance in a different namespace (kubernetes) or in a different folder (docker-compose). Once this instance is running, you can use the following commands to restore the data. On docker-compose, you still have to run the `migrate` command, to create all database structures.
|
||||
|
||||
(docker-compose)
|
||||
```
|
||||
docker cp passbook_dump.json new_passbook_server_1:/tmp/passbook_dump.json
|
||||
docker-compose exec server ./manage.py loaddata /tmp/passbook_dump.json
|
||||
```
|
||||
|
||||
(with kubernetes)
|
||||
```
|
||||
kubectl cp passbook_dump.json passbook-web-...:/tmp/passbook_dump.json
|
||||
kubectl exec -it passbook-web-... -- ./manage.py loaddata /tmp/passbook_dump.json
|
||||
```
|
||||
|
||||
Now, you should be able to login to the new passbook instance, and migrate the rest of the data over.
|
@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
appVersion: "0.7.9-beta"
|
||||
appVersion: "0.9.0-pre1"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.7.9-beta"
|
||||
version: "0.9.0-pre1"
|
||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||
|
@ -8,9 +8,16 @@ data:
|
||||
host: "{{ .Release.Name }}-postgresql"
|
||||
name: "{{ .Values.postgresql.postgresqlDatabase }}"
|
||||
user: postgres
|
||||
{{- if .Values.backup }}
|
||||
backup:
|
||||
access_key: "{{ .Values.backup.access_key }}"
|
||||
secret_key: "{{ .Values.backup.secret_key }}"
|
||||
bucket: "{{ .Values.backup.bucket }}"
|
||||
host: "{{ .Values.backup.host }}"
|
||||
{{- end}}
|
||||
redis:
|
||||
host: "{{ .Release.Name }}-redis-master"
|
||||
cache_db: 0
|
||||
message_queue_db: 1
|
||||
error_reporting: {{ .Values.config.error_reporting }}
|
||||
domain: ".{{ index .Values.ingress.hosts 0 }}"
|
||||
log_level: "{{ .Values.config.log_level }}"
|
||||
|
@ -7,11 +7,13 @@ metadata:
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.passbook.io/component: static
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: static
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
@ -25,19 +27,19 @@ spec:
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 5
|
||||
httpGet:
|
||||
path: /-/ping
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 5
|
||||
httpGet:
|
||||
path: /-/ping
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
requests:
|
||||
|
@ -11,7 +11,7 @@ metadata:
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 8080
|
||||
- port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
|
@ -7,18 +7,20 @@ metadata:
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.passbook.io/component: web
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
passbook.io/component: web
|
||||
k8s.passbook.io/component: web
|
||||
spec:
|
||||
volumes:
|
||||
- name: config-volume
|
||||
@ -27,9 +29,9 @@ spec:
|
||||
initContainers:
|
||||
- name: passbook-database-migrations
|
||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||
command:
|
||||
- ./manage.py
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- ./manage.py
|
||||
- migrate
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
@ -57,10 +59,9 @@ spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- uwsgi
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- uwsgi
|
||||
- uwsgi.ini
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
|
@ -18,4 +18,4 @@ spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
passbook.io/component: web
|
||||
k8s.passbook.io/component: web
|
||||
|
@ -18,6 +18,7 @@ spec:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
key: monitoring_username
|
||||
port: http
|
||||
path: /metrics/
|
||||
interval: 10s
|
||||
selector:
|
||||
matchLabels:
|
||||
|
@ -7,18 +7,20 @@ metadata:
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.passbook.io/component: worker
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: worker
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
passbook.io/component: worker
|
||||
k8s.passbook.io/component: worker
|
||||
spec:
|
||||
volumes:
|
||||
- name: config-volume
|
||||
@ -28,9 +30,8 @@ spec:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- celery
|
||||
args:
|
||||
- celery
|
||||
- worker
|
||||
- --autoscale=10,3
|
||||
- -E
|
||||
|
@ -2,7 +2,7 @@
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
image:
|
||||
tag: 0.7.9-beta
|
||||
tag: 0.9.0-pre1
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
@ -11,14 +11,22 @@ config:
|
||||
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
error_reporting: false
|
||||
email:
|
||||
host: localhost
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
log_level: warning
|
||||
|
||||
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
|
||||
# This requires the CoreOS Prometheus Operator.
|
||||
monitoring:
|
||||
enabled: false
|
||||
|
||||
# Enable Database Backups to S3
|
||||
# backup:
|
||||
# access_key: access-key
|
||||
# secret_key: secret-key
|
||||
# bucket: s3-bucket
|
||||
# host: s3-host
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
@ -42,3 +50,5 @@ redis:
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
|
@ -2,9 +2,12 @@
|
||||
"""Django manage.py"""
|
||||
import os
|
||||
import sys
|
||||
from defusedxml import defuse_stdlib
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'passbook.root.settings')
|
||||
defuse_stdlib()
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
|
72
mkdocs.yml
@ -1,31 +1,73 @@
|
||||
site_name: passbook Docs
|
||||
site_url: https://docs.passbook.beryju.org
|
||||
site_url: https://passbook.beryju.org/
|
||||
copyright: "Copyright © 2019 - 2020 BeryJu.org"
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Terminology: terminology.md
|
||||
- Installation:
|
||||
- Installation: installation/install.md
|
||||
- docker-compose: installation/docker-compose.md
|
||||
- Kubernetes: installation/kubernetes.md
|
||||
- docker-compose: installation/docker-compose.md
|
||||
- Kubernetes: installation/kubernetes.md
|
||||
- Flows:
|
||||
Overview: flow/flows.md
|
||||
Examples:
|
||||
- Login: flow/examples/login.md
|
||||
- Stages:
|
||||
- Captcha Stage: flow/stages/captcha/index.md
|
||||
- Dummy Stage: flow/stages/dummy/index.md
|
||||
- E-Mail Stage: flow/stages/email/index.md
|
||||
- Identification Stage: flow/stages/identification/index.md
|
||||
- Invitation Stage: flow/stages/invitation/index.md
|
||||
- OTP Stage: flow/stages/otp/index.md
|
||||
- Password Stage: flow/stages/password/index.md
|
||||
- Prompt Stage: flow/stages/prompt/index.md
|
||||
- Prompt Stage Validation: flow/stages/prompt/validation.md
|
||||
- User Delete Stage: flow/stages/user_delete.md
|
||||
- User Login Stage: flow/stages/user_login.md
|
||||
- User Logout Stage: flow/stages/user_logout.md
|
||||
- User Write Stage: flow/stages/user_write.md
|
||||
- Sources: sources.md
|
||||
- Providers: providers.md
|
||||
- Property Mappings: property-mappings.md
|
||||
- Factors: factors.md
|
||||
- Policies: policies.md
|
||||
- Expressions:
|
||||
- Overview: expressions/index.md
|
||||
- Reference:
|
||||
- User Object: expressions/reference/user-object.md
|
||||
- Property Mappings:
|
||||
- Overview: property-mappings/index.md
|
||||
- Expressions: property-mappings/expression.md
|
||||
- Policies:
|
||||
- Overview: policies/index.md
|
||||
- Expression: policies/expression.md
|
||||
- Integrations:
|
||||
- as Provider:
|
||||
- GitLab: integrations/services/gitlab/index.md
|
||||
- Rancher: integrations/services/rancher/index.md
|
||||
- Harbor: integrations/services/harbor/index.md
|
||||
- Sentry: integrations/services/sentry/index.md
|
||||
- as Provider:
|
||||
- Amazon Web Services: integrations/services/aws/index.md
|
||||
- GitLab: integrations/services/gitlab/index.md
|
||||
- Rancher: integrations/services/rancher/index.md
|
||||
- Harbor: integrations/services/harbor/index.md
|
||||
- Sentry: integrations/services/sentry/index.md
|
||||
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
|
||||
- Upgrading from v0.8.x: upgrading-from-0.8.x.md
|
||||
|
||||
repo_name: "BeryJu.org/passbook"
|
||||
repo_name: "BeryJu/passbook"
|
||||
repo_url: https://github.com/BeryJu/passbook
|
||||
theme:
|
||||
name: "material"
|
||||
logo: "images/logo.svg"
|
||||
name: material
|
||||
logo: images/logo.svg
|
||||
favicon: images/logo.svg
|
||||
palette:
|
||||
scheme: slate
|
||||
primary: white
|
||||
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
permalink: "¶"
|
||||
- admonition
|
||||
- codehilite
|
||||
- pymdownx.betterem:
|
||||
smart_enable: all
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.magiclink
|
||||
- attr_list
|
||||
|
||||
plugins:
|
||||
- search
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.7.9-beta"
|
||||
__version__ = "0.9.0-pre1"
|
||||
|
@ -4,6 +4,27 @@ from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class CodeMirrorWidget(forms.Textarea):
|
||||
"""Custom Textarea-based Widget that triggers a CodeMirror editor"""
|
||||
|
||||
# CodeMirror mode to enable
|
||||
mode: str
|
||||
|
||||
def __init__(self, *args, mode="yaml", **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.mode = mode
|
||||
|
||||
def render(self, *args, **kwargs):
|
||||
if "attrs" not in kwargs:
|
||||
kwargs["attrs"] = {}
|
||||
attrs = kwargs["attrs"]
|
||||
if "class" not in attrs:
|
||||
attrs["class"] = ""
|
||||
attrs["class"] += " codemirror"
|
||||
attrs["data-cm-mode"] = self.mode
|
||||
return super().render(*args, **kwargs)
|
||||
|
||||
|
||||
class InvalidYAMLInput(str):
|
||||
"""Invalid YAML String type"""
|
||||
|
||||
@ -48,7 +69,7 @@ class YAMLField(forms.CharField):
|
||||
def prepare_value(self, value):
|
||||
if isinstance(value, InvalidYAMLInput):
|
||||
return value
|
||||
return yaml.dump(value, explicit_start=True)
|
||||
return yaml.dump(value, explicit_start=True, default_flow_style=False)
|
||||
|
||||
def has_changed(self, initial, data):
|
||||
if super().has_changed(initial, data):
|
||||
|
@ -1,40 +0,0 @@
|
||||
"""passbook form helpers"""
|
||||
from django import forms
|
||||
|
||||
from passbook.admin.fields import YAMLField
|
||||
|
||||
|
||||
class TagModelForm(forms.ModelForm):
|
||||
"""Base form for models that have attributes"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Check if we have an instance, load tags otherwise use an empty dict
|
||||
instance = kwargs.get("instance", None)
|
||||
tags = instance.tags if instance else {}
|
||||
# Make sure all predefined tags exist in tags, and set default if they don't
|
||||
predefined_tags = (
|
||||
self._meta.model().get_predefined_tags() # pylint: disable=no-member
|
||||
)
|
||||
for key, value in predefined_tags.items():
|
||||
if key not in tags:
|
||||
tags[key] = value
|
||||
# Format JSON
|
||||
kwargs["initial"]["tags"] = tags
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_tags(self):
|
||||
"""Make sure all required tags are set"""
|
||||
if hasattr(self.instance, "get_required_keys") and hasattr(
|
||||
self.instance, "tags"
|
||||
):
|
||||
for key in self.instance.get_required_keys():
|
||||
if key not in self.cleaned_data.get("tags"):
|
||||
raise forms.ValidationError("Tag %s missing." % key)
|
||||
return self.cleaned_data.get("tags")
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class TagModelFormMeta:
|
||||
"""Base Meta class that uses the YAMLField"""
|
||||
|
||||
field_classes = {"tags": YAMLField}
|
@ -1,6 +1,7 @@
|
||||
"""passbook administration forms"""
|
||||
from django import forms
|
||||
|
||||
from passbook.admin.fields import CodeMirrorWidget, YAMLField
|
||||
from passbook.core.models import User
|
||||
|
||||
|
||||
@ -8,3 +9,4 @@ class PolicyTestForm(forms.Form):
|
||||
"""Form to test policies against user"""
|
||||
|
||||
user = forms.ModelChoiceField(queryset=User.objects.all())
|
||||
context = YAMLField(widget=CodeMirrorWidget(), required=False, initial=dict)
|
||||
|
@ -1,7 +1,17 @@
|
||||
"""passbook core source form fields"""
|
||||
# from django import forms
|
||||
|
||||
SOURCE_FORM_FIELDS = ["name", "slug", "enabled", "policies"]
|
||||
SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled", "policies"]
|
||||
|
||||
# class SourceForm(forms.Form)
|
||||
SOURCE_FORM_FIELDS = [
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
]
|
||||
SOURCE_SERIALIZER_FIELDS = [
|
||||
"pk",
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
]
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from django import forms
|
||||
|
||||
from passbook.admin.fields import YAMLField
|
||||
from passbook.admin.fields import CodeMirrorWidget, YAMLField
|
||||
from passbook.core.models import User
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ class UserForm(forms.ModelForm):
|
||||
fields = ["username", "name", "email", "is_staff", "is_active", "attributes"]
|
||||
widgets = {
|
||||
"name": forms.TextInput,
|
||||
"attributes": CodeMirrorWidget,
|
||||
}
|
||||
field_classes = {
|
||||
"attributes": YAMLField,
|
||||
|
@ -1,209 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
var noOptions = {};
|
||||
var nonWS = /[^\s\u00a0]/;
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function firstNonWS(str) {
|
||||
var found = str.search(nonWS);
|
||||
return found == -1 ? 0 : found;
|
||||
}
|
||||
|
||||
CodeMirror.commands.toggleComment = function(cm) {
|
||||
cm.toggleComment();
|
||||
};
|
||||
|
||||
CodeMirror.defineExtension("toggleComment", function(options) {
|
||||
if (!options) options = noOptions;
|
||||
var cm = this;
|
||||
var minLine = Infinity, ranges = this.listSelections(), mode = null;
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
var from = ranges[i].from(), to = ranges[i].to();
|
||||
if (from.line >= minLine) continue;
|
||||
if (to.line >= minLine) to = Pos(minLine, 0);
|
||||
minLine = from.line;
|
||||
if (mode == null) {
|
||||
if (cm.uncomment(from, to, options)) mode = "un";
|
||||
else { cm.lineComment(from, to, options); mode = "line"; }
|
||||
} else if (mode == "un") {
|
||||
cm.uncomment(from, to, options);
|
||||
} else {
|
||||
cm.lineComment(from, to, options);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Rough heuristic to try and detect lines that are part of multi-line string
|
||||
function probablyInsideString(cm, pos, line) {
|
||||
return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line)
|
||||
}
|
||||
|
||||
function getMode(cm, pos) {
|
||||
var mode = cm.getMode()
|
||||
return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos)
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("lineComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = getMode(self, from);
|
||||
var firstLine = self.getLine(from.line);
|
||||
if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
|
||||
|
||||
var commentString = options.lineComment || mode.lineComment;
|
||||
if (!commentString) {
|
||||
if (options.blockCommentStart || mode.blockCommentStart) {
|
||||
options.fullLines = true;
|
||||
self.blockComment(from, to, options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
|
||||
var pad = options.padding == null ? " " : options.padding;
|
||||
var blankLines = options.commentBlankLines || from.line == to.line;
|
||||
|
||||
self.operation(function() {
|
||||
if (options.indent) {
|
||||
var baseString = null;
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
var line = self.getLine(i);
|
||||
var whitespace = line.slice(0, firstNonWS(line));
|
||||
if (baseString == null || baseString.length > whitespace.length) {
|
||||
baseString = whitespace;
|
||||
}
|
||||
}
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
var line = self.getLine(i), cut = baseString.length;
|
||||
if (!blankLines && !nonWS.test(line)) continue;
|
||||
if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
|
||||
self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
|
||||
}
|
||||
} else {
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
if (blankLines || nonWS.test(self.getLine(i)))
|
||||
self.replaceRange(commentString + pad, Pos(i, 0));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("blockComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = getMode(self, from);
|
||||
var startString = options.blockCommentStart || mode.blockCommentStart;
|
||||
var endString = options.blockCommentEnd || mode.blockCommentEnd;
|
||||
if (!startString || !endString) {
|
||||
if ((options.lineComment || mode.lineComment) && options.fullLines != false)
|
||||
self.lineComment(from, to, options);
|
||||
return;
|
||||
}
|
||||
if (/\bcomment\b/.test(self.getTokenTypeAt(Pos(from.line, 0)))) return
|
||||
|
||||
var end = Math.min(to.line, self.lastLine());
|
||||
if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
|
||||
|
||||
var pad = options.padding == null ? " " : options.padding;
|
||||
if (from.line > end) return;
|
||||
|
||||
self.operation(function() {
|
||||
if (options.fullLines != false) {
|
||||
var lastLineHasText = nonWS.test(self.getLine(end));
|
||||
self.replaceRange(pad + endString, Pos(end));
|
||||
self.replaceRange(startString + pad, Pos(from.line, 0));
|
||||
var lead = options.blockCommentLead || mode.blockCommentLead;
|
||||
if (lead != null) for (var i = from.line + 1; i <= end; ++i)
|
||||
if (i != end || lastLineHasText)
|
||||
self.replaceRange(lead + pad, Pos(i, 0));
|
||||
} else {
|
||||
self.replaceRange(endString, to);
|
||||
self.replaceRange(startString, from);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("uncomment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = getMode(self, from);
|
||||
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
|
||||
|
||||
// Try finding line comments
|
||||
var lineString = options.lineComment || mode.lineComment, lines = [];
|
||||
var pad = options.padding == null ? " " : options.padding, didSomething;
|
||||
lineComment: {
|
||||
if (!lineString) break lineComment;
|
||||
for (var i = start; i <= end; ++i) {
|
||||
var line = self.getLine(i);
|
||||
var found = line.indexOf(lineString);
|
||||
if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
|
||||
if (found == -1 && nonWS.test(line)) break lineComment;
|
||||
if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
|
||||
lines.push(line);
|
||||
}
|
||||
self.operation(function() {
|
||||
for (var i = start; i <= end; ++i) {
|
||||
var line = lines[i - start];
|
||||
var pos = line.indexOf(lineString), endPos = pos + lineString.length;
|
||||
if (pos < 0) continue;
|
||||
if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
|
||||
didSomething = true;
|
||||
self.replaceRange("", Pos(i, pos), Pos(i, endPos));
|
||||
}
|
||||
});
|
||||
if (didSomething) return true;
|
||||
}
|
||||
|
||||
// Try block comments
|
||||
var startString = options.blockCommentStart || mode.blockCommentStart;
|
||||
var endString = options.blockCommentEnd || mode.blockCommentEnd;
|
||||
if (!startString || !endString) return false;
|
||||
var lead = options.blockCommentLead || mode.blockCommentLead;
|
||||
var startLine = self.getLine(start), open = startLine.indexOf(startString)
|
||||
if (open == -1) return false
|
||||
var endLine = end == start ? startLine : self.getLine(end)
|
||||
var close = endLine.indexOf(endString, end == start ? open + startString.length : 0);
|
||||
var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
|
||||
if (close == -1 ||
|
||||
!/comment/.test(self.getTokenTypeAt(insideStart)) ||
|
||||
!/comment/.test(self.getTokenTypeAt(insideEnd)) ||
|
||||
self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1)
|
||||
return false;
|
||||
|
||||
// Avoid killing block comments completely outside the selection.
|
||||
// Positions of the last startString before the start of the selection, and the first endString after it.
|
||||
var lastStart = startLine.lastIndexOf(startString, from.ch);
|
||||
var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
|
||||
if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
|
||||
// Positions of the first endString after the end of the selection, and the last startString before it.
|
||||
firstEnd = endLine.indexOf(endString, to.ch);
|
||||
var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
|
||||
lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
|
||||
if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
|
||||
|
||||
self.operation(function() {
|
||||
self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
|
||||
Pos(end, close + endString.length));
|
||||
var openEnd = open + startString.length;
|
||||
if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
|
||||
self.replaceRange("", Pos(start, open), Pos(start, openEnd));
|
||||
if (lead) for (var i = start + 1; i <= end; ++i) {
|
||||
var line = self.getLine(i), found = line.indexOf(lead);
|
||||
if (found == -1 || nonWS.test(line.slice(0, found))) continue;
|
||||
var foundEnd = found + lead.length;
|
||||
if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
|
||||
self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
});
|
||||
});
|
@ -1,78 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
function continueComment(cm) {
|
||||
if (cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
var ranges = cm.listSelections(), mode, inserts = [];
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var pos = ranges[i].head
|
||||
if (!/\bcomment\b/.test(cm.getTokenTypeAt(pos))) return CodeMirror.Pass;
|
||||
var modeHere = cm.getModeAt(pos)
|
||||
if (!mode) mode = modeHere;
|
||||
else if (mode != modeHere) return CodeMirror.Pass;
|
||||
|
||||
var insert = null;
|
||||
if (mode.blockCommentStart && mode.blockCommentContinue) {
|
||||
var line = cm.getLine(pos.line).slice(0, pos.ch)
|
||||
var end = line.lastIndexOf(mode.blockCommentEnd), found
|
||||
if (end != -1 && end == pos.ch - mode.blockCommentEnd.length) {
|
||||
// Comment ended, don't continue it
|
||||
} else if ((found = line.lastIndexOf(mode.blockCommentStart)) > -1 && found > end) {
|
||||
insert = line.slice(0, found)
|
||||
if (/\S/.test(insert)) {
|
||||
insert = ""
|
||||
for (var j = 0; j < found; ++j) insert += " "
|
||||
}
|
||||
} else if ((found = line.indexOf(mode.blockCommentContinue)) > -1 && !/\S/.test(line.slice(0, found))) {
|
||||
insert = line.slice(0, found)
|
||||
}
|
||||
if (insert != null) insert += mode.blockCommentContinue
|
||||
}
|
||||
if (insert == null && mode.lineComment && continueLineCommentEnabled(cm)) {
|
||||
var line = cm.getLine(pos.line), found = line.indexOf(mode.lineComment);
|
||||
if (found > -1) {
|
||||
insert = line.slice(0, found);
|
||||
if (/\S/.test(insert)) insert = null;
|
||||
else insert += mode.lineComment + line.slice(found + mode.lineComment.length).match(/^\s*/)[0];
|
||||
}
|
||||
}
|
||||
if (insert == null) return CodeMirror.Pass;
|
||||
inserts[i] = "\n" + insert;
|
||||
}
|
||||
|
||||
cm.operation(function() {
|
||||
for (var i = ranges.length - 1; i >= 0; i--)
|
||||
cm.replaceRange(inserts[i], ranges[i].from(), ranges[i].to(), "+insert");
|
||||
});
|
||||
}
|
||||
|
||||
function continueLineCommentEnabled(cm) {
|
||||
var opt = cm.getOption("continueComments");
|
||||
if (opt && typeof opt == "object")
|
||||
return opt.continueLineComment !== false;
|
||||
return true;
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("continueComments", null, function(cm, val, prev) {
|
||||
if (prev && prev != CodeMirror.Init)
|
||||
cm.removeKeyMap("continueComment");
|
||||
if (val) {
|
||||
var key = "Enter";
|
||||
if (typeof val == "string")
|
||||
key = val;
|
||||
else if (typeof val == "object" && val.key)
|
||||
key = val.key;
|
||||
var map = {name: "continueComment"};
|
||||
map[key] = continueComment;
|
||||
cm.addKeyMap(map);
|
||||
}
|
||||
});
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
.CodeMirror-dialog {
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
background: inherit;
|
||||
z-index: 15;
|
||||
padding: .1em .8em;
|
||||
overflow: hidden;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog-top {
|
||||
border-bottom: 1px solid #eee;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog-bottom {
|
||||
border-top: 1px solid #eee;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
width: 20em;
|
||||
color: inherit;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog button {
|
||||
font-size: 70%;
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
// Open simple dialogs on top of an editor. Relies on dialog.css.
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
function dialogDiv(cm, template, bottom) {
|
||||
var wrap = cm.getWrapperElement();
|
||||
var dialog;
|
||||
dialog = wrap.appendChild(document.createElement("div"));
|
||||
if (bottom)
|
||||
dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
|
||||
else
|
||||
dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
|
||||
|
||||
if (typeof template == "string") {
|
||||
dialog.innerHTML = template;
|
||||
} else { // Assuming it's a detached DOM element.
|
||||
dialog.appendChild(template);
|
||||
}
|
||||
CodeMirror.addClass(wrap, 'dialog-opened');
|
||||
return dialog;
|
||||
}
|
||||
|
||||
function closeNotification(cm, newVal) {
|
||||
if (cm.state.currentNotificationClose)
|
||||
cm.state.currentNotificationClose();
|
||||
cm.state.currentNotificationClose = newVal;
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("openDialog", function(template, callback, options) {
|
||||
if (!options) options = {};
|
||||
|
||||
closeNotification(this, null);
|
||||
|
||||
var dialog = dialogDiv(this, template, options.bottom);
|
||||
var closed = false, me = this;
|
||||
function close(newVal) {
|
||||
if (typeof newVal == 'string') {
|
||||
inp.value = newVal;
|
||||
} else {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
me.focus();
|
||||
|
||||
if (options.onClose) options.onClose(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
var inp = dialog.getElementsByTagName("input")[0], button;
|
||||
if (inp) {
|
||||
inp.focus();
|
||||
|
||||
if (options.value) {
|
||||
inp.value = options.value;
|
||||
if (options.selectValueOnOpen !== false) {
|
||||
inp.select();
|
||||
}
|
||||
}
|
||||
|
||||
if (options.onInput)
|
||||
CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
|
||||
if (options.onKeyUp)
|
||||
CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
|
||||
|
||||
CodeMirror.on(inp, "keydown", function(e) {
|
||||
if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
|
||||
if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
|
||||
inp.blur();
|
||||
CodeMirror.e_stop(e);
|
||||
close();
|
||||
}
|
||||
if (e.keyCode == 13) callback(inp.value, e);
|
||||
});
|
||||
|
||||
if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
|
||||
} else if (button = dialog.getElementsByTagName("button")[0]) {
|
||||
CodeMirror.on(button, "click", function() {
|
||||
close();
|
||||
me.focus();
|
||||
});
|
||||
|
||||
if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
|
||||
|
||||
button.focus();
|
||||
}
|
||||
return close;
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
|
||||
closeNotification(this, null);
|
||||
var dialog = dialogDiv(this, template, options && options.bottom);
|
||||
var buttons = dialog.getElementsByTagName("button");
|
||||
var closed = false, me = this, blurring = 1;
|
||||
function close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
me.focus();
|
||||
}
|
||||
buttons[0].focus();
|
||||
for (var i = 0; i < buttons.length; ++i) {
|
||||
var b = buttons[i];
|
||||
(function(callback) {
|
||||
CodeMirror.on(b, "click", function(e) {
|
||||
CodeMirror.e_preventDefault(e);
|
||||
close();
|
||||
if (callback) callback(me);
|
||||
});
|
||||
})(callbacks[i]);
|
||||
CodeMirror.on(b, "blur", function() {
|
||||
--blurring;
|
||||
setTimeout(function() { if (blurring <= 0) close(); }, 200);
|
||||
});
|
||||
CodeMirror.on(b, "focus", function() { ++blurring; });
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* openNotification
|
||||
* Opens a notification, that can be closed with an optional timer
|
||||
* (default 5000ms timer) and always closes on click.
|
||||
*
|
||||
* If a notification is opened while another is opened, it will close the
|
||||
* currently opened one and open the new one immediately.
|
||||
*/
|
||||
CodeMirror.defineExtension("openNotification", function(template, options) {
|
||||
closeNotification(this, close);
|
||||
var dialog = dialogDiv(this, template, options && options.bottom);
|
||||
var closed = false, doneTimer;
|
||||
var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
|
||||
|
||||
function close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
clearTimeout(doneTimer);
|
||||
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
}
|
||||
|
||||
CodeMirror.on(dialog, 'click', function(e) {
|
||||
CodeMirror.e_preventDefault(e);
|
||||
close();
|
||||
});
|
||||
|
||||
if (duration)
|
||||
doneTimer = setTimeout(close, duration);
|
||||
|
||||
return close;
|
||||
});
|
||||
});
|
@ -1,47 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"))
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod)
|
||||
else // Plain browser env
|
||||
mod(CodeMirror)
|
||||
})(function(CodeMirror) {
|
||||
"use strict"
|
||||
|
||||
CodeMirror.defineOption("autoRefresh", false, function(cm, val) {
|
||||
if (cm.state.autoRefresh) {
|
||||
stopListening(cm, cm.state.autoRefresh)
|
||||
cm.state.autoRefresh = null
|
||||
}
|
||||
if (val && cm.display.wrapper.offsetHeight == 0)
|
||||
startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250})
|
||||
})
|
||||
|
||||
function startListening(cm, state) {
|
||||
function check() {
|
||||
if (cm.display.wrapper.offsetHeight) {
|
||||
stopListening(cm, state)
|
||||
if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight)
|
||||
cm.refresh()
|
||||
} else {
|
||||
state.timeout = setTimeout(check, state.delay)
|
||||
}
|
||||
}
|
||||
state.timeout = setTimeout(check, state.delay)
|
||||
state.hurry = function() {
|
||||
clearTimeout(state.timeout)
|
||||
state.timeout = setTimeout(check, 50)
|
||||
}
|
||||
CodeMirror.on(window, "mouseup", state.hurry)
|
||||
CodeMirror.on(window, "keyup", state.hurry)
|
||||
}
|
||||
|
||||
function stopListening(_cm, state) {
|
||||
clearTimeout(state.timeout)
|
||||
CodeMirror.off(window, "mouseup", state.hurry)
|
||||
CodeMirror.off(window, "keyup", state.hurry)
|
||||
}
|
||||
});
|
@ -1,6 +0,0 @@
|
||||
.CodeMirror-fullscreen {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
height: auto;
|
||||
z-index: 9;
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineOption("fullScreen", false, function(cm, val, old) {
|
||||
if (old == CodeMirror.Init) old = false;
|
||||
if (!old == !val) return;
|
||||
if (val) setFullscreen(cm);
|
||||
else setNormal(cm);
|
||||
});
|
||||
|
||||
function setFullscreen(cm) {
|
||||
var wrap = cm.getWrapperElement();
|
||||
cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset,
|
||||
width: wrap.style.width, height: wrap.style.height};
|
||||
wrap.style.width = "";
|
||||
wrap.style.height = "auto";
|
||||
wrap.className += " CodeMirror-fullscreen";
|
||||
document.documentElement.style.overflow = "hidden";
|
||||
cm.refresh();
|
||||
}
|
||||
|
||||
function setNormal(cm) {
|
||||
var wrap = cm.getWrapperElement();
|
||||
wrap.className = wrap.className.replace(/\s*CodeMirror-fullscreen\b/, "");
|
||||
document.documentElement.style.overflow = "";
|
||||
var info = cm.state.fullScreenRestore;
|
||||
wrap.style.width = info.width; wrap.style.height = info.height;
|
||||
window.scrollTo(info.scrollLeft, info.scrollTop);
|
||||
cm.refresh();
|
||||
}
|
||||
});
|
@ -1,127 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
CodeMirror.defineExtension("addPanel", function(node, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!this.state.panels) initPanels(this);
|
||||
|
||||
var info = this.state.panels;
|
||||
var wrapper = info.wrapper;
|
||||
var cmWrapper = this.getWrapperElement();
|
||||
var replace = options.replace instanceof Panel && !options.replace.cleared;
|
||||
|
||||
if (options.after instanceof Panel && !options.after.cleared) {
|
||||
wrapper.insertBefore(node, options.before.node.nextSibling);
|
||||
} else if (options.before instanceof Panel && !options.before.cleared) {
|
||||
wrapper.insertBefore(node, options.before.node);
|
||||
} else if (replace) {
|
||||
wrapper.insertBefore(node, options.replace.node);
|
||||
info.panels++;
|
||||
options.replace.clear();
|
||||
} else if (options.position == "bottom") {
|
||||
wrapper.appendChild(node);
|
||||
} else if (options.position == "before-bottom") {
|
||||
wrapper.insertBefore(node, cmWrapper.nextSibling);
|
||||
} else if (options.position == "after-top") {
|
||||
wrapper.insertBefore(node, cmWrapper);
|
||||
} else {
|
||||
wrapper.insertBefore(node, wrapper.firstChild);
|
||||
}
|
||||
|
||||
var height = (options && options.height) || node.offsetHeight;
|
||||
this._setSize(null, info.heightLeft -= height);
|
||||
if (!replace) {
|
||||
info.panels++;
|
||||
}
|
||||
if (options.stable && isAtTop(this, node))
|
||||
this.scrollTo(null, this.getScrollInfo().top + height)
|
||||
|
||||
return new Panel(this, node, options, height);
|
||||
});
|
||||
|
||||
function Panel(cm, node, options, height) {
|
||||
this.cm = cm;
|
||||
this.node = node;
|
||||
this.options = options;
|
||||
this.height = height;
|
||||
this.cleared = false;
|
||||
}
|
||||
|
||||
Panel.prototype.clear = function() {
|
||||
if (this.cleared) return;
|
||||
this.cleared = true;
|
||||
var info = this.cm.state.panels;
|
||||
this.cm._setSize(null, info.heightLeft += this.height);
|
||||
if (this.options.stable && isAtTop(this.cm, this.node))
|
||||
this.cm.scrollTo(null, this.cm.getScrollInfo().top - this.height)
|
||||
info.wrapper.removeChild(this.node);
|
||||
if (--info.panels == 0) removePanels(this.cm);
|
||||
};
|
||||
|
||||
Panel.prototype.changed = function(height) {
|
||||
var newHeight = height == null ? this.node.offsetHeight : height;
|
||||
var info = this.cm.state.panels;
|
||||
this.cm._setSize(null, info.heightLeft -= (newHeight - this.height));
|
||||
this.height = newHeight;
|
||||
};
|
||||
|
||||
function initPanels(cm) {
|
||||
var wrap = cm.getWrapperElement();
|
||||
var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle;
|
||||
var height = parseInt(style.height);
|
||||
var info = cm.state.panels = {
|
||||
setHeight: wrap.style.height,
|
||||
heightLeft: height,
|
||||
panels: 0,
|
||||
wrapper: document.createElement("div")
|
||||
};
|
||||
wrap.parentNode.insertBefore(info.wrapper, wrap);
|
||||
var hasFocus = cm.hasFocus();
|
||||
info.wrapper.appendChild(wrap);
|
||||
if (hasFocus) cm.focus();
|
||||
|
||||
cm._setSize = cm.setSize;
|
||||
if (height != null) cm.setSize = function(width, newHeight) {
|
||||
if (newHeight == null) return this._setSize(width, newHeight);
|
||||
info.setHeight = newHeight;
|
||||
if (typeof newHeight != "number") {
|
||||
var px = /^(\d+\.?\d*)px$/.exec(newHeight);
|
||||
if (px) {
|
||||
newHeight = Number(px[1]);
|
||||
} else {
|
||||
info.wrapper.style.height = newHeight;
|
||||
newHeight = info.wrapper.offsetHeight;
|
||||
info.wrapper.style.height = "";
|
||||
}
|
||||
}
|
||||
cm._setSize(width, info.heightLeft += (newHeight - height));
|
||||
height = newHeight;
|
||||
};
|
||||
}
|
||||
|
||||
function removePanels(cm) {
|
||||
var info = cm.state.panels;
|
||||
cm.state.panels = null;
|
||||
|
||||
var wrap = cm.getWrapperElement();
|
||||
info.wrapper.parentNode.replaceChild(wrap, info.wrapper);
|
||||
wrap.style.height = info.setHeight;
|
||||
cm.setSize = cm._setSize;
|
||||
cm.setSize();
|
||||
}
|
||||
|
||||
function isAtTop(cm, dom) {
|
||||
for (var sibling = dom.nextSibling; sibling; sibling = sibling.nextSibling)
|
||||
if (sibling == cm.getWrapperElement()) return true
|
||||
return false
|
||||
}
|
||||
});
|
@ -1,63 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
CodeMirror.defineOption("placeholder", "", function(cm, val, old) {
|
||||
var prev = old && old != CodeMirror.Init;
|
||||
if (val && !prev) {
|
||||
cm.on("blur", onBlur);
|
||||
cm.on("change", onChange);
|
||||
cm.on("swapDoc", onChange);
|
||||
onChange(cm);
|
||||
} else if (!val && prev) {
|
||||
cm.off("blur", onBlur);
|
||||
cm.off("change", onChange);
|
||||
cm.off("swapDoc", onChange);
|
||||
clearPlaceholder(cm);
|
||||
var wrapper = cm.getWrapperElement();
|
||||
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "");
|
||||
}
|
||||
|
||||
if (val && !cm.hasFocus()) onBlur(cm);
|
||||
});
|
||||
|
||||
function clearPlaceholder(cm) {
|
||||
if (cm.state.placeholder) {
|
||||
cm.state.placeholder.parentNode.removeChild(cm.state.placeholder);
|
||||
cm.state.placeholder = null;
|
||||
}
|
||||
}
|
||||
function setPlaceholder(cm) {
|
||||
clearPlaceholder(cm);
|
||||
var elt = cm.state.placeholder = document.createElement("pre");
|
||||
elt.style.cssText = "height: 0; overflow: visible";
|
||||
elt.style.direction = cm.getOption("direction");
|
||||
elt.className = "CodeMirror-placeholder CodeMirror-line-like";
|
||||
var placeHolder = cm.getOption("placeholder")
|
||||
if (typeof placeHolder == "string") placeHolder = document.createTextNode(placeHolder)
|
||||
elt.appendChild(placeHolder)
|
||||
cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild);
|
||||
}
|
||||
|
||||
function onBlur(cm) {
|
||||
if (isEmpty(cm)) setPlaceholder(cm);
|
||||
}
|
||||
function onChange(cm) {
|
||||
var wrapper = cm.getWrapperElement(), empty = isEmpty(cm);
|
||||
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "") + (empty ? " CodeMirror-empty" : "");
|
||||
|
||||
if (empty) setPlaceholder(cm);
|
||||
else clearPlaceholder(cm);
|
||||
}
|
||||
|
||||
function isEmpty(cm) {
|
||||
return (cm.lineCount() === 1) && (cm.getLine(0) === "");
|
||||
}
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineOption("rulers", false, function(cm, val) {
|
||||
if (cm.state.rulerDiv) {
|
||||
cm.state.rulerDiv.parentElement.removeChild(cm.state.rulerDiv)
|
||||
cm.state.rulerDiv = null
|
||||
cm.off("refresh", drawRulers)
|
||||
}
|
||||
if (val && val.length) {
|
||||
cm.state.rulerDiv = cm.display.lineSpace.parentElement.insertBefore(document.createElement("div"), cm.display.lineSpace)
|
||||
cm.state.rulerDiv.className = "CodeMirror-rulers"
|
||||
drawRulers(cm)
|
||||
cm.on("refresh", drawRulers)
|
||||
}
|
||||
});
|
||||
|
||||
function drawRulers(cm) {
|
||||
cm.state.rulerDiv.textContent = ""
|
||||
var val = cm.getOption("rulers");
|
||||
var cw = cm.defaultCharWidth();
|
||||
var left = cm.charCoords(CodeMirror.Pos(cm.firstLine(), 0), "div").left;
|
||||
cm.state.rulerDiv.style.minHeight = (cm.display.scroller.offsetHeight + 30) + "px";
|
||||
for (var i = 0; i < val.length; i++) {
|
||||
var elt = document.createElement("div");
|
||||
elt.className = "CodeMirror-ruler";
|
||||
var col, conf = val[i];
|
||||
if (typeof conf == "number") {
|
||||
col = conf;
|
||||
} else {
|
||||
col = conf.column;
|
||||
if (conf.className) elt.className += " " + conf.className;
|
||||
if (conf.color) elt.style.borderColor = conf.color;
|
||||
if (conf.lineStyle) elt.style.borderLeftStyle = conf.lineStyle;
|
||||
if (conf.width) elt.style.borderLeftWidth = conf.width;
|
||||
}
|
||||
elt.style.left = (left + col * cw) + "px";
|
||||
cm.state.rulerDiv.appendChild(elt)
|
||||
}
|
||||
}
|
||||
});
|
@ -1,191 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
var defaults = {
|
||||
pairs: "()[]{}''\"\"",
|
||||
closeBefore: ")]}'\":;>",
|
||||
triples: "",
|
||||
explode: "[]{}"
|
||||
};
|
||||
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
cm.removeKeyMap(keyMap);
|
||||
cm.state.closeBrackets = null;
|
||||
}
|
||||
if (val) {
|
||||
ensureBound(getOption(val, "pairs"))
|
||||
cm.state.closeBrackets = val;
|
||||
cm.addKeyMap(keyMap);
|
||||
}
|
||||
});
|
||||
|
||||
function getOption(conf, name) {
|
||||
if (name == "pairs" && typeof conf == "string") return conf;
|
||||
if (typeof conf == "object" && conf[name] != null) return conf[name];
|
||||
return defaults[name];
|
||||
}
|
||||
|
||||
var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
|
||||
function ensureBound(chars) {
|
||||
for (var i = 0; i < chars.length; i++) {
|
||||
var ch = chars.charAt(i), key = "'" + ch + "'"
|
||||
if (!keyMap[key]) keyMap[key] = handler(ch)
|
||||
}
|
||||
}
|
||||
ensureBound(defaults.pairs + "`")
|
||||
|
||||
function handler(ch) {
|
||||
return function(cm) { return handleChar(cm, ch); };
|
||||
}
|
||||
|
||||
function getConfig(cm) {
|
||||
var deflt = cm.state.closeBrackets;
|
||||
if (!deflt || deflt.override) return deflt;
|
||||
var mode = cm.getModeAt(cm.getCursor());
|
||||
return mode.closeBrackets || deflt;
|
||||
}
|
||||
|
||||
function handleBackspace(cm) {
|
||||
var conf = getConfig(cm);
|
||||
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
|
||||
var pairs = getOption(conf, "pairs");
|
||||
var ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
if (!ranges[i].empty()) return CodeMirror.Pass;
|
||||
var around = charsAround(cm, ranges[i].head);
|
||||
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
|
||||
}
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
var cur = ranges[i].head;
|
||||
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
|
||||
}
|
||||
}
|
||||
|
||||
function handleEnter(cm) {
|
||||
var conf = getConfig(cm);
|
||||
var explode = conf && getOption(conf, "explode");
|
||||
if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
|
||||
var ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
if (!ranges[i].empty()) return CodeMirror.Pass;
|
||||
var around = charsAround(cm, ranges[i].head);
|
||||
if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
|
||||
}
|
||||
cm.operation(function() {
|
||||
var linesep = cm.lineSeparator() || "\n";
|
||||
cm.replaceSelection(linesep + linesep, null);
|
||||
cm.execCommand("goCharLeft");
|
||||
ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var line = ranges[i].head.line;
|
||||
cm.indentLine(line, null, true);
|
||||
cm.indentLine(line + 1, null, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function contractSelection(sel) {
|
||||
var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
|
||||
return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
|
||||
head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
|
||||
}
|
||||
|
||||
function handleChar(cm, ch) {
|
||||
var conf = getConfig(cm);
|
||||
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
|
||||
var pairs = getOption(conf, "pairs");
|
||||
var pos = pairs.indexOf(ch);
|
||||
if (pos == -1) return CodeMirror.Pass;
|
||||
|
||||
var closeBefore = getOption(conf,"closeBefore");
|
||||
|
||||
var triples = getOption(conf, "triples");
|
||||
|
||||
var identical = pairs.charAt(pos + 1) == ch;
|
||||
var ranges = cm.listSelections();
|
||||
var opening = pos % 2 == 0;
|
||||
|
||||
var type;
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i], cur = range.head, curType;
|
||||
var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
|
||||
if (opening && !range.empty()) {
|
||||
curType = "surround";
|
||||
} else if ((identical || !opening) && next == ch) {
|
||||
if (identical && stringStartsAfter(cm, cur))
|
||||
curType = "both";
|
||||
else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
|
||||
curType = "skipThree";
|
||||
else
|
||||
curType = "skip";
|
||||
} else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
|
||||
cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) {
|
||||
if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass;
|
||||
curType = "addFour";
|
||||
} else if (identical) {
|
||||
var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur)
|
||||
if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both";
|
||||
else return CodeMirror.Pass;
|
||||
} else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) {
|
||||
curType = "both";
|
||||
} else {
|
||||
return CodeMirror.Pass;
|
||||
}
|
||||
if (!type) type = curType;
|
||||
else if (type != curType) return CodeMirror.Pass;
|
||||
}
|
||||
|
||||
var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
|
||||
var right = pos % 2 ? ch : pairs.charAt(pos + 1);
|
||||
cm.operation(function() {
|
||||
if (type == "skip") {
|
||||
cm.execCommand("goCharRight");
|
||||
} else if (type == "skipThree") {
|
||||
for (var i = 0; i < 3; i++)
|
||||
cm.execCommand("goCharRight");
|
||||
} else if (type == "surround") {
|
||||
var sels = cm.getSelections();
|
||||
for (var i = 0; i < sels.length; i++)
|
||||
sels[i] = left + sels[i] + right;
|
||||
cm.replaceSelections(sels, "around");
|
||||
sels = cm.listSelections().slice();
|
||||
for (var i = 0; i < sels.length; i++)
|
||||
sels[i] = contractSelection(sels[i]);
|
||||
cm.setSelections(sels);
|
||||
} else if (type == "both") {
|
||||
cm.replaceSelection(left + right, null);
|
||||
cm.triggerElectric(left + right);
|
||||
cm.execCommand("goCharLeft");
|
||||
} else if (type == "addFour") {
|
||||
cm.replaceSelection(left + left + left + left, "before");
|
||||
cm.execCommand("goCharRight");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function charsAround(cm, pos) {
|
||||
var str = cm.getRange(Pos(pos.line, pos.ch - 1),
|
||||
Pos(pos.line, pos.ch + 1));
|
||||
return str.length == 2 ? str : null;
|
||||
}
|
||||
|
||||
function stringStartsAfter(cm, pos) {
|
||||
var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
|
||||
return /\bstring/.test(token.type) && token.start == pos.ch &&
|
||||
(pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos)))
|
||||
}
|
||||
});
|