├── .babelrc
├── .coveragerc
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .python-version
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── build.sh
├── bun.lockb
├── package.json
├── pyproject.toml
├── teamvault
├── __init__.py
├── __version__.py
├── apps
│ ├── __init__.py
│ ├── accounts
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── context_processors.py
│ │ ├── forms.py
│ │ ├── migrations
│ │ │ ├── 0001_initial.py
│ │ │ ├── 0002_add_user_settings.py
│ │ │ ├── 0003_usersettings_avatar.py
│ │ │ ├── 0004_alter_usersettings_hide_deleted_secrets.py
│ │ │ ├── 0005_rename_usersettings_userprofile.py
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── templates
│ │ │ └── accounts
│ │ │ │ ├── _avatar.html
│ │ │ │ ├── login.html
│ │ │ │ ├── logout.html
│ │ │ │ ├── user_detail.html
│ │ │ │ ├── user_list.html
│ │ │ │ └── user_settings.html
│ │ ├── urls.py
│ │ ├── utils.py
│ │ └── views.py
│ ├── audit
│ │ ├── __init__.py
│ │ ├── auditlog.py
│ │ ├── filters.py
│ │ ├── migrations
│ │ │ ├── 0001_initial.py
│ │ │ ├── 0002_auto_20170313_1544.py
│ │ │ ├── 0003_logentry_categories.py
│ │ │ ├── 0004_alter_logentry_category.py
│ │ │ ├── 0005_alter_logentry_category.py
│ │ │ ├── 0006_logentry_reason.py
│ │ │ ├── 0007_alter_logentry_category.py
│ │ │ ├── 0008_logentry_logentry_category_idx_and_more.py
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── templates
│ │ │ └── audit
│ │ │ │ └── log.html
│ │ ├── urls.py
│ │ └── views.py
│ ├── secrets
│ │ ├── __init__.py
│ │ ├── api
│ │ │ ├── __init__.py
│ │ │ ├── serializers.py
│ │ │ ├── urls.py
│ │ │ └── views.py
│ │ ├── context_processors.py
│ │ ├── exceptions.py
│ │ ├── filters.py
│ │ ├── forms.py
│ │ ├── management
│ │ │ ├── __init__.py
│ │ │ └── commands
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create_fake_data.py
│ │ │ │ └── update_search_index.py
│ │ ├── migrations
│ │ │ ├── 0001_initial.py
│ │ │ ├── 0002_secret_filename.py
│ │ │ ├── 0003_auto_20150113_1915.py
│ │ │ ├── 0004_unaccent_extension.py
│ │ │ ├── 0005_secret_search_index.py
│ │ │ ├── 0006_auto_20150124_1103.py
│ │ │ ├── 0007_auto_20150205_1918.py
│ │ │ ├── 0008_auto_20150322_0944.py
│ │ │ ├── 0009_auto_20150322_0949.py
│ │ │ ├── 0011_remove_secret_search_index.py
│ │ │ ├── 0012_secret_search_index.py
│ │ │ ├── 0013_auto_20161021_1411.py
│ │ │ ├── 0014_auto_20170313_1544.py
│ │ │ ├── 0015_secretrevision_plaintext_data_sha256.py
│ │ │ ├── 0016_auto_20180220_1053.py
│ │ │ ├── 0017_auto_20180220_1115.py
│ │ │ ├── 0018_auto_20180220_1244.py
│ │ │ ├── 0019_secret_notify_on_access_request.py
│ │ │ ├── 0020_auto_20180220_1356.py
│ │ │ ├── 0021_auto_20180220_1428.py
│ │ │ ├── 0022_secret_last_changed.py
│ │ │ ├── 0023_auto_20190822_1234.py
│ │ │ ├── 0024_auto_20210824_1203.py
│ │ │ ├── 0025_alter_secret_description_alter_secret_name.py
│ │ │ ├── 0026_allowed_groups_users_intermediate.py
│ │ │ ├── 0027_sharedsecretdata_only_one_set.py
│ │ │ ├── 0028_sharedsecretdata_grant_description_and_more.py
│ │ │ ├── 0029_sharedsecretdata_granted_by.py
│ │ │ ├── 0030_sharedsecretdata_granted_on.py
│ │ │ ├── 0031_rename_share_data_fields.py
│ │ │ ├── 0032_alter_sharedsecretdata_granted_by.py
│ │ │ ├── 0033_secretrevision_encrypted_otp_key.py
│ │ │ ├── 0034_remove_secretrevision_encrypted_otp_key_and_more.py
│ │ │ ├── 0035_remove_secretrevision_encrypted_otp_key_data_and_more.py
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── tasks.py
│ │ ├── templates
│ │ │ ├── opensearch.xml
│ │ │ └── secrets
│ │ │ │ ├── addedit_content
│ │ │ │ ├── cc.html
│ │ │ │ ├── file.html
│ │ │ │ └── password.html
│ │ │ │ ├── dashboard.html
│ │ │ │ ├── detail_content
│ │ │ │ ├── _js.html
│ │ │ │ ├── _su_confirm_modal.html
│ │ │ │ ├── cc.html
│ │ │ │ ├── file.html
│ │ │ │ ├── meta.html
│ │ │ │ └── password.html
│ │ │ │ ├── search
│ │ │ │ └── _search_item.html
│ │ │ │ ├── secret_addedit.html
│ │ │ │ ├── secret_delete.html
│ │ │ │ ├── secret_detail.html
│ │ │ │ ├── secret_list.html
│ │ │ │ ├── secret_restore.html
│ │ │ │ ├── secret_row.html
│ │ │ │ ├── secret_search.html
│ │ │ │ └── share_content
│ │ │ │ ├── _share_list_entry.html
│ │ │ │ └── share_list_modal.html
│ │ ├── templatetags
│ │ │ ├── __init__.py
│ │ │ └── smart_pagination.py
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ └── test_model_consistency.py
│ │ ├── urls.py
│ │ ├── utils.py
│ │ ├── validators.py
│ │ └── views.py
│ └── settings
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ │ ├── models.py
│ │ └── webpack.py
├── cli.py
├── manage.py
├── middleware.py
├── settings.py
├── static
│ ├── js
│ │ ├── index.js
│ │ ├── otp.js
│ │ └── utils.js
│ └── scss
│ │ ├── avatars.scss
│ │ ├── base.scss
│ │ ├── card.scss
│ │ ├── circularProgressbar.scss
│ │ ├── fontawesome.scss
│ │ ├── scrollbar.scss
│ │ ├── search.scss
│ │ ├── secrets.scss
│ │ ├── select2.scss
│ │ └── theme.scss
├── templates
│ ├── 404_anon.html
│ ├── 404_loggedin.html
│ ├── base.html
│ ├── base_nav.html
│ ├── helpers
│ │ ├── filter.html
│ │ ├── filter_item.html
│ │ └── filter_row.html
│ ├── pagination.html
│ └── rest_framework
│ │ └── api.html
├── urls.py
├── views.py
└── wsgi.py
├── uv.lock
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["@babel/syntax-dynamic-import"],
3 | "presets": [
4 | [
5 | "@babel/preset-env",
6 | {
7 | "modules": false
8 | }
9 | ]
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | source = teamvault
4 |
5 | [report]
6 | omit = */migrations/*
7 |
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | [*.{js,json,yml}]
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | /.yarn/** linguist-vendored
2 | /.yarn/releases/* binary
3 | /.yarn/plugins/**/* binary
4 | /.pnp.* binary linguist-generated
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info/
2 | build/
3 | dist/
4 | huey.db
5 | teamvault.cfg
6 | teamvault/static/bundled/
7 | teamvault/static_collected/
8 | teamvault/webpack-stats.json
9 | package-lock.json
10 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.9.2
2 |
3 | 2021-02-08
4 |
5 | * follow deeplinks after Google login
6 |
7 |
8 | # 0.9.1
9 |
10 | 2021-02-01
11 |
12 | * fixed missing social auth depedency
13 |
14 |
15 | # 0.9.0
16 |
17 | 2021-02-01
18 |
19 | * added Google OAuth2
20 | * added LDAP client certificate auth and StartTLS
21 | * descriptions now have line breaks and clickable links
22 | * improved LDAP debug logging
23 | * removed Share button
24 | * updated Python dependencies
25 |
26 |
27 | # 0.8.5
28 |
29 | 2020-11-13
30 |
31 | * fixed bootstrapping problem when running `teamvault upgrade` on empty DB
32 |
33 |
34 | # 0.8.4
35 |
36 | 2019-11-06
37 |
38 | * fixed sending email notifications
39 |
40 |
41 | # 0.8.3
42 |
43 | 2019-10-24
44 |
45 | * fixed creating access requests
46 |
47 |
48 | # 0.8.2
49 |
50 | 2019-10-23
51 |
52 | * fixed creating secrets by API
53 |
54 |
55 | # 0.8.1
56 |
57 | 2019-09-23
58 |
59 | * fixed packaging issue
60 |
61 |
62 | # 0.8.0
63 |
64 | 2019-09-23
65 |
66 | * added hidden URL parameters for filtering search results
67 | * replaced owners with notification settings
68 | * fixed storage of credit card CVV values as integers
69 | * fixed deleting secrets by API
70 | * fixed storing past iterations of passwords
71 |
72 |
73 | # 0.7.3
74 |
75 | 2017-03-26
76 |
77 | * fixed pagination with GET parameters
78 |
79 |
80 | # 0.7.2
81 |
82 | 2017-03-13
83 |
84 | * fixed missing opensearch.xml
85 | * improved database integrity protection
86 |
87 |
88 | # 0.7.1
89 |
90 | 2017-03-06
91 |
92 | * fixed "needs changing on leave" option
93 | * include actions on user in user audit log
94 |
95 |
96 | # 0.7.0
97 |
98 | 2017-03-05
99 |
100 | * added `teamvault run --bind`
101 | * added audit log
102 | * added OpenSearch
103 | * added user management
104 | * added user-friendly URLs to API output
105 | * removed syslog logging in favor of stdout
106 | * improved secret status diplay
107 | * fixed access request API
108 | * fixed API pagination
109 |
110 |
111 | # 0.6.1
112 |
113 | 2016-11-07
114 |
115 | * fixed an issue that prevented adding oneself to owners and allowed users
116 |
117 |
118 | # 0.6.0
119 |
120 | 2016-11-06
121 |
122 | * added search bar to every page
123 | * added secret details in access request view
124 | * added most used and recently used secrets to dashboard
125 | * added secret owners
126 | * new fonts
127 | * removed broken hotkey copy feature
128 | * fixed assignment of deactivated users as reviewers
129 |
130 |
131 | # 0.5.1
132 |
133 | 2015-10-27
134 |
135 | * added more copy confirmation messages
136 | * used brighter colors for password strength indication
137 | * fix exception when searching via API
138 |
139 |
140 | # 0.5.0
141 |
142 | 2015-10-24
143 |
144 | * added rudimentary password generator and strength meter
145 | * added 404 error pages
146 | * added secret restoration for admins
147 | * fixed revealing credit card secrets
148 | * fixed display of deleted secrets
149 |
150 |
151 | # 0.4.3
152 |
153 | 2015-05-25
154 |
155 | * show username field by default when adding passwords
156 | * fixed `teamvault upgrade` missing update_search_field
157 | * fixed typing in secret sharing modal
158 |
159 |
160 | # 0.4.2
161 |
162 | 2015-05-19
163 |
164 | * added a password copy confirmation message
165 | * improved pagination
166 | * made session settings configurable
167 | * fixed duplicate search results
168 |
169 |
170 | # 0.4.1
171 |
172 | 2015-04-15
173 |
174 | * fixed missing email templates in distribution
175 | * fixed Python 3 tag on wheel distribution
176 | * fixed exceptions not being logged
177 | * fixed exception when closing access request as non-reviewer
178 |
179 |
180 | # 0.4.0
181 |
182 | 2015-04-06
183 |
184 | * changed URLs to use hashids
185 | * added substring search for filename, URL, and username
186 | * added notification emails for access requests
187 | * fixed display of allowed users/group in secret detail view
188 |
189 |
190 | # 0.3.0
191 |
192 | 2015-02-05
193 |
194 | * added full text search
195 | * added search API
196 | * improved secret list display
197 | * added pagination for secret lists
198 | * relaxed URL validation even further
199 |
200 |
201 | # 0.2.2
202 |
203 | 2015-01-27
204 |
205 | * fixed overzealous URL validation
206 | * fixed access policy selection
207 |
208 |
209 | # 0.2.1
210 |
211 | 2015-01-20
212 |
213 | * fixed uploading of non-tiny files (#30)
214 | * fixed editing secrets without changing encrypted data (#30)
215 |
216 |
217 | # 0.2.0
218 |
219 | 2015-01-11
220 |
221 | * added file secrets
222 | * added credit card secrets
223 | * added logging to syslog
224 | * added `teamvault plumbing` command
225 | * fixed login with some WebKit-based browsers
226 |
227 |
228 | # 0.1.0
229 |
230 | 2014-12-20
231 |
232 | * first public release
233 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include CHANGELOG.md
2 | include LICENSE
3 | include MANIFEST.in
4 | include README.md
5 | include pyproject.toml
6 |
7 | recursive-include teamvault *.html
8 | recursive-include teamvault *.json
9 | recursive-include teamvault *.txt
10 | recursive-include teamvault *.xml
11 | recursive-include teamvault/static *
12 | recursive-exclude * *.py[co]
13 | recursive-exclude * .DS_Store
14 | recursive-exclude * __pycache__
15 | recursive-exclude node_modules *
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TeamVault
2 |
3 | TeamVault is an open-source web-based shared password manager for behind-the-firewall installation. It requires Python 3.10+ and PostgreSQL (with the unaccent extension).
4 |
5 | ## Installation
6 |
7 | apt-get install libffi-dev libldap2-dev libpq-dev libsasl2-dev python3.X-dev postgresql-contrib
8 | pip install teamvault
9 | teamvault setup
10 | vim /etc/teamvault.conf
11 | # note that the teamvault database user will need SUPERUSER privileges
12 | # during this step in order to activate the unaccent extension
13 | teamvault upgrade
14 | teamvault plumbing createsuperuser
15 | teamvault run
16 |
17 | ## Update
18 |
19 | pip install --upgrade teamvault
20 | teamvault upgrade
21 |
22 | ## Development
23 | ### Start a PostgreSQL database
24 | Create a database and superuser for TeamVault to use, for example by starting a Docker container:
25 |
26 | docker run --rm --detach --publish=5432:5432 --name teamvault-postgres -e POSTGRES_USER=teamvault -e POSTGRES_PASSWORD=teamvault postgres:latest
27 |
28 |
29 | ### Run Webpack to serve static files
30 | To compile all JS & SCSS files, you'll need to install all required packages via bun (or yarn/npm) with node >= v18.
31 |
32 | Use ```bun/yarn/npm run serve``` to start a dev server.
33 |
34 | **Note**:
35 | Some MacOS users have reported errors when running the dev server via bun. In this case feel free to switch to NPM.
36 |
37 |
38 | ### Configure your Virtualenv via uv
39 | uv sync
40 |
41 | ### Setup TeamVault
42 | export TEAMVAULT_CONFIG_FILE=teamvault.cfg
43 | teamvault setup
44 | vim teamvault.cfg # base_url = http://localhost:8000
45 | # session_cookie_secure = False
46 | # database config as needed
47 | teamvault upgrade
48 | teamvault plumbing createsuperuser
49 |
50 | ### Start the development server
51 | teamvault run
52 |
53 | Now open http://localhost:8000
54 |
55 | ## Scheduled background jobs
56 |
57 | We use [huey](https://huey.readthedocs.io/en/latest/) to run background jobs. This requires you to run a second process, in parallel to TeamVault itself. You can launch it via `manage.py`:
58 |
59 | teamvault run_huey
60 |
61 | ## Release process
62 | 1. Bump the version in ```teamvault/__version__.py``` and ```pyproject.toml```
63 | 2. Update CHANGELOG.md with the new version and current date
64 | 3. Make a release commit with the changes made above
65 | 4. Push the commit
66 | 5. Run ```./build.sh``` to create a new package
67 | 6. Sign and push the artifacts to PyPI via ```uv publish```
68 | 7. Test that the package can be installed: ```uv run --isolated --no-cache --prerelease allow --with teamvault --no-project -- teamvault --version```
69 | 8. Add a new GitHub release
70 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if command -v bun &> /dev/null; then
4 | BUILDER="bun"
5 | elif command -v yarn &> /dev/null; then
6 | BUILDER="yarn"
7 | else
8 | BUILDER="npm"
9 | fi
10 |
11 | PROJECT_DIR="$(dirname "$0")"
12 | PKG_DIR="$PROJECT_DIR/dist"
13 |
14 | cd "$PROJECT_DIR"
15 |
16 | printf ">> Cleaning up old build packages...\n\n"
17 | [ -e "$PKG_DIR" ] && printf "Deleting existing dist in $PKG_DIR...\n" && rm -r "$PKG_DIR"
18 |
19 | printf ">> Creating webpack bundle via $BUILDER...\n\n"
20 | $BUILDER run build
21 | [ $? -eq 0 ] && printf "\n>> Bundle created.\n"
22 |
23 | printf ">> Generating python package...\n\n"
24 | uv build --no-sources
25 | [ $? -eq 0 ] && printf "\n>> Generated python package in dist/."
26 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/bun.lockb
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "teamvault",
3 | "dependencies": {
4 | "@eonasdan/tempus-dominus": "^6.9.10",
5 | "@fontsource/poppins": "^5.0.14",
6 | "@fortawesome/fontawesome-free": "^6.6.0",
7 | "@popperjs/core": "^2.11.8",
8 | "@tarekraafat/autocomplete.js": "^10.2.7",
9 | "@zxcvbn-ts/core": "^3.0.4",
10 | "@zxcvbn-ts/language-common": "^3.0.4",
11 | "@zxcvbn-ts/language-en": "^3.0.2",
12 | "bigtext": "https://github.com/zachleat/BigText",
13 | "bootstrap": "^5.3.3",
14 | "card": "^2.5.4",
15 | "clipboard": "^2.0.11",
16 | "core-js": "^3.38.0",
17 | "dompurify": "^3.1.6",
18 | "esbuild": "^0.20.0",
19 | "htmx.org": "^1.9.12",
20 | "jquery": "^3.7.1",
21 | "jsqr": "^1.4.0",
22 | "lodash": "^4.17.21",
23 | "notyf": "^3.10.0",
24 | "select2": "4.0.13",
25 | "select2-bootstrap-5-theme": "^1.3.0"
26 | },
27 | "devDependencies": {
28 | "@babel/core": "^7.25.2",
29 | "@babel/preset-env": "^7.25.3",
30 | "@webpack-cli/generators": "^3.0.7",
31 | "babel-loader": "^9.1.3",
32 | "css-loader": "^6.11.0",
33 | "mini-css-extract-plugin": "^2.9.0",
34 | "sass-embedded": "^1.86.0",
35 | "sass-loader": "^16.0.5",
36 | "style-loader": "^3.3.4",
37 | "webpack": "^5.93.0",
38 | "webpack-bundle-tracker": "^3.1.0",
39 | "webpack-cli": "^5.1.4",
40 | "webpack-dev-server": "^5.0.4",
41 | "webpack-merge": "^6.0.1"
42 | },
43 | "packageChangelogComments": {
44 | "@eonasdan/tempus-dominus": "https://github.com/Eonasdan/tempus-dominus/releases",
45 | "@fontsource/poppins": "https://github.com/fontsource/fontsource/blob/main/CHANGELOG.md",
46 | "@fortawesome/fontawesome-free": "https://fontawesome.com/changelog",
47 | "@popperjs/core": "https://github.com/floating-ui/floating-ui/releases",
48 | "@tarekraafat/autocomplete.js": "https://github.com/TarekRaafat/autoComplete.js/releases",
49 | "@zxcvbn-ts/core": "https://github.com/zxcvbn-ts/zxcvbn/blob/master/packages/libraries/main/CHANGELOG.md",
50 | "@zxcvbn-ts/language-common": "https://github.com/zxcvbn-ts/zxcvbn/blob/master/packages/libraries/main/CHANGELOG.md",
51 | "@zxcvbn-ts/language-en": "https://github.com/zxcvbn-ts/zxcvbn/blob/master/packages/libraries/main/CHANGELOG.md",
52 | "bigtext": "No updates since 7 years",
53 | "bootstrap": "https://github.com/twbs/bootstrap/releases",
54 | "card": "https://github.com/jessepollak/card/releases",
55 | "clipboard": "https://github.com/zenorocha/clipboard.js/releases",
56 | "core-js": "https://github.com/zloirock/core-js/blob/master/CHANGELOG.md",
57 | "dompurify": "https://github.com/cure53/DOMPurify/releases",
58 | "esbuild": "https://github.com/evanw/esbuild/blob/main/CHANGELOG.md",
59 | "htmx.org": "https://github.com/bigskysoftware/htmx/blob/master/CHANGELOG.md",
60 | "jquery": "https://github.com/jquery/jquery/blob/main/changelog.md",
61 | "lodash": "https://github.com/lodash/lodash/wiki/Changelog",
62 | "notyf": "https://github.com/caroso1222/notyf/blob/master/CHANGELOG.md",
63 | "select2": "https://github.com/select2/select2/blob/develop/CHANGELOG.md",
64 | "select2-bootstrap-5-theme": "https://github.com/apalfrey/select2-bootstrap-5-theme/releases",
65 | "@babel/core": "https://github.com/babel/babel/blob/main/CHANGELOG.md",
66 | "@babel/preset-env": "https://github.com/babel/babel/blob/main/CHANGELOG.md",
67 | "@webpack-cli/generators": "https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md",
68 | "babel-loader": "https://github.com/babel/babel-loader/blob/main/CHANGELOG.md",
69 | "css-loader": "https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md",
70 | "mini-css-extract-plugin": "https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md",
71 | "sass(-embedded)": "https://github.com/sass/dart-sass/blob/main/CHANGELOG.md",
72 | "sass-loader": "https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md",
73 | "style-loader": "https://github.com/webpack-contrib/style-loader/blob/master/CHANGELOG.md",
74 | "webpack": "https://github.com/webpack/webpack/releases",
75 | "webpack-bundle-tracker": "https://github.com/django-webpack/webpack-bundle-tracker/releases",
76 | "webpack-cli": "https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md",
77 | "webpack-dev-server": "https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md",
78 | "webpack-merge": "https://github.com/survivejs/webpack-merge/blob/develop/CHANGELOG.md"
79 | },
80 | "version": "1.0.0",
81 | "description": "Teamvault",
82 | "scripts": {
83 | "build": "webpack --config webpack.prod.js",
84 | "serve": "webpack serve --config webpack.dev.js"
85 | },
86 | "trustedDependencies": [
87 | "core-js",
88 | "esbuild"
89 | ]
90 | }
91 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["uv_build>=0.6,<0.7"]
3 | build-backend = "uv_build"
4 |
5 | [project]
6 | name = "teamvault"
7 | description = "Keep your passwords behind the firewall"
8 | readme = "README.md"
9 | license-files = ["LICENSE"]
10 | authors = [{ name = "Seibert Group GmbH" }]
11 | requires-python = ">=3.10"
12 | classifiers = [
13 | "Development Status :: 3 - Alpha",
14 | "Environment :: Web Environment",
15 | "Framework :: Django",
16 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
17 | "Natural Language :: English",
18 | "Operating System :: POSIX :: Linux",
19 | "Programming Language :: Python :: 3",
20 | "Programming Language :: Python :: 3.10",
21 | "Programming Language :: Python :: 3.11",
22 | "Programming Language :: Python :: 3.12",
23 | "Topic :: Office/Business",
24 | "Topic :: Security",
25 | ]
26 | keywords = ["password", "safe", "manager", "sharing"]
27 | dependencies = [
28 | "cryptography~=42.0",
29 | "django-auth-ldap~=4.6",
30 | "django-bootstrap5==23.4",
31 | "django-filter==23.5",
32 | "django-htmx~=1.17",
33 | "django-webpack-loader~=3.0",
34 | "django~=5.1.8",
35 | "djangorestframework~=3.14",
36 | "gunicorn~=21.2",
37 | "hashids~=1.3",
38 | "pyotp~=2.9",
39 | "huey~=2.5",
40 | "psycopg~=3.2",
41 | "pytz~=2024.2",
42 | "requests~=2.32",
43 | "social-auth-app-django~=5.4",
44 | "whitenoise[brotli]~=6.6",
45 | ]
46 |
47 | # dynamic = ["version"] - Currently unsupported by uv_build
48 | version = '1.0.0rc6' # Also change in teamvault/__version__.py
49 |
50 | [dependency-groups]
51 | dev = [
52 | "django-stubs~=5.1",
53 | "djangorestframework-stubs~=3.15",
54 | "faker",
55 | ]
56 |
57 | [project.scripts]
58 | teamvault = "teamvault.cli:main"
59 |
60 | [project.urls]
61 | Source = "https://github.com/seibert-media/teamvault"
62 |
63 | [tool.uv]
64 | package = true
65 |
66 | [tool.uv.build-backend]
67 | module-root = ""
68 | source-include = [
69 | "CHANGELOG.md",
70 | "MANIFEST.in",
71 | ]
72 |
73 | [tool.uv.sources]
74 | teamvault = { workspace = true }
75 |
--------------------------------------------------------------------------------
/teamvault/__init__.py:
--------------------------------------------------------------------------------
1 | from teamvault.__version__ import __version__
2 |
--------------------------------------------------------------------------------
/teamvault/__version__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.0.0rc6" # Also change in pyproject.toml
2 |
--------------------------------------------------------------------------------
/teamvault/apps/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/teamvault/apps/__init__.py
--------------------------------------------------------------------------------
/teamvault/apps/accounts/__init__.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AccountsConfig(AppConfig):
5 | name = 'teamvault.apps.accounts'
6 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/auth.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.conf import settings
4 | from django_auth_ldap.backend import LDAPBackend, _LDAPUser
5 | from django_auth_ldap.config import LDAPSearch
6 |
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | def find_ldap_username_for_social_auth(details, *_args, **kwargs):
12 | if not kwargs.get('is_new'):
13 | return {}
14 |
15 | connection = _LDAPUser(LDAPBackend(), username='').connection
16 | ldap_mail_attribute = settings.AUTH_LDAP_USER_ATTR_MAP['email']
17 | social_auth_mail_value = details['email']
18 | logger.info(f'Trying to find LDAP username for social auth user {social_auth_mail_value}...')
19 | search = LDAPSearch(
20 | settings.AUTH_LDAP_USER_SEARCH.base_dn,
21 | settings.AUTH_LDAP_USER_SEARCH.scope,
22 | f'({ldap_mail_attribute}={social_auth_mail_value})',
23 | ['uid']
24 | )
25 | results = search.execute(connection)
26 | if results is not None and len(results) > 0:
27 | uid = results[0][1]['uid'][0]
28 | logger.info(f'Found LDAP username for social auth user {social_auth_mail_value}: {uid}')
29 | return {'username': uid}
30 | logger.info(f'No LDAP username found for social auth user {social_auth_mail_value}')
31 | return {}
32 |
33 |
34 | def populate_from_ldap(*_args, **kwargs):
35 | LDAPBackend().populate_user(kwargs['user'].username)
36 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/context_processors.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 |
4 | def google_auth_enabled(request):
5 | return {'google_auth_enabled': settings.GOOGLE_AUTH_ENABLED}
6 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from teamvault.apps.accounts.models import UserProfile
4 |
5 |
6 | class UserProfileForm(forms.ModelForm):
7 | def __init__(self, *args, **kwargs):
8 | super().__init__(*args, **kwargs)
9 | self.fields['default_sharing_groups'].queryset = self.fields['default_sharing_groups'].queryset.order_by('name')
10 |
11 | class Meta:
12 | fields = ['default_sharing_groups', 'hide_deleted_secrets']
13 | model = UserProfile
14 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.1 on 2023-06-15 17:48
2 |
3 | from django.db import migrations
4 |
5 |
6 | def remove_inactivate_users_from_groups(apps, schema_editor):
7 | user_model = apps.get_model('auth', 'User')
8 | for user in user_model.objects.all().exclude(groups__isnull=False, is_active=True):
9 | user.groups.clear()
10 |
11 |
12 | class Migration(migrations.Migration):
13 | dependencies = [('auth', '__latest__')]
14 |
15 | operations = [
16 | migrations.RunPython(remove_inactivate_users_from_groups),
17 | ]
18 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/migrations/0002_add_user_settings.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-07-06 15:13
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 | initial = True
10 |
11 | dependencies = [
12 | ("auth", "0012_alter_user_first_name_max_length"),
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ("accounts", "0001_initial"),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name="UserSettings",
20 | fields=[
21 | (
22 | "id",
23 | models.AutoField(
24 | auto_created=True,
25 | primary_key=True,
26 | serialize=False,
27 | verbose_name="ID",
28 | ),
29 | ),
30 | ("hide_deleted_secrets", models.BooleanField(default=True)),
31 | (
32 | "default_sharing_groups",
33 | models.ManyToManyField(
34 | blank=True,
35 | help_text="New secrets created by you will be shared with these groups.",
36 | related_name="+",
37 | to="auth.group",
38 | ),
39 | ),
40 | (
41 | "user",
42 | models.OneToOneField(
43 | on_delete=django.db.models.deletion.CASCADE,
44 | related_name="profile",
45 | to=settings.AUTH_USER_MODEL,
46 | ),
47 | ),
48 | ],
49 | ),
50 | ]
51 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/migrations/0003_usersettings_avatar.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-07-11 18:56
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("accounts", "0002_add_user_settings"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="usersettings",
14 | name="avatar",
15 | field=models.BinaryField(blank=True, null=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/migrations/0004_alter_usersettings_hide_deleted_secrets.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-08-29 12:45
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("accounts", "0003_usersettings_avatar"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="usersettings",
14 | name="hide_deleted_secrets",
15 | field=models.BooleanField(
16 | default=True,
17 | help_text="Hides deleted secrets per default. Enable them in filters to see them again.",
18 | ),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/migrations/0005_rename_usersettings_userprofile.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-09-05 11:57
2 |
3 | from django.conf import settings
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
10 | ("auth", "0012_alter_user_first_name_max_length"),
11 | ("accounts", "0004_alter_usersettings_hide_deleted_secrets"),
12 | ]
13 |
14 | operations = [
15 | migrations.RenameModel(
16 | old_name="UserSettings",
17 | new_name="UserProfile",
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/teamvault/apps/accounts/migrations/__init__.py
--------------------------------------------------------------------------------
/teamvault/apps/accounts/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import Group, User
2 | from django.db import models
3 | from django.utils.translation import gettext_lazy as _
4 |
5 |
6 | class UserProfile(models.Model):
7 | # Since our static files are not served by some webserver but by TeamVault (/Whitenoise) directly
8 | # to keep the installation overhead low, we'd have to do the same thing with media files.
9 | # Static files will get replaced with each teamvault deployment, media files should not.
10 | # Because of that, we'd have to make admins configure a persistent directory for them.
11 | # For now, that trade-off is not worth it, so let's store avatars as binary data, instead.
12 | avatar = models.BinaryField(blank=True, null=True)
13 | default_sharing_groups = models.ManyToManyField(
14 | Group,
15 | blank=True,
16 | help_text=_('New secrets created by you will be shared with these groups.'),
17 | related_name='+',
18 | )
19 | hide_deleted_secrets = models.BooleanField(
20 | default=True,
21 | help_text=_('Hides deleted secrets per default. Enable them in filters to see them again.')
22 | )
23 | user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
24 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/templates/accounts/_avatar.html:
--------------------------------------------------------------------------------
1 | {% if user.profile.avatar %}
2 |
10 | {% elif user.first_name and user.last_name %}
11 |
18 |
19 | {{ user.first_name.0.capitalize }}{{ user.last_name.0.capitalize }}
20 |
21 |
22 | {% else %}
23 |
24 |
25 |
26 | {% endif %}
27 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/templates/accounts/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load i18n %}
3 | {% load static %}
4 | {% block navbar %}{% endblock %}
5 | {% block title %}{% trans "Login" %}{% endblock %}
6 | {% block super_content %}
7 |
40 | {% endblock %}
41 | {% block footer %}{% endblock %}
42 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/templates/accounts/logout.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load i18n %}
3 | {% load static %}
4 | {% block navbar %}{% endblock %}
5 | {% block title %}{% trans "Logout" %}{% endblock %}
6 |
7 | {% block super_content %}
8 |
9 |
10 |
11 | Team Vault
12 |
13 |
14 | {% trans "Logged out." %}
15 |
16 |
{% trans "Log in again?" %}
17 |
18 |
19 | {% endblock %}
20 | {% block footer %}{% endblock %}
21 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/templates/accounts/user_list.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load i18n %}
3 | {% load smart_pagination %}
4 | {% block title %}{% trans "Users" %}{% endblock %}
5 | {% block content %}
6 |
7 |
8 |
9 | {% trans "Users" %}
10 |
11 |
12 |
13 |
14 |
15 | {% trans "Username" %}
16 | {% trans "Email" %}
17 | {% trans "Active" %}
18 | {% trans "Admin" %}
19 | {% trans "Last login" %}
20 |
21 | {% for user in users %}
22 |
23 |
24 | {{ user.username }}
25 |
26 |
27 | {{ user.email }}
28 |
29 |
30 | {% if user.is_active %}
31 |
32 | {% else %}
33 |
34 | {% endif %}
35 |
36 |
37 | {% if user.is_superuser %}
38 |
39 | {% else %}
40 |
41 | {% endif %}
42 |
43 | {{ user.last_login|date:"Y-m-d H:i:s e" }}
44 |
45 | {% endfor %}
46 |
47 |
48 |
49 | {% include "pagination.html" %}
50 |
51 |
52 | {% endblock %}
53 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/templates/accounts/user_settings.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load django_bootstrap5 %}
3 | {% load i18n %}
4 | {% block title %}{% translate "Settings" %}{% endblock %}
5 | {% block content %}
6 |
7 |
{% translate "Settings" %}
8 |
24 |
25 | {% endblock %}
26 | {% block additionalJS %}
27 |
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.views import LoginView, LogoutView
2 | from django.urls import path
3 |
4 | from . import views
5 |
6 | urlpatterns = (
7 | path(
8 | 'login/',
9 | LoginView.as_view(template_name="accounts/login.html"),
10 | name='accounts.login',
11 | ),
12 | path(
13 | 'logout/',
14 | LogoutView.as_view(template_name="accounts/logout.html"),
15 | name='accounts.logout',
16 | ),
17 | path(
18 | 'users/',
19 | views.users,
20 | name='accounts.user-list',
21 | ),
22 | path(
23 | 'users//',
24 | views.user_detail,
25 | name='accounts.user-detail',
26 | ),
27 | path(
28 | 'users//reactivate',
29 | views.user_activate,
30 | name='accounts.user-reactivate',
31 | ),
32 | path(
33 | 'users//deactivate',
34 | views.user_activate,
35 | {'deactivate': True},
36 | name='accounts.user-deactivate',
37 | ),
38 | path(
39 | 'settings/',
40 | views.user_settings,
41 | name='accounts.user-settings',
42 | ),
43 | )
44 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from base64 import b64encode
3 | from hashlib import md5
4 |
5 | import requests
6 |
7 | from teamvault.apps.accounts.models import UserProfile as UserProfileModel, UserProfile
8 | from teamvault.apps.audit.models import LogEntry
9 | from teamvault.apps.secrets.models import SharedSecretData, Secret, SecretRevision
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | def save_gravatar(user, *_args, **_kwargs):
15 | email_hash = md5(user.email.strip().lower().encode("utf-8")).hexdigest()
16 | resp = requests.get(f'https://gravatar.com/avatar/{email_hash}?s=200&r=g&d=mp')
17 | if resp.ok:
18 | user_settings = UserProfileModel.objects.get_or_create(user=user)[0]
19 | user_settings.avatar = b64encode(resp.content)
20 | user_settings.save()
21 |
22 |
23 | def save_google_avatar(response, user, *_args, **_kwargs):
24 | resp = requests.get(response['picture'])
25 | if resp.ok:
26 | user_settings = UserProfileModel.objects.get_or_create(user=user)[0]
27 | user_settings.avatar = b64encode(resp.content)
28 | user_settings.save()
29 |
30 |
31 | def merge_users(user1, user2, dry_run=True):
32 | logger.info(
33 | f'Merging user {user1.username} into {user2.username}\n'
34 | f'Secrets & Audit Logs will be merged. User Profiles, Social Auth data and User itself will be deleted.\n'
35 | f'Dry run: {dry_run}'
36 | )
37 |
38 | # Secrets / SharedSecretData / SecretRevisions
39 | user1_secrets = SharedSecretData.objects.filter(user=user1)
40 | user2_secrets = SharedSecretData.objects.filter(user=user2)
41 | secrets_to_merge = user1_secrets.exclude(pk__in=user2_secrets.values_list('pk', flat=True))
42 | logger.info(f'{secrets_to_merge.count()} Secrets found: {secrets_to_merge.values_list("pk", flat=True)}')
43 |
44 | user1_created = Secret.objects.filter(created_by=user1)
45 | logger.info(f'{user1_created.count()} Secrets w/ created_by found: {user1_created.values_list("pk", flat=True)}')
46 |
47 | user1_revisions = SecretRevision.objects.filter(set_by=user1)
48 | logger.info(f'{user1_revisions.count()} SecretRevisions found: {user1_revisions.values_list("pk", flat=True)}')
49 |
50 | # Audit Logs
51 | user1_actor_logs = LogEntry.objects.filter(actor=user1)
52 | user1_user_logs = LogEntry.objects.filter(user=user1)
53 | logger.info(f'{user1_actor_logs.count()} Actor Logs found: {user1_actor_logs.values_list("pk", flat=True)}')
54 | logger.info(f'{user1_user_logs.count()} User Logs found: {user1_user_logs.values_list("pk", flat=True)}')
55 |
56 | # User Profiles
57 | user1_profiles = UserProfile.objects.filter(user=user1)
58 | logger.info(f'{user1_profiles.count()} User Profiles found: {user1_profiles.values_list("pk", flat=True)}')
59 |
60 | # User Social Auth data
61 | user1_social_data = user1.social_auth.all().exclude(pk__in=user2.social_auth.all().values_list('pk', flat=True))
62 | logger.info(f'{user1_social_data.count()} Social Auth data found: {user1_social_data.values_list("pk", flat=True)}')
63 |
64 | if not dry_run:
65 | secrets_to_merge.update(user=user2)
66 | user1_created.update(created_by=user2)
67 | user1_revisions.update(set_by=user2)
68 | logger.info('Updated secrets.')
69 |
70 | user1_actor_logs.update(actor=user2)
71 | user1_user_logs.update(user=user2)
72 | logger.info('Updated logs.')
73 |
74 | user1_profiles.delete()
75 | logger.info('Deleted User Profiles.')
76 |
77 | user1_social_data.delete()
78 | logger.info('Deleted Social Auth data.')
79 |
80 | user1.delete()
81 | logger.info('Deleted User')
82 |
--------------------------------------------------------------------------------
/teamvault/apps/accounts/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib import messages
2 | from django.contrib.auth.decorators import user_passes_test, login_required
3 | from django.contrib.auth.models import User
4 | from django.db import transaction
5 | from django.http import HttpResponseRedirect
6 | from django.shortcuts import get_object_or_404
7 | from django.urls import reverse, reverse_lazy
8 | from django.utils.translation import gettext_lazy as _
9 | from django.views.decorators.http import require_http_methods
10 | from django.views.generic import DetailView, ListView, UpdateView
11 |
12 | from .forms import UserProfileForm
13 | from .models import UserProfile as UserProfileModel
14 | from ..audit.auditlog import log
15 | from ..audit.models import AuditLogCategoryChoices
16 | from ..secrets.models import Secret, SecretRevision
17 |
18 |
19 | class UserProfile(UpdateView):
20 | form_class = UserProfileForm
21 | model = UserProfileModel
22 | template_name = "accounts/user_settings.html"
23 | success_url = reverse_lazy('accounts.user-settings')
24 |
25 | def get_object(self, *args, **kwargs):
26 | return UserProfileModel.objects.get_or_create(user=self.request.user)[0]
27 |
28 | def form_valid(self, form):
29 | response = super().form_valid(form)
30 | messages.success(self.request, _('Successfully updated settings.'))
31 | return response
32 |
33 |
34 | user_settings = login_required(UserProfile.as_view())
35 |
36 |
37 | class UserList(ListView):
38 | context_object_name = 'users'
39 | paginate_by = 25
40 | template_name = "accounts/user_list.html"
41 |
42 | def get_queryset(self):
43 | return User.objects.order_by('username')
44 |
45 |
46 | users = user_passes_test(lambda u: u.is_superuser)(UserList.as_view())
47 |
48 |
49 | class UserDetail(DetailView):
50 | context_object_name = 'user'
51 | model = User
52 | slug_field = 'username'
53 | slug_url_kwarg = 'username'
54 | template_name = 'accounts/user_detail.html'
55 |
56 |
57 | user_detail = user_passes_test(lambda u: u.is_superuser)(UserDetail.as_view())
58 |
59 |
60 | @user_passes_test(lambda u: u.is_superuser)
61 | @require_http_methods(["POST"])
62 | def user_activate(request, username, deactivate=False):
63 | user = get_object_or_404(
64 | User,
65 | username=username,
66 | is_active=deactivate,
67 | )
68 | user.is_active = not deactivate
69 | user.save()
70 | if deactivate:
71 | user.groups.clear()
72 | accessed_revs = SecretRevision.objects.filter(
73 | accessed_by=user,
74 | ).exclude(
75 | secret__needs_changing_on_leave=False,
76 | ).exclude(
77 | secret__status=Secret.STATUS_NEEDS_CHANGING,
78 | ).select_related(
79 | 'secret',
80 | )
81 | secrets = set()
82 | for rev in accessed_revs:
83 | if rev.is_current_revision:
84 | secrets.add(rev.secret)
85 | with transaction.atomic():
86 | for secret in secrets:
87 | secret.status = Secret.STATUS_NEEDS_CHANGING
88 | secret.save()
89 | msg = _(
90 | "secret '{secret}' needs changing because user '{user}' was deactivated"
91 | ).format(
92 | secret=secret.name,
93 | user=user.username,
94 | )
95 | log(
96 | msg,
97 | actor=request.user,
98 | category=AuditLogCategoryChoices.USER_DEACTIVATED,
99 | secret=secret,
100 | user=user,
101 | )
102 | log(
103 | _("{actor} deactivated {user}, {secrets} secrets marked for changing").format(
104 | actor=request.user.username,
105 | user=user.username,
106 | secrets=len(secrets),
107 | ),
108 | actor=request.user,
109 | category=AuditLogCategoryChoices.USER_DEACTIVATED,
110 | user=user,
111 | )
112 | else:
113 | log(
114 | _("{actor} reactivated {user}").format(
115 | actor=request.user.username,
116 | user=user.username,
117 | ),
118 | actor=request.user,
119 | category=AuditLogCategoryChoices.USER_ACTIVATED,
120 | user=user,
121 | )
122 | return HttpResponseRedirect(reverse('accounts.user-detail', kwargs={'username': user.username}))
123 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/__init__.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AuditConfig(AppConfig):
5 | name = 'teamvault.apps.audit'
6 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/auditlog.py:
--------------------------------------------------------------------------------
1 | from logging import getLogger
2 |
3 | from .models import LogEntry
4 |
5 | AUDIT_LOG = getLogger(__name__)
6 |
7 |
8 | def log(
9 | msg,
10 | level='info',
11 | category=None,
12 | actor=None,
13 | reason=None,
14 | secret=None,
15 | secret_revision=None,
16 | group=None,
17 | user=None,
18 | ):
19 | getattr(AUDIT_LOG, level)(msg)
20 | entry = LogEntry()
21 | entry.message = msg
22 | entry.category = category
23 | entry.actor = actor
24 | entry.reason = reason
25 | entry.secret = secret
26 | entry.secret_revision = secret_revision
27 | entry.group = group
28 | entry.user = user
29 | entry.save()
30 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/filters.py:
--------------------------------------------------------------------------------
1 | import django_filters
2 | from django import forms
3 | from django.contrib.auth.models import User
4 | from django.utils.translation import gettext_lazy as _
5 |
6 | from .models import LogEntry, AuditLogCategoryChoices
7 | from ..secrets.models import Secret
8 |
9 |
10 | class AuditLogFilter(django_filters.FilterSet):
11 | actor = django_filters.ModelChoiceFilter(
12 | to_field_name='username',
13 | queryset=User.objects.all().order_by('username'),
14 | )
15 | category = django_filters.MultipleChoiceFilter(
16 | choices=AuditLogCategoryChoices.choices,
17 | label=_('Categories'),
18 | widget=forms.CheckboxSelectMultiple(),
19 | )
20 | secret = django_filters.ModelChoiceFilter(
21 | field_name='secret',
22 | to_field_name='hashid',
23 | queryset=Secret.objects.all(),
24 | )
25 | user = django_filters.ModelChoiceFilter(
26 | to_field_name='username',
27 | queryset=User.objects.all().order_by('username'),
28 | )
29 |
30 | class Meta:
31 | model = LogEntry
32 | fields = ['secret', 'actor', 'user']
33 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 | from django.conf import settings
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('auth', '0005_alter_user_last_login_null'),
13 | ('secrets', '0006_auto_20150124_1103'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='LogEntry',
19 | fields=[
20 | ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
21 | ('message', models.TextField()),
22 | ('time', models.DateTimeField(auto_now_add=True)),
23 | ('actor', models.ForeignKey(null=True, on_delete=models.CASCADE, related_name='logged_actions', to=settings.AUTH_USER_MODEL, blank=True)),
24 | ('group', models.ForeignKey(null=True, on_delete=models.CASCADE, related_name='logged_actions', to='auth.Group', blank=True)),
25 | ('secret', models.ForeignKey(null=True, on_delete=models.CASCADE, related_name='logged_actions', to='secrets.Secret', blank=True)),
26 | ('secret_revision', models.ForeignKey(null=True, on_delete=models.CASCADE, related_name='logged_actions', to='secrets.SecretRevision', blank=True)),
27 | ('user', models.ForeignKey(null=True, on_delete=models.CASCADE, related_name='affected_by_actions', to=settings.AUTH_USER_MODEL, blank=True)),
28 | ],
29 | options={
30 | 'ordering': ('-time',),
31 | },
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/migrations/0002_auto_20170313_1544.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.6 on 2017-03-13 15:44
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | dependencies = [
13 | ('audit', '0001_initial'),
14 | ]
15 |
16 | operations = [
17 | migrations.AlterField(
18 | model_name='logentry',
19 | name='actor',
20 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='logged_actions', to=settings.AUTH_USER_MODEL),
21 | ),
22 | migrations.AlterField(
23 | model_name='logentry',
24 | name='group',
25 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='logged_actions', to='auth.Group'),
26 | ),
27 | migrations.AlterField(
28 | model_name='logentry',
29 | name='secret',
30 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='logged_actions', to='secrets.Secret'),
31 | ),
32 | migrations.AlterField(
33 | model_name='logentry',
34 | name='secret_revision',
35 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='logged_actions', to='secrets.SecretRevision'),
36 | ),
37 | migrations.AlterField(
38 | model_name='logentry',
39 | name='user',
40 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='affected_by_actions', to=settings.AUTH_USER_MODEL),
41 | ),
42 | ]
43 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/migrations/0003_logentry_categories.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-07-20 12:41
2 |
3 | from django.db import migrations, models
4 |
5 | category_choices = [
6 | ("secret_read", "secret_read"),
7 | ("secret_elevated_superuser_read", "secret_elevated_superuser_read"),
8 | ("secret_permission_violation", "secret_permission_violation"),
9 | ("secret_changed", "secret_changed"),
10 | ("secret_needs_changing_reminder", "secret_needs_changing_reminder"),
11 | ("secret_legacy_access_requests", "secret_legacy_access_requests"),
12 | ("secret_shared", "secret_shared"),
13 | ("secret_superuser_shared", "secret_superuser_shared"),
14 | ("user_activated", "user_activated"),
15 | ("user_deactivated", "user_deactivated"),
16 | ("user_settings_changed", "user_settings_changed"),
17 | ("miscellaneous", "miscellaneous"),
18 | ]
19 |
20 |
21 | def categorize_log_entries(apps, schema_editor):
22 | log_entry_model = apps.get_model('audit', 'LogEntry')
23 |
24 | mapping = {
25 | r"^.* used superuser privileges to read '.*'$": "secret_superuser_read",
26 | r"^.* read '.*'$": "secret_read",
27 | r"^.* tried to access '.*' without permission$": "secret_permission_violation",
28 | r"^.* shared '.* with .*$": "secret_shared",
29 | r"^.* granted access to \w+ '.*'.*$": "secret_shared",
30 | r"^.* set a new secret for '.*' \([\w\d]+->[\w\d]+\)$": "secret_changed",
31 | r"^.* deleted '.*' \(\d+:\d+\)$": "secret_changed",
32 | r"^.* restore '.*' \(\d+:\d+\)$": "secret_changed",
33 | r"^.* has \w+ access request #\d+ for .*, ?\w* allowing access to '.*'$": "secret_legacy_access_requests",
34 | r"^secret '.*' needs changing because user '.*' was deactivated$": "secret_needs_changing_reminder",
35 | r"^.* reactivated .*": "user_activated$",
36 | r"^.* deactivated .*, \d+ secrets marked for changing": "user_deactivated$",
37 | }
38 |
39 | for regex, category in mapping.items():
40 | log_entry_model.objects.filter(message__regex=regex).update(category=category)
41 |
42 |
43 | class Migration(migrations.Migration):
44 | dependencies = [
45 | ("audit", "0002_auto_20170313_1544"),
46 | ]
47 |
48 | operations = [
49 | migrations.AddField(
50 | model_name="logentry",
51 | name="category",
52 | field=models.CharField(
53 | choices=category_choices,
54 | default="miscellaneous",
55 | max_length=64,
56 | ),
57 | ),
58 | migrations.RunPython(
59 | code=categorize_log_entries,
60 | ),
61 | migrations.AlterField(
62 | model_name="logentry",
63 | name="category",
64 | field=models.CharField(
65 | choices=category_choices,
66 | max_length=64,
67 | ),
68 | ),
69 | ]
70 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/migrations/0004_alter_logentry_category.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-07-25 17:46
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("audit", "0003_logentry_categories"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="logentry",
14 | name="category",
15 | field=models.CharField(
16 | choices=[
17 | ("secret_read", "secret_read"),
18 | (
19 | "secret_elevated_superuser_read",
20 | "secret_elevated_superuser_read",
21 | ),
22 | ("secret_permission_violation", "secret_permission_violation"),
23 | ("secret_changed", "secret_changed"),
24 | (
25 | "secret_needs_changing_reminder",
26 | "secret_needs_changing_reminder",
27 | ),
28 | ("secret_shared", "secret_shared"),
29 | ("secret_superuser_shared", "secret_superuser_shared"),
30 | ("secret_legacy_access_requests", "secret_legacy_access_requests"),
31 | ("user_activated", "user_activated"),
32 | ("user_deactivated", "user_deactivated"),
33 | ("user_settings_changed", "user_settings_changed"),
34 | ("miscellaneous", "miscellaneous"),
35 | ],
36 | default="miscellaneous",
37 | max_length=64,
38 | ),
39 | ),
40 | ]
41 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/migrations/0005_alter_logentry_category.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.5 on 2024-02-13 15:00
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | def categorize_log_entries_of_shares(apps, schema_editor):
7 | log_entry_model = apps.get_model('audit', 'LogEntry')
8 | log_entry_model.objects.filter(
9 | category='secret_shared',
10 | message__regex=r"^.* removed access of \w+ '.*'.*$",
11 | ).update(
12 | category='secret_share_removed'
13 | )
14 |
15 |
16 | def categorize_log_entries_of_shares_reverse(apps, schema_editor):
17 | log_entry_model = apps.get_model('audit', 'LogEntry')
18 | log_entry_model.objects.filter(
19 | category='secret_share_removed'
20 | ).update(
21 | category='secret_shared'
22 | )
23 |
24 |
25 | class Migration(migrations.Migration):
26 | dependencies = [
27 | ("audit", "0004_alter_logentry_category"),
28 | ]
29 |
30 | operations = [
31 | migrations.AlterField(
32 | model_name="logentry",
33 | name="category",
34 | field=models.CharField(
35 | choices=[
36 | ("secret_read", "secret_read"),
37 | ("secret_elevated_superuser_read", "secret_elevated_superuser_read"),
38 | ("secret_permission_violation", "secret_permission_violation"),
39 | ("secret_changed", "secret_changed"),
40 | ("secret_needs_changing_reminder", "secret_needs_changing_reminder"),
41 | ("secret_shared", "secret_shared"),
42 | ("secret_superuser_shared", "secret_superuser_shared"),
43 | ("secret_share_removed", "secret_share_removed"),
44 | ("secret_superuser_share_removed", "secret_superuser_share_removed"),
45 | ("secret_legacy_access_requests", "secret_legacy_access_requests"),
46 | ("user_activated", "user_activated"),
47 | ("user_deactivated", "user_deactivated"),
48 | ("user_settings_changed", "user_settings_changed"),
49 | ("miscellaneous", "miscellaneous"),
50 | ],
51 | default="miscellaneous",
52 | max_length=64,
53 | ),
54 | ),
55 | migrations.RunPython(
56 | code=categorize_log_entries_of_shares,
57 | reverse_code=categorize_log_entries_of_shares_reverse,
58 | ),
59 | ]
60 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/migrations/0006_logentry_reason.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.5 on 2024-02-13 15:40
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | def add_current_share_reasons_to_audit_log(apps, schema_editor):
7 | log_entry_model = apps.get_model('audit', 'LogEntry')
8 | shared_secret_data_model = apps.get_model('secrets', 'SharedSecretData')
9 | shares = shared_secret_data_model.objects.all().exclude(granted_by__isnull=True)
10 | shared_log_entries = log_entry_model.objects.filter(
11 | category__in=['secret_shared', 'secret_superuser_shared']
12 | ).only('secret__id', 'actor__id', 'reason')
13 |
14 | for share in shares:
15 | matched_entry = shared_log_entries.filter(
16 | actor__id=share.granted_by.id,
17 | secret__id=share.secret.id,
18 | ).order_by('time').last()
19 |
20 | # Matching can fail for immediate shares while creating new secrets
21 | if matched_entry:
22 | matched_entry.reason = share.grant_description
23 | matched_entry.save(update_fields=['reason'])
24 |
25 |
26 | class Migration(migrations.Migration):
27 | dependencies = [
28 | ("audit", "0005_alter_logentry_category"),
29 | ("secrets", "0032_alter_sharedsecretdata_granted_by")
30 | ]
31 |
32 | operations = [
33 | migrations.AddField(
34 | model_name="logentry",
35 | name="reason",
36 | field=models.TextField(blank=True, null=True),
37 | ),
38 | migrations.RunPython(
39 | code=add_current_share_reasons_to_audit_log,
40 | reverse_code=migrations.RunPython.noop,
41 | ),
42 | ]
43 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/migrations/0007_alter_logentry_category.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2024-03-05 14:15
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("audit", "0006_logentry_reason"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="logentry",
14 | name="category",
15 | field=models.CharField(
16 | choices=[
17 | ("secret_read", "secret_read"),
18 | ("secret_elevated_superuser_read", "secret_elevated_superuser_read"),
19 | ("secret_permission_violation", "secret_permission_violation"),
20 | ("secret_changed", "secret_changed"),
21 | ("secret_needs_changing_reminder", "secret_needs_changing_reminder"),
22 | ("secret_shared", "secret_shared"),
23 | ("secret_superuser_shared", "secret_superuser_shared"),
24 | ("secret_share_removed", "secret_share_removed"),
25 | ("secret_superuser_share_removed", "secret_superuser_share_removed"),
26 | ("secret_legacy_access_requests", "secret_legacy_access_requests"),
27 | ("user_activated", "user_activated"),
28 | ("user_deactivated", "user_deactivated"),
29 | ("user_settings_changed", "user_settings_changed"),
30 | ("share_automatically_revoked", "share_automatically_revoked"),
31 | ("miscellaneous", "miscellaneous"),
32 | ],
33 | default="miscellaneous",
34 | max_length=64,
35 | ),
36 | ),
37 | ]
38 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/migrations/0008_logentry_logentry_category_idx_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.16 on 2024-11-14 13:38
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("audit", "0007_alter_logentry_category"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddIndex(
14 | model_name="logentry",
15 | index=models.Index(fields=["category"], name="logentry_category_idx"),
16 | ),
17 | migrations.AddIndex(
18 | model_name="logentry",
19 | index=models.Index(fields=["time"], name="logentry_time_idx"),
20 | ),
21 | migrations.AddIndex(
22 | model_name="logentry",
23 | index=models.Index(fields=["category", "time"], name="logentry_category_time_idx"),
24 | ),
25 | migrations.AddIndex(
26 | model_name="logentry",
27 | index=models.Index(fields=["actor"], name="logentry_actor_idx"),
28 | ),
29 | migrations.AddIndex(
30 | model_name="logentry",
31 | index=models.Index(fields=["group"], name="logentry_group_idx"),
32 | ),
33 | migrations.AddIndex(
34 | model_name="logentry",
35 | index=models.Index(fields=["secret"], name="logentry_secret_idx"),
36 | ),
37 | migrations.AddIndex(
38 | model_name="logentry",
39 | index=models.Index(fields=["user"], name="logentry_user_idx"),
40 | ),
41 | ]
42 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/teamvault/apps/audit/migrations/__init__.py
--------------------------------------------------------------------------------
/teamvault/apps/audit/models.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.db import models
3 | from django.db.models import TextChoices
4 | from django.utils.translation import gettext_lazy as _
5 |
6 |
7 | class AuditLogCategoryChoices(TextChoices):
8 | SECRET_READ = 'secret_read', _('secret_read')
9 | SECRET_ELEVATED_SUPERUSER_READ = 'secret_elevated_superuser_read', _('secret_elevated_superuser_read')
10 | SECRET_PERMISSION_VIOLATION = 'secret_permission_violation', _('secret_permission_violation')
11 | SECRET_CHANGED = 'secret_changed', _('secret_changed')
12 | SECRET_NEEDS_CHANGING_REMINDER = 'secret_needs_changing_reminder', _('secret_needs_changing_reminder')
13 | SECRET_SHARED = 'secret_shared', _('secret_shared')
14 | SECRET_SUPERUSER_SHARED = 'secret_superuser_shared', _('secret_superuser_shared')
15 | SECRET_SHARE_REMOVED = 'secret_share_removed', _('secret_share_removed')
16 | SECRET_SUPERUSER_SHARE_REMOVED = 'secret_superuser_share_removed', _('secret_superuser_share_removed')
17 | SECRET_ACCESS_REQUEST = 'secret_legacy_access_requests', _('secret_legacy_access_requests')
18 |
19 | USER_ACTIVATED = 'user_activated', _('user_activated')
20 | USER_DEACTIVATED = 'user_deactivated', _('user_deactivated')
21 | USER_SETTINGS_CHANGED = 'user_settings_changed', _('user_settings_changed')
22 |
23 | SHARE_AUTOMATICALLY_REVOKED = 'share_automatically_revoked', _('share_automatically_revoked')
24 |
25 | MISCELLANEOUS = 'miscellaneous', _('miscellaneous')
26 |
27 |
28 | class LogEntry(models.Model):
29 | actor = models.ForeignKey(
30 | settings.AUTH_USER_MODEL,
31 | models.PROTECT,
32 | blank=True,
33 | null=True,
34 | related_name='logged_actions',
35 | )
36 | category = models.CharField(
37 | choices=AuditLogCategoryChoices.choices,
38 | default=AuditLogCategoryChoices.MISCELLANEOUS,
39 | max_length=64,
40 | )
41 | group = models.ForeignKey(
42 | 'auth.Group',
43 | models.PROTECT,
44 | blank=True,
45 | null=True,
46 | related_name='logged_actions',
47 | )
48 | message = models.TextField()
49 | reason = models.TextField(
50 | blank=True,
51 | null=True,
52 | )
53 | secret = models.ForeignKey(
54 | 'secrets.Secret',
55 | models.PROTECT,
56 | blank=True,
57 | null=True,
58 | related_name='logged_actions',
59 | )
60 | secret_revision = models.ForeignKey(
61 | 'secrets.SecretRevision',
62 | models.PROTECT,
63 | blank=True,
64 | null=True,
65 | related_name='logged_actions',
66 | )
67 | time = models.DateTimeField(
68 | auto_now_add=True,
69 | )
70 | user = models.ForeignKey(
71 | settings.AUTH_USER_MODEL,
72 | models.PROTECT,
73 | blank=True,
74 | null=True,
75 | related_name='affected_by_actions',
76 | )
77 |
78 | class Meta:
79 | indexes = [
80 | models.Index(fields=['category'], name='logentry_category_idx'),
81 | models.Index(fields=['time'], name='logentry_time_idx'),
82 | models.Index(fields=['category', 'time'], name='logentry_category_time_idx'),
83 |
84 | models.Index(fields=['actor'], name='logentry_actor_idx'),
85 | models.Index(fields=['group'], name='logentry_group_idx'),
86 | models.Index(fields=['secret'], name='logentry_secret_idx'),
87 | models.Index(fields=['user'], name='logentry_user_idx'),
88 | ]
89 | ordering = ('-time',)
90 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/templates/audit/log.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 | {% load static %}
5 | {% load smart_pagination %}
6 | {% block title %}{% trans "Audit log" %}{% endblock %}
7 | {% block content %}
8 |
9 |
10 |
11 |
12 | {% translate "Audit log" %}
13 |
14 |
15 |
16 | {% include 'helpers/filter.html' %}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% translate "Time" %}
26 | {% translate "Actor" %}
27 | {% translate "User" %}
28 | {% translate "Secret" %}
29 | {% translate "Message" %}
30 | {% translate "Category" %}
31 |
32 |
33 |
34 | {% for entry in log_entries %}
35 |
36 | {{ entry.time|date:"Y-m-d H:i:s e" }}
37 | {% if entry.actor %}{{ entry.actor.username }}{% endif %}
38 | {{ entry.user|default_if_none:'' }}
39 | {% if entry.secret %}
40 | {{ entry.secret.name }} {% endif %}
41 |
42 |
43 | {{ entry.message }}
44 | {% if entry.reason %}
45 | {% translate "Reason" %}: {{ entry.reason }}
46 | {% endif %}
47 |
48 | {{ entry.category }}
49 |
50 | {% endfor %}
51 |
52 |
53 |
54 |
55 |
56 |
57 | {% include "pagination.html" %}
58 |
59 |
60 | {% endblock %}
61 |
62 | {% block additionalJS %}
63 |
103 | {% endblock %}
104 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 | urlpatterns = (
6 | path(
7 | 'log/',
8 | views.auditlog,
9 | name='audit.log',
10 | ),
11 | )
12 |
--------------------------------------------------------------------------------
/teamvault/apps/audit/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.decorators import user_passes_test
2 | from django.db.models import Q
3 | from django.views.generic import ListView
4 |
5 | from .filters import AuditLogFilter
6 | from .models import LogEntry
7 | from ..secrets.models import Secret
8 | from ...views import FilterMixin
9 |
10 |
11 | class LogEntryList(ListView, FilterMixin):
12 | filter = None
13 | filter_class = AuditLogFilter
14 | context_object_name = 'log_entries'
15 | paginate_by = 25
16 | template_name = "audit/log.html"
17 |
18 | def get_queryset(self):
19 | queryset = LogEntry.objects.all()
20 | if "search" in self.request.GET:
21 | query = self.request.GET['search']
22 | queryset = queryset.filter(
23 | Q(actor__icontains=query) |
24 | Q(message__icontains=query)
25 | )
26 |
27 | return self.get_filtered_queryset(queryset)
28 |
29 | @staticmethod
30 | def manipulate_filter_form(bound_data, filter_form):
31 | # Set queryset since we'll retrieve choices via ajax and need to show the initial one
32 | if bound_data.get('secret'):
33 | secret_choices = Secret.objects.filter(pk=bound_data['secret'].pk)
34 | else:
35 | secret_choices = Secret.objects.none()
36 | filter_form.fields['secret'].queryset = secret_choices
37 | return filter_form
38 |
39 |
40 | auditlog = user_passes_test(lambda u: u.is_superuser)(LogEntryList.as_view())
41 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/__init__.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SecretsConfig(AppConfig):
5 | name = 'teamvault.apps.secrets'
6 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/teamvault/apps/secrets/api/__init__.py
--------------------------------------------------------------------------------
/teamvault/apps/secrets/api/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from .views import SecretDetail, SecretList, SecretRevisionDetail, SecretShare, SecretShareDetail, data_get, \
4 | generate_password_view, otp_get
5 |
6 | urlpatterns = (
7 | path(
8 | 'secrets/',
9 | SecretList.as_view(),
10 | name='api.secret_list',
11 | ),
12 | path(
13 | 'secrets//',
14 | SecretDetail.as_view(),
15 | name='api.secret_detail',
16 | ),
17 | path(
18 | 'secrets//shares/',
19 | SecretShare.as_view(),
20 | name='api.secret_share',
21 | ),
22 | path(
23 | 'secrets//shares/',
24 | SecretShareDetail.as_view(),
25 | name='api.secret_share_detail',
26 | ),
27 | path(
28 | 'secret-revisions//',
29 | SecretRevisionDetail.as_view(),
30 | name='api.secret-revision_detail',
31 | ),
32 | path(
33 | 'secret-revisions//data',
34 | data_get,
35 | name='api.secret-revision_data',
36 | ),
37 | path(
38 | 'secret-revisions//data/otp',
39 | otp_get,
40 | name='api.secret-revision_otp',
41 | ),
42 | path(
43 | 'generate_password/',
44 | generate_password_view,
45 | name='api.generate-password',
46 | )
47 | )
48 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/context_processors.py:
--------------------------------------------------------------------------------
1 | from teamvault.__version__ import __version__
2 |
3 |
4 | def version(request):
5 | return {'version': __version__}
6 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/exceptions.py:
--------------------------------------------------------------------------------
1 | class PermissionError(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/filters.py:
--------------------------------------------------------------------------------
1 | import enum
2 |
3 | import django_filters
4 | from django.contrib.auth import get_user_model
5 | from django import forms
6 | from django.db.models import IntegerChoices
7 | from django.utils.html import format_html
8 | from django.utils.safestring import mark_safe
9 | from django.utils.translation import gettext_lazy as _
10 |
11 | from teamvault.apps.secrets.models import Secret
12 |
13 | User = get_user_model()
14 |
15 |
16 | def add_tooltip(label, tooltip_message):
17 | return format_html(
18 | '{} '
19 | ''
21 | ' ',
22 | label,
23 | tooltip_message
24 | )
25 |
26 |
27 | class Icons(enum.Enum):
28 | CREDIT_CARD = "fa-credit-card text-secondary"
29 | DELETED_DANGER = "fa-trash text-danger"
30 | FILE = "fa-file text-secondary"
31 | KEY = "fa-key text-secondary"
32 | REFRESH_DANGER = "fa-refresh text-danger"
33 |
34 | @property
35 | def html(self):
36 | return f' '
37 |
38 |
39 | class ContentTypeChoice(IntegerChoices):
40 | # TODO: Merge CONTENT_* vars with these ones.
41 | # Preferably migrate occurances of Secret.CONTENT_CHOICES to this class
42 | PASSWORD = Secret.CONTENT_PASSWORD, mark_safe(Icons.KEY.html + _('Password'))
43 | CREDIT_CARD = Secret.CONTENT_CC, mark_safe(Icons.CREDIT_CARD.html + _('Credit Card'))
44 | FILE = Secret.CONTENT_FILE, mark_safe(Icons.FILE.html + _('File'))
45 |
46 |
47 | class StatusChoices(IntegerChoices):
48 | # TODO: Merge STATUS_* vars with these ones.
49 | # Preferably migrate occurances of Secret.STATUS_CHOICES to this class
50 | OK = Secret.STATUS_OK, mark_safe(Icons.KEY.html + _('Regular'))
51 | NEEDS_CHANGING = Secret.STATUS_NEEDS_CHANGING, mark_safe(Icons.REFRESH_DANGER.html + _('Needs Changing'))
52 | DELETED = Secret.STATUS_DELETED, mark_safe(Icons.DELETED_DANGER.html) + f"{add_tooltip( _('Deleted'),_('Hide deleted secrets per default by changing your settings.'))}"
53 |
54 |
55 | class SecretFilter(django_filters.FilterSet):
56 | content_type = django_filters.MultipleChoiceFilter(
57 | choices=ContentTypeChoice,
58 | widget=forms.CheckboxSelectMultiple,
59 | label=_('Type')
60 | )
61 | status = django_filters.MultipleChoiceFilter(
62 | choices=StatusChoices,
63 | widget=forms.CheckboxSelectMultiple,
64 | label=_('Status')
65 | )
66 | created_by = django_filters.ModelChoiceFilter(
67 | queryset=User.objects.all().order_by('username'),
68 | )
69 |
70 | class Meta:
71 | model = Secret
72 | fields = ['content_type', 'status', 'created_by']
73 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/teamvault/apps/secrets/management/__init__.py
--------------------------------------------------------------------------------
/teamvault/apps/secrets/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/teamvault/apps/secrets/management/commands/__init__.py
--------------------------------------------------------------------------------
/teamvault/apps/secrets/management/commands/create_fake_data.py:
--------------------------------------------------------------------------------
1 | import json
2 | import random
3 | from datetime import timedelta
4 | from hashlib import sha256
5 |
6 | from cryptography.fernet import Fernet
7 | from django.conf import settings
8 | from django.core.management.base import BaseCommand
9 | from django.utils import timezone
10 |
11 | from django.contrib.auth.models import User, Group
12 | from teamvault.apps.secrets.models import Secret, SecretRevision, SharedSecretData
13 |
14 |
15 | class Command(BaseCommand):
16 | help = 'Create fake users and secrets'
17 |
18 | def handle(self, *args, **kwargs):
19 | from faker import Faker
20 |
21 | fake = Faker()
22 | if not hasattr(settings, 'TEAMVAULT_SECRET_KEY'):
23 | self.stderr.write(self.style.ERROR('TEAMVAULT_SECRET_KEY is not set in settings.'))
24 | return
25 |
26 | try:
27 | fernet = Fernet(settings.TEAMVAULT_SECRET_KEY)
28 | except Exception as e:
29 | self.stderr.write(self.style.ERROR(f'Invalid TEAMVAULT_SECRET_KEY: {e}'))
30 | return
31 |
32 | groups = []
33 | for _ in range(5):
34 | group_name = fake.unique.word().capitalize()
35 | group, created = Group.objects.get_or_create(name=group_name)
36 | groups.append(group)
37 | self.stdout.write(self.style.SUCCESS(f'Created {len(groups)} groups.'))
38 |
39 | # Create 50 fake users
40 | users = []
41 | for _ in range(50):
42 | try:
43 | username = fake.unique.user_name()
44 | email = fake.unique.email()
45 | password = fake.password(length=12)
46 | user = User.objects.create_user(username=username, email=email, password=password)
47 | # Optionally, assign user to random groups
48 | if groups:
49 | user_groups = random.sample(groups, k=random.randint(0, len(groups)))
50 | user.groups.set(user_groups)
51 | user.save()
52 | users.append(user)
53 | except Exception as e:
54 | self.stderr.write(self.style.ERROR(f'Error creating user: {e}'))
55 | self.stdout.write(self.style.SUCCESS('Created 50 fake users.'))
56 |
57 | # Create secrets for each user
58 | for user in users:
59 | for _ in range(10):
60 | try:
61 | secret_name = fake.unique.word().capitalize()
62 | description = fake.sentence(nb_words=10)
63 | url = fake.url()
64 | username_field = fake.user_name()
65 | # Create the Secret object
66 | secret = Secret.objects.create(
67 | name=secret_name,
68 | description=description,
69 | url=url,
70 | username=username_field,
71 | content_type=Secret.CONTENT_PASSWORD,
72 | created_by=user,
73 | status=Secret.STATUS_OK,
74 | )
75 | # Optionally, share the secret with random groups/users
76 | if groups and random.choice([True, False]):
77 | group = random.choice(groups)
78 | SharedSecretData.objects.create(
79 | group=group,
80 | secret=secret,
81 | grant_description='Shared via fake data script',
82 | granted_by=user,
83 | granted_until=timezone.now() + timedelta(days=30)
84 | )
85 | # Create a SecretRevision
86 | plaintext_password = fake.password(length=12)
87 | plaintext_data = {
88 | 'password': plaintext_password
89 | }
90 | plaintext_data_json = json.dumps(plaintext_data)
91 | plaintext_data_sha256 = sha256(plaintext_data_json.encode('utf-8')).hexdigest()
92 | encrypted_data = fernet.encrypt(plaintext_data_json.encode('utf-8'))
93 | secret_revision = SecretRevision.objects.create(
94 | secret=secret,
95 | set_by=user,
96 | encrypted_data=encrypted_data,
97 | length=len(plaintext_password),
98 | plaintext_data_sha256=plaintext_data_sha256,
99 | )
100 | # Assign the current_revision
101 | secret.current_revision = secret_revision
102 | secret.last_changed = timezone.now()
103 | secret.last_read = timezone.now()
104 | secret.save()
105 | except Exception as e:
106 | self.stderr.write(self.style.ERROR(f'Error creating secret for user {user.username}: {e}'))
107 | self.stdout.write(self.style.SUCCESS('Created secrets and secret revisions for all users.'))
108 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/management/commands/update_search_index.py:
--------------------------------------------------------------------------------
1 | from django.contrib.postgres.search import SearchVector
2 | from django.core.management.base import BaseCommand
3 |
4 | from ...models import Secret
5 |
6 |
7 | class Command(BaseCommand):
8 | help = 'Update search index'
9 |
10 | def handle(self, *args, **options):
11 | secrets_total = Secret.objects.count()
12 | Secret.objects.all().update(
13 | search_index=(
14 | SearchVector('name', weight='A') +
15 | SearchVector('description', weight='B') +
16 | SearchVector('username', weight='C') +
17 | SearchVector('filename', weight='D')
18 | )
19 | )
20 | self.stdout.write(self.style.SUCCESS(
21 | "Finished updating search index for {} objects.".format(secrets_total)
22 | ))
23 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 | from django.conf import settings
6 | import django.utils.timezone
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13 | ('auth', '0001_initial'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='AccessRequest',
19 | fields=[
20 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
21 | ('closed', models.DateTimeField(null=True, blank=True)),
22 | ('created', models.DateTimeField(auto_now_add=True)),
23 | ('reason_request', models.TextField(null=True, blank=True)),
24 | ('reason_rejected', models.TextField(null=True, blank=True)),
25 | ('status', models.PositiveSmallIntegerField(default=1, choices=[(1, 'pending'), (2, 'rejected'), (3, 'approved')])),
26 | ('closed_by', models.ForeignKey(null=True, blank=True, on_delete=models.CASCADE, related_name='access_requests_closed', to=settings.AUTH_USER_MODEL)),
27 | ('requester', models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL, related_name='access_requests_created')),
28 | ('reviewers', models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='access_requests_reviewed')),
29 | ],
30 | options={
31 | 'ordering': ('-created',),
32 | },
33 | bases=(models.Model,),
34 | ),
35 | migrations.CreateModel(
36 | name='Secret',
37 | fields=[
38 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
39 | ('access_policy', models.PositiveSmallIntegerField(default=1, choices=[(1, 'request'), (2, 'everyone'), (3, 'hidden')])),
40 | ('content_type', models.PositiveSmallIntegerField(default=1, choices=[(1, 'Password'), (2, 'Credit Card'), (3, 'File')])),
41 | ('created', models.DateTimeField(auto_now_add=True)),
42 | ('description', models.TextField(null=True, blank=True)),
43 | ('last_read', models.DateTimeField(default=django.utils.timezone.now)),
44 | ('name', models.CharField(max_length=92)),
45 | ('needs_changing_on_leave', models.BooleanField(default=True)),
46 | ('status', models.PositiveSmallIntegerField(default=1, choices=[(1, 'OK'), (2, 'needs changing'), (3, 'deleted')])),
47 | ('url', models.URLField(null=True, blank=True)),
48 | ('username', models.CharField(null=True, max_length=255, blank=True)),
49 | ('allowed_groups', models.ManyToManyField(to='auth.Group', blank=True, related_name='allowed_passwords')),
50 | ('allowed_users', models.ManyToManyField(to=settings.AUTH_USER_MODEL, blank=True, related_name='allowed_passwords')),
51 | ('created_by', models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL, related_name='passwords_created')),
52 | ],
53 | options={
54 | 'ordering': ('name',),
55 | },
56 | bases=(models.Model,),
57 | ),
58 | migrations.CreateModel(
59 | name='SecretRevision',
60 | fields=[
61 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
62 | ('created', models.DateTimeField(auto_now_add=True)),
63 | ('encrypted_data', models.BinaryField()),
64 | ('length', models.PositiveIntegerField(default=0)),
65 | ('accessed_by', models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
66 | ('secret', models.ForeignKey(on_delete=models.CASCADE, to='secrets.Secret')),
67 | ('set_by', models.ForeignKey(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL, related_name='password_revisions_set')),
68 | ],
69 | options={
70 | 'ordering': ('-created',),
71 | },
72 | bases=(models.Model,),
73 | ),
74 | migrations.AlterUniqueTogether(
75 | name='secretrevision',
76 | unique_together=set([('encrypted_data', 'secret')]),
77 | ),
78 | migrations.AddField(
79 | model_name='secret',
80 | name='current_revision',
81 | field=models.ForeignKey(null=True, blank=True, on_delete=models.CASCADE, related_name='_password_current_revision', to='secrets.SecretRevision'),
82 | preserve_default=True,
83 | ),
84 | migrations.AddField(
85 | model_name='accessrequest',
86 | name='secret',
87 | field=models.ForeignKey(on_delete=models.CASCADE, to='secrets.Secret', related_name='access_requests'),
88 | preserve_default=True,
89 | ),
90 | ]
91 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0002_secret_filename.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('secrets', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='secret',
16 | name='filename',
17 | field=models.CharField(blank=True, max_length=255, null=True),
18 | preserve_default=True,
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0003_auto_20150113_1915.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('secrets', '0002_secret_filename'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='secretrevision',
16 | name='encrypted_data_sha256',
17 | field=models.CharField(default="0000000000000000000000000000000000000000000000000000000000000000", max_length=64),
18 | preserve_default=False,
19 | ),
20 | migrations.AlterUniqueTogether(
21 | name='secretrevision',
22 | unique_together=set([('encrypted_data_sha256', 'secret')]),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0004_unaccent_extension.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.contrib.postgres.operations import UnaccentExtension
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ('secrets', '0003_auto_20150113_1915'),
11 | ]
12 | operations = [
13 | UnaccentExtension(),
14 | ]
15 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0005_secret_search_index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('secrets', '0004_unaccent_extension'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='secret',
16 | name='search_index',
17 | field=models.CharField(default="X", max_length=1),
18 | preserve_default=False,
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0006_auto_20150124_1103.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('secrets', '0005_secret_search_index'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='secret',
16 | options={'ordering': ('name', 'username')},
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0007_auto_20150205_1918.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 | import teamvault.apps.secrets.models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('secrets', '0006_auto_20150124_1103'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='secret',
17 | name='url',
18 | field=models.CharField(blank=True, null=True, validators=[teamvault.apps.secrets.models.validate_url], max_length=255),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0008_auto_20150322_0944.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('secrets', '0007_auto_20150205_1918'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='accessrequest',
16 | name='hashid',
17 | field=models.CharField(unique=True, max_length=24, null=True),
18 | ),
19 | migrations.AddField(
20 | model_name='secret',
21 | name='hashid',
22 | field=models.CharField(unique=True, max_length=24, null=True),
23 | ),
24 | migrations.AddField(
25 | model_name='secretrevision',
26 | name='hashid',
27 | field=models.CharField(unique=True, max_length=24, null=True),
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0009_auto_20150322_0949.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.conf import settings
5 | from django.db import migrations
6 | from hashids import Hashids
7 |
8 |
9 | def generate_hashids(apps, schema_editor):
10 | AccessRequest = apps.get_model("secrets", "AccessRequest")
11 | Secret = apps.get_model("secrets", "Secret")
12 | SecretRevision = apps.get_model("secrets", "SecretRevision")
13 |
14 | for model_class, hashid_namespace in (
15 | (AccessRequest, "AccessRequest"),
16 | (Secret, "Secret"),
17 | (SecretRevision, "SecretRevision"),
18 | ):
19 | for obj in model_class.objects.all():
20 | if not obj.hashid:
21 | # We cannot use the same salt for every model because
22 | # 1. sequentially create lots of secrets
23 | # 2. note the hashid of each secrets
24 | # 3. you can now enumerate access requests by using the same
25 | # hashids
26 | # it's not a huge deal, but let's avoid it anyway
27 | hasher = Hashids(
28 | min_length=settings.HASHID_MIN_LENGTH,
29 | salt=hashid_namespace + settings.HASHID_SALT,
30 | )
31 | obj.hashid = hasher.encode(obj.pk)
32 | obj.save()
33 |
34 |
35 | class Migration(migrations.Migration):
36 |
37 | dependencies = [
38 | ('secrets', '0008_auto_20150322_0944'),
39 | ]
40 |
41 | operations = [
42 | migrations.RunPython(generate_hashids),
43 | ]
44 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0011_remove_secret_search_index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.1 on 2016-09-30 10:41
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('secrets', '0009_auto_20150322_0949'),
12 | ]
13 |
14 | operations = [
15 | migrations.RemoveField(
16 | model_name='secret',
17 | name='search_index',
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0012_secret_search_index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.1 on 2016-09-30 10:51
3 | from __future__ import unicode_literals
4 |
5 | import django.contrib.postgres.search
6 | from django.db import migrations
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('secrets', '0011_remove_secret_search_index'),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name='secret',
18 | name='search_index',
19 | field=django.contrib.postgres.search.SearchVectorField(blank=True, null=True),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0013_auto_20161021_1411.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.2 on 2016-10-21 14:11
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13 | ('auth', '0008_alter_user_username_max_length'),
14 | ('secrets', '0012_secret_search_index'),
15 | ]
16 |
17 | operations = [
18 | migrations.AddField(
19 | model_name='secret',
20 | name='owner_groups',
21 | field=models.ManyToManyField(blank=True, related_name='owned_passwords', to='auth.Group'),
22 | ),
23 | migrations.AddField(
24 | model_name='secret',
25 | name='owner_users',
26 | field=models.ManyToManyField(blank=True, related_name='owned_passwords', to=settings.AUTH_USER_MODEL),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0014_auto_20170313_1544.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.6 on 2017-03-13 15:44
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | dependencies = [
13 | ('secrets', '0013_auto_20161021_1411'),
14 | ]
15 |
16 | operations = [
17 | migrations.AlterField(
18 | model_name='accessrequest',
19 | name='closed_by',
20 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='access_requests_closed', to=settings.AUTH_USER_MODEL),
21 | ),
22 | migrations.AlterField(
23 | model_name='accessrequest',
24 | name='requester',
25 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='access_requests_created', to=settings.AUTH_USER_MODEL),
26 | ),
27 | migrations.AlterField(
28 | model_name='accessrequest',
29 | name='secret',
30 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='access_requests', to='secrets.Secret'),
31 | ),
32 | migrations.AlterField(
33 | model_name='secret',
34 | name='created_by',
35 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='passwords_created', to=settings.AUTH_USER_MODEL),
36 | ),
37 | migrations.AlterField(
38 | model_name='secret',
39 | name='current_revision',
40 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='_password_current_revision', to='secrets.SecretRevision'),
41 | ),
42 | migrations.AlterField(
43 | model_name='secretrevision',
44 | name='secret',
45 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='secrets.Secret'),
46 | ),
47 | migrations.AlterField(
48 | model_name='secretrevision',
49 | name='set_by',
50 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='password_revisions_set', to=settings.AUTH_USER_MODEL),
51 | ),
52 | ]
53 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0015_secretrevision_plaintext_data_sha256.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-02-20 10:52
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('secrets', '0014_auto_20170313_1544'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='secretrevision',
15 | name='plaintext_data_sha256',
16 | field=models.CharField(default='', max_length=64),
17 | preserve_default=False,
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0016_auto_20180220_1053.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-02-20 10:53
2 | from hashlib import sha256
3 |
4 | from cryptography.fernet import Fernet
5 | from django.conf import settings
6 | from django.db import migrations
7 |
8 |
9 | def backfill_plaintext_hashes(apps, schema_editor):
10 | SecretRevision = apps.get_model('secrets', 'SecretRevision')
11 | f = Fernet(settings.TEAMVAULT_SECRET_KEY)
12 | for srev in SecretRevision.objects.all():
13 | encrypted_data = srev.encrypted_data
14 | if isinstance(encrypted_data, memoryview): # backwards compatibility with psycopg2
15 | encrypted_data = encrypted_data.tobytes()
16 | plaintext_data = f.decrypt(encrypted_data)
17 | srev.plaintext_data_sha256 = sha256(plaintext_data).hexdigest()
18 | srev.save()
19 |
20 |
21 | class Migration(migrations.Migration):
22 |
23 | dependencies = [
24 | ('secrets', '0015_secretrevision_plaintext_data_sha256'),
25 | ]
26 |
27 | operations = [
28 | migrations.RunPython(backfill_plaintext_hashes),
29 | ]
30 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0017_auto_20180220_1115.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-02-20 11:15
2 |
3 | from django.db import migrations
4 |
5 |
6 | def remove_duplicate_srevs(apps, schema_editor):
7 | LogEntry = apps.get_model('audit', 'LogEntry')
8 | Secret = apps.get_model('secrets', 'Secret')
9 | SecretRevision = apps.get_model('secrets', 'SecretRevision')
10 | for secret in Secret.objects.all():
11 | revisions = SecretRevision.objects.filter(secret=secret).order_by('-id')
12 | hashes_to_revisions = {}
13 | for revision in revisions:
14 | if revision.plaintext_data_sha256 in hashes_to_revisions:
15 | correct_revision = hashes_to_revisions[revision.plaintext_data_sha256]
16 | assert revision != revision.secret.current_revision
17 | for log_entry in LogEntry.objects.filter(secret_revision=revision):
18 | log_entry.secret_revision = correct_revision
19 | log_entry.save()
20 | correct_revision.accessed_by.add(*list(revision.accessed_by.all()))
21 | revision.delete()
22 | else:
23 | hashes_to_revisions[revision.plaintext_data_sha256] = revision
24 |
25 |
26 | class Migration(migrations.Migration):
27 |
28 | dependencies = [
29 | ('audit', '0002_auto_20170313_1544'),
30 | ('secrets', '0016_auto_20180220_1053'),
31 | ]
32 |
33 | operations = [
34 | migrations.RunPython(remove_duplicate_srevs),
35 | ]
36 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0018_auto_20180220_1244.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-02-20 12:44
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('secrets', '0017_auto_20180220_1115'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterUniqueTogether(
14 | name='secretrevision',
15 | unique_together={('plaintext_data_sha256', 'secret')},
16 | ),
17 | migrations.RemoveField(
18 | model_name='secretrevision',
19 | name='encrypted_data_sha256',
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0019_secret_notify_on_access_request.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-02-20 13:56
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11 | ('secrets', '0018_auto_20180220_1244'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='secret',
17 | name='notify_on_access_request',
18 | field=models.ManyToManyField(blank=True, related_name='notify_on_access_requests_for', to=settings.AUTH_USER_MODEL),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0020_auto_20180220_1356.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-02-20 13:56
2 |
3 | from django.db import migrations
4 |
5 |
6 | def copy_owner_data(apps, schema_editor):
7 | Secret = apps.get_model('secrets', 'Secret')
8 | for secret in Secret.objects.all():
9 | secret.allowed_groups.add(*list(secret.owner_groups.all()))
10 | secret.allowed_users.add(*list(secret.owner_users.all()))
11 | secret.notify_on_access_request.add(*list(secret.owner_users.all()))
12 | for group in secret.owner_groups.all():
13 | secret.notify_on_access_request.add(*list(group.user_set.all()))
14 |
15 |
16 | class Migration(migrations.Migration):
17 |
18 | dependencies = [
19 | ('secrets', '0019_secret_notify_on_access_request'),
20 | ]
21 |
22 | operations = [
23 | migrations.RunPython(copy_owner_data),
24 | ]
25 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0021_auto_20180220_1428.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-02-20 14:28
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('secrets', '0020_auto_20180220_1356'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='secret',
15 | name='owner_groups',
16 | ),
17 | migrations.RemoveField(
18 | model_name='secret',
19 | name='owner_users',
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0022_secret_last_changed.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2019-08-22 12:34
2 |
3 | from django.db import migrations, models
4 | import django.utils.timezone
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('secrets', '0021_auto_20180220_1428'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='secret',
16 | name='last_changed',
17 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
18 | preserve_default=False,
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0023_auto_20190822_1234.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2019-08-22 12:34
2 |
3 | from django.db import migrations
4 |
5 |
6 | def backfill_last_changed(apps, schema_editor):
7 | # We can't import the Person model directly as it may be a newer
8 | # version than this migration expects. We use the historical version.
9 | Secret = apps.get_model('secrets', 'Secret')
10 | for secret in Secret.objects.all():
11 | if secret.current_revision: # leftovers from a bug
12 | secret.last_changed = secret.current_revision.created
13 | secret.save()
14 |
15 |
16 | class Migration(migrations.Migration):
17 |
18 | dependencies = [
19 | ('secrets', '0022_secret_last_changed'),
20 | ]
21 |
22 | operations = [
23 | migrations.RunPython(backfill_last_changed),
24 | ]
25 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0024_auto_20210824_1203.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.6 on 2021-08-24 12:03
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('secrets', '0023_auto_20190822_1234'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='secret',
15 | name='notify_on_access_request',
16 | ),
17 | migrations.AlterField(
18 | model_name='secret',
19 | name='access_policy',
20 | field=models.PositiveSmallIntegerField(choices=[(1, 'discoverable'), (2, 'everyone'), (3, 'hidden')], default=1),
21 | ),
22 | migrations.DeleteModel(
23 | name='AccessRequest',
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0025_alter_secret_description_alter_secret_name.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.1 on 2023-06-11 16:50
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("secrets", "0024_auto_20210824_1203"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="secret",
14 | name="description",
15 | field=models.TextField(
16 | blank=True, help_text="Further information on the secret.", null=True
17 | ),
18 | ),
19 | migrations.AlterField(
20 | model_name="secret",
21 | name="name",
22 | field=models.CharField(
23 | help_text="Enter a unique name for the secret", max_length=92
24 | ),
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0026_allowed_groups_users_intermediate.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.1 on 2023-06-11 21:27
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | def copy_shared_secrets_data(apps, schema_editor):
9 | secret_model = apps.get_model('secrets', 'Secret')
10 | shared_secret_data_model = apps.get_model('secrets', 'SharedSecretData')
11 |
12 | secret_data = secret_model.objects.all().only(
13 | 'id',
14 | 'allowed_users',
15 | 'allowed_groups'
16 | )
17 |
18 | user_shares = []
19 | group_shares = []
20 | for secret in secret_data:
21 | group_shares += [(secret, group) for group in secret.allowed_groups.all()]
22 | user_shares += [(secret, user) for user in secret.allowed_users.all()]
23 |
24 | shared_secret_data_model.objects.bulk_create(
25 | [shared_secret_data_model(secret=secret, user=user) for secret, user in user_shares] +
26 | [shared_secret_data_model(secret=secret, group=group) for secret, group in group_shares]
27 | )
28 |
29 |
30 | class Migration(migrations.Migration):
31 | dependencies = [
32 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
33 | ('auth', '0012_alter_user_first_name_max_length'),
34 | ('secrets', '0025_alter_secret_description_alter_secret_name'),
35 | ]
36 |
37 | operations = [
38 | migrations.CreateModel(
39 | name='SharedSecretData',
40 | fields=[
41 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
42 | ('group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='auth.group')),
43 | ('secret', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='secrets.secret')),
44 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
45 | ],
46 | options={
47 | 'unique_together': {('user', 'secret'), ('group', 'secret')},
48 | },
49 | ),
50 | migrations.RunPython(
51 | code=copy_shared_secrets_data,
52 | ),
53 | migrations.RemoveField(
54 | model_name='secret',
55 | name='allowed_groups',
56 | ),
57 | migrations.RemoveField(
58 | model_name='secret',
59 | name='allowed_users',
60 | ),
61 | migrations.AddField(
62 | model_name='secret',
63 | name='allowed_groups',
64 | field=models.ManyToManyField(blank=True, related_name='allowed_passwords', through='secrets.SharedSecretData', to='auth.group'),
65 | ),
66 | migrations.AddField(
67 | model_name='secret',
68 | name='allowed_users',
69 | field=models.ManyToManyField(blank=True, related_name='allowed_passwords', through='secrets.SharedSecretData', to=settings.AUTH_USER_MODEL),
70 | ),
71 | ]
72 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0027_sharedsecretdata_only_one_set.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.1 on 2023-06-11 22:41
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("secrets", "0026_allowed_groups_users_intermediate"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddConstraint(
13 | model_name="sharedsecretdata",
14 | constraint=models.CheckConstraint(
15 | check=models.Q(
16 | models.Q(("group__isnull", False), ("user__isnull", True)),
17 | models.Q(("group__isnull", True), ("user__isnull", False)),
18 | _connector="OR",
19 | ),
20 | name="only_one_set",
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0028_sharedsecretdata_grant_description_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.1 on 2023-06-11 22:56
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("secrets", "0027_sharedsecretdata_only_one_set"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="sharedsecretdata",
14 | name="grant_description",
15 | field=models.TextField(null=True),
16 | ),
17 | migrations.AddField(
18 | model_name="sharedsecretdata",
19 | name="granted_until",
20 | field=models.DateTimeField(blank=True, null=True),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0029_sharedsecretdata_granted_by.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.1 on 2023-06-11 23:18
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11 | ("secrets", "0028_sharedsecretdata_grant_description_and_more"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="sharedsecretdata",
17 | name="granted_by",
18 | field=models.ForeignKey(
19 | null=True,
20 | on_delete=django.db.models.deletion.CASCADE,
21 | related_name="+",
22 | to=settings.AUTH_USER_MODEL,
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0030_sharedsecretdata_granted_on.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.1 on 2023-06-12 00:09
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("secrets", "0029_sharedsecretdata_granted_by"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="sharedsecretdata",
14 | name="granted_on",
15 | field=models.DateTimeField(auto_now_add=True, null=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0031_rename_share_data_fields.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-07-07 13:22
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11 | ("auth", "0012_alter_user_first_name_max_length"),
12 | ("secrets", "0030_sharedsecretdata_granted_on"),
13 | ]
14 |
15 | operations = [
16 | migrations.RemoveField(
17 | model_name="secret",
18 | name="allowed_groups",
19 | ),
20 | migrations.RemoveField(
21 | model_name="secret",
22 | name="allowed_users",
23 | ),
24 | migrations.AddField(
25 | model_name="secret",
26 | name="shared_groups",
27 | field=models.ManyToManyField(
28 | blank=True, through="secrets.SharedSecretData", to="auth.group"
29 | ),
30 | ),
31 | migrations.AddField(
32 | model_name="secret",
33 | name="shared_users",
34 | field=models.ManyToManyField(
35 | blank=True,
36 | through="secrets.SharedSecretData",
37 | to=settings.AUTH_USER_MODEL,
38 | ),
39 | ),
40 | migrations.AlterField(
41 | model_name="sharedsecretdata",
42 | name="granted_by",
43 | field=models.ForeignKey(
44 | null=True,
45 | on_delete=django.db.models.deletion.SET_NULL,
46 | related_name="+",
47 | to=settings.AUTH_USER_MODEL,
48 | ),
49 | ),
50 | migrations.AlterField(
51 | model_name="sharedsecretdata",
52 | name="group",
53 | field=models.ForeignKey(
54 | null=True,
55 | on_delete=django.db.models.deletion.CASCADE,
56 | related_name="secret_share_data",
57 | to="auth.group",
58 | ),
59 | ),
60 | migrations.AlterField(
61 | model_name="sharedsecretdata",
62 | name="secret",
63 | field=models.ForeignKey(
64 | on_delete=django.db.models.deletion.CASCADE,
65 | related_name="share_data",
66 | to="secrets.secret",
67 | ),
68 | ),
69 | migrations.AlterField(
70 | model_name="sharedsecretdata",
71 | name="user",
72 | field=models.ForeignKey(
73 | null=True,
74 | on_delete=django.db.models.deletion.CASCADE,
75 | related_name="secret_share_data",
76 | to=settings.AUTH_USER_MODEL,
77 | ),
78 | ),
79 | ]
80 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0032_alter_sharedsecretdata_granted_by.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-09-05 10:20
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11 | ("secrets", "0031_rename_share_data_fields"),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name="sharedsecretdata",
17 | name="granted_by",
18 | field=models.ForeignKey(
19 | null=True,
20 | on_delete=django.db.models.deletion.PROTECT,
21 | related_name="+",
22 | to=settings.AUTH_USER_MODEL,
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0033_secretrevision_encrypted_otp_key.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.13 on 2024-07-15 11:56
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('secrets', '0032_alter_sharedsecretdata_granted_by'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='secretrevision',
15 | name='encrypted_otp_key',
16 | field=models.BinaryField(blank=True, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0034_remove_secretrevision_encrypted_otp_key_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.13 on 2024-07-22 14:06
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('secrets', '0033_secretrevision_encrypted_otp_key'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='secretrevision',
15 | name='encrypted_otp_key',
16 | ),
17 | migrations.AddField(
18 | model_name='secretrevision',
19 | name='encrypted_otp_key_data',
20 | field=models.BinaryField(blank=True, null=True),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/0035_remove_secretrevision_encrypted_otp_key_data_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.13 on 2024-08-08 14:07
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('secrets', '0034_remove_secretrevision_encrypted_otp_key_and_more'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='secretrevision',
15 | name='encrypted_otp_key_data',
16 | ),
17 | migrations.AddField(
18 | model_name='secretrevision',
19 | name='otp_key_set',
20 | field=models.BooleanField(default=False),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/teamvault/apps/secrets/migrations/__init__.py
--------------------------------------------------------------------------------
/teamvault/apps/secrets/tasks.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | import logging
4 |
5 | from ..audit.auditlog import log
6 | from ..audit.models import LogEntry, AuditLogCategoryChoices
7 | from ..secrets.models import SharedSecretData
8 |
9 | from django.conf import settings
10 | from django.contrib.auth.models import Group
11 | from django.utils.timezone import now
12 | from django.utils.translation import gettext_lazy as _
13 | from huey import crontab
14 | from huey.contrib.djhuey import periodic_task
15 |
16 |
17 | huey_log = logging.getLogger('huey')
18 |
19 |
20 | @periodic_task(crontab(**settings.HUEY_TASKS['scheduler_frequency']))
21 | def prune_expired_shares():
22 | for share in SharedSecretData.objects.with_expiry_state().filter(is_expired=True):
23 | huey_log.info(
24 | _("Removing expired share of '{secret}' ({secret_id}) for {share_type} '{who}', was valid until {until}").format(
25 | secret=share.secret,
26 | secret_id=share.secret.hashid,
27 | share_type=_("user") if share.user else _("group"),
28 | until=share.granted_until,
29 | who=share.user or share.group,
30 | ),
31 | )
32 | share.delete()
33 |
34 |
35 | @periodic_task(crontab(**settings.HUEY_TASKS['scheduler_frequency']))
36 | def revoke_unused_shares():
37 | if settings.HUEY_TASKS['revoke_unused_shares_after_days'] is None:
38 | return
39 |
40 | grace_period = now() - timedelta(days=settings.HUEY_TASKS['revoke_unused_shares_after_days'])
41 |
42 | for share in SharedSecretData.objects.filter(
43 | granted_on__lt=grace_period,
44 | ):
45 | users_to_check = []
46 |
47 | if share.user:
48 | users_to_check.append(share.user)
49 | else:
50 | users_to_check.extend(share.group.user_set.all())
51 |
52 | do_revoke = True
53 | for user in users_to_check:
54 | accessed = LogEntry.objects.filter(
55 | actor=user,
56 | secret=share.secret,
57 | time__gte=grace_period,
58 | )
59 | if accessed:
60 | do_revoke = False
61 | # Skip further unnecessary checks.
62 | break
63 |
64 | if do_revoke:
65 | log(
66 | _(
67 | "Share for {share_type} '{who}' automatically revoked, "
68 | "not used since {grace_period}"
69 | ).format(
70 | grace_period=grace_period,
71 | share_type=_("user") if share.user else _("group"),
72 | who=share.user or share.group,
73 | ),
74 | category=AuditLogCategoryChoices.SHARE_AUTOMATICALLY_REVOKED,
75 | level='info',
76 | secret=share.secret,
77 | )
78 | share.delete()
79 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/opensearch.xml:
--------------------------------------------------------------------------------
1 |
2 | TeamVault
3 | UTF-8
4 |
5 |
6 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/addedit_content/cc.html:
--------------------------------------------------------------------------------
1 | {% extends "secrets/secret_addedit.html" %}
2 | {% load django_bootstrap5 %}
3 | {% load i18n %}
4 | {% block content_type_fields %}
5 |
6 |
31 |
32 |
35 |
36 |
37 | {% translate "All fields in this section will be stored securely." %}
38 |
39 |
40 |
41 |
42 | {% endblock %}
43 |
44 | {% block additionalJS %}
45 | {{ block.super }}
46 |
78 | {% endblock %}
79 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/addedit_content/file.html:
--------------------------------------------------------------------------------
1 | {% extends "secrets/secret_addedit.html" %}
2 | {% load django_bootstrap5 %}
3 | {% load i18n %}
4 | {% block form_attributes %}enctype="multipart/form-data"{% endblock %}
5 | {% block content_type_fields %}
6 |
7 |
8 | {{ form.file.label }}
9 |
10 |
11 |
23 | {% for error in form.file.errors %}
24 |
{{ error }}
25 | {% endfor %}
26 |
27 |
28 | {% endblock %}
29 |
30 | {% block additionalJS %}
31 | {{ block.super }}
32 |
47 | {% endblock %}
48 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/dashboard.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load i18n %}
3 | {% load static %}
4 | {% block title %}{% trans "Dashboard" %}{% endblock %}
5 | {% block content %}
6 |
7 |
8 |
{% trans "Recently used secrets" %}
9 |
10 |
11 | {% if recently_used_secrets %}
12 |
13 | {% for secret in recently_used_secrets %}
14 | {% include "secrets/secret_row.html" %}
15 | {% endfor %}
16 |
17 | {% else %}
18 |
19 | {% translate "You have not used any secrets recently." %}
20 |
21 | {% endif %}
22 |
23 |
24 |
25 |
26 |
{% trans "Most used secrets" %}
27 |
28 |
29 | {% if most_used_secrets %}
30 |
31 | {% for secret in most_used_secrets %}
32 | {% include "secrets/secret_row.html" %}
33 | {% endfor %}
34 |
35 | {% else %}
36 |
37 | {% translate "You have not used any secrets yet." %}
38 |
39 | {% endif %}
40 |
41 |
42 |
43 |
44 | {% endblock %}
45 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/detail_content/_js.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
132 |
150 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/detail_content/_su_confirm_modal.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
5 |
6 |
7 |
12 |
13 |
14 | {% blocktranslate %}
15 | This secret was not explicitly shared with you or a group you belong to.
16 |
17 | To view this secret using your superadmin permissions, choose Continue .
18 |
19 | Every superuser access will be logged as such.
20 | {% endblocktranslate %}
21 |
22 |
23 |
33 |
34 |
35 |
36 |
37 |
42 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/detail_content/file.html:
--------------------------------------------------------------------------------
1 | {% extends 'secrets/secret_detail.html' %}
2 | {% load i18n %}
3 |
4 | {% block secret_content %}
5 |
13 | {% endblock %}
14 |
15 | {% block secret_attributes %}
16 | {% if secret.description %}
17 |
18 |
19 |
20 |
21 | {% trans "Description" %}
22 | {{ secret.description|linebreaksbr|urlize }}
23 |
24 |
25 |
26 | {% endif %}
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/detail_content/meta.html:
--------------------------------------------------------------------------------
1 | {% load humanize %}
2 | {% load i18n %}
3 |
4 |
5 |
6 |
7 |
8 | {% trans "Changed" %}
9 |
10 |
12 | {{ secret.last_changed|naturalday:"Y-m-d" }}
13 |
14 |
15 |
16 |
17 | {% trans "Changed by" %}
18 | {{ secret.current_revision.set_by.username }}
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% trans "Created" %}
26 |
27 |
29 | {{ secret.created|naturalday:"Y-m-d" }}
30 |
31 |
32 |
33 |
34 | {% trans "Created by" %}
35 | {{ secret.created_by.username }}
36 |
37 |
38 |
39 |
40 |
41 |
42 | {% trans "Shared with" %}
43 |
44 |
46 | {% blocktrans with groupcount=allowed_groups|length %}
47 | {{ groupcount }} group(s)
48 | {% endblocktrans %}
49 |
50 |
51 |
53 | {% blocktrans with usercount=allowed_users|length %}
54 | {{ usercount }} user(s)
55 | {% endblocktrans %}
56 |
57 |
58 |
59 |
60 |
61 | {% if request.user.is_superuser %}
62 |
63 |
64 | {% trans "Audit log" %}
65 |
66 | {% endif %}
67 |
68 |
69 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/search/_search_item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ${data.value.name}
5 |
6 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/secret_delete.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load i18n %}
3 | {% load static %}
4 | {% block title %}{% trans "Confirm deletion" %}{% endblock %}
5 | {% block content %}
6 |
7 |
8 |
9 | {% blocktrans with name=secret.name content_type=secret.get_content_type_display %}
10 | Delete {{ content_type }} '{{ name }}'?
11 | {% endblocktrans %}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {% blocktrans %}
20 | Passwords are never completely erased from the system. Administrators can undelete them. Regular users will not be able to see or access deleted passwords. If a password has been revealed to an unauthorized party, you should change it instead.
21 | {% endblocktrans %}
22 |
23 |
28 |
29 |
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/secret_list.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 | {% load static %}
5 | {% load smart_pagination %}
6 | {% block title %}{% translate "Browse" %}{% endblock %}
7 | {% block content %}
8 |
9 |
10 |
11 |
12 |
13 |
14 | {% if request.GET.search %}
15 | {% blocktrans with search=request.GET.search %}Search results for '{{ search }}'...
16 | {% endblocktrans %}
17 | {% else %}
18 | {% translate "Browse all items" %}
19 | {% endif %}
20 |
21 |
22 |
23 |
24 |
25 |
26 | {% blocktranslate with count=page_obj.paginator.count %}
27 | {{ count }} item(s) found
28 | {% endblocktranslate %}
29 |
30 |
31 | {% include 'helpers/filter.html' %}
32 |
33 |
34 |
35 | {% for secret in secrets %}
36 | {% include "secrets/secret_row.html" %}
37 | {% endfor %}
38 |
39 |
40 |
41 | {% include "pagination.html" %}
42 |
43 |
44 | {% endblock %}
45 | {% block additionalJS %}
46 |
53 | {% endblock %}
54 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/secret_restore.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load i18n %}
3 | {% load static %}
4 | {% block title %}{% trans "Confirm restore" %}{% endblock %}
5 | {% block content %}
6 |
7 |
8 |
9 | {% blocktrans with name=secret.name content_type=secret.get_content_type_display %}
10 | Restore {{ content_type }} '{{ name }}'?
11 | {% endblocktrans %}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {% blocktrans %}
20 | The secret will be restored with its current permissions. Please be sure that the secret should be
21 | restored and become visible to users.
22 | {% endblocktrans %}
23 |
24 |
31 |
32 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templates/secrets/secret_row.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 |
5 | {% if secret.content_type == secret.CONTENT_PASSWORD %}
6 |
7 | {% elif secret.content_type == secret.CONTENT_FILE %}
8 |
9 | {% elif secret.content_type == secret.CONTENT_CC %}
10 |
11 | {% endif %}
12 |
13 |
18 | {% if secret.username or secret.filename %}
19 |
20 | {% if secret.username %}
21 | {{ secret.username }}
22 | {% endif %}
23 | {% if secret.filename %}
24 | {{ secret.filename }}
25 | {% endif %}
26 |
27 | {% endif %}
28 |
29 | {% if secret.status == secret.STATUS_DELETED %}
30 |
31 | {% elif secret.status == secret.STATUS_NEEDS_CHANGING %}
32 |
33 | {% endif %}
34 | {% if secret not in readable_secrets %}
35 |
36 | {% endif %}
37 |
38 |
39 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/teamvault/apps/secrets/templatetags/__init__.py
--------------------------------------------------------------------------------
/teamvault/apps/secrets/templatetags/smart_pagination.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urlencode
2 |
3 | from django import template
4 |
5 |
6 | register = template.Library()
7 |
8 |
9 | @register.simple_tag()
10 | def querystring(request, **kwargs):
11 | """
12 | Append or update params in a querystring.
13 | """
14 | querydict = request.GET.copy()
15 | for k, v in kwargs.items():
16 | if v is not None:
17 | querydict[k] = str(v)
18 | elif k in querydict:
19 | querydict.pop(k)
20 | qs = querydict.urlencode(safe='/')
21 | if qs:
22 | return '?' + qs
23 | else:
24 | return ''
25 |
26 |
27 | @register.filter
28 | def smart_pages(all_pages, current_page):
29 | all_pages = list(all_pages)
30 | smart_pages = set([
31 | 1,
32 | all_pages[-1],
33 | current_page,
34 | max(min(current_page // 2, all_pages[-1]), 1),
35 | max(min(current_page + ((all_pages[-1] - current_page) // 2), all_pages[-1]), 1),
36 | max(min(current_page + 1, all_pages[-1]), 1),
37 | max(min(current_page + 2, all_pages[-1]), 1),
38 | max(min(current_page - 1, all_pages[-1]), 1),
39 | max(min(current_page - 2, all_pages[-1]), 1),
40 | ])
41 | return sorted(smart_pages)
42 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/teamvault/apps/secrets/tests/__init__.py
--------------------------------------------------------------------------------
/teamvault/apps/secrets/tests/test_model_consistency.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User, Group
2 | from django.db import IntegrityError
3 | from django.test import TestCase
4 |
5 | from teamvault.apps.secrets.models import Secret, SharedSecretData
6 |
7 |
8 | class SecretConsistencyTestCase(TestCase):
9 | def setUp(self):
10 | User.objects.create(username='testuser')
11 | Group.objects.create(name='testgroup')
12 |
13 | def test_secret_unique_together(self):
14 | user = User.objects.get(username='testuser')
15 | group = Group.objects.get(name='testgroup')
16 |
17 | secret = Secret.objects.create(name="testsecret", created_by=user)
18 | secret.shared_users.add(user, through_defaults={})
19 | secret.shared_groups.add(group, through_defaults={})
20 |
21 | with self.assertRaises(IntegrityError):
22 | SharedSecretData.objects.create(secret=secret, user=user)
23 | SharedSecretData.objects.create(secret=secret, group=group)
24 |
25 | def test_shared_secret_data_only_one_constraint(self):
26 | user = User.objects.get(username='testuser')
27 | group = Group.objects.get(name='testgroup')
28 | secret = Secret.objects.create(name="testsecret", created_by=user)
29 |
30 | with self.assertRaises(IntegrityError):
31 | SharedSecretData.objects.create(secret=secret, group=group, user=user)
32 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 | urlpatterns = (
6 | path(
7 | '',
8 | views.dashboard,
9 | name='dashboard',
10 | ),
11 | path(
12 | 'opensearch.xml',
13 | views.opensearch,
14 | name='opensearch',
15 | ),
16 | path(
17 | 'secrets/',
18 | views.secret_list,
19 | name='secrets.secret-list',
20 | ),
21 | path(
22 | 'secrets//',
23 | views.secret_detail,
24 | name='secrets.secret-detail',
25 | ),
26 | path(
27 | 'secrets//delete',
28 | views.secret_delete,
29 | name='secrets.secret-delete',
30 | ),
31 | path(
32 | 'secrets//download',
33 | views.secret_download,
34 | name='secrets.secret-download',
35 | ),
36 | path(
37 | 'secrets//edit',
38 | views.secret_edit,
39 | name='secrets.secret-edit',
40 | ),
41 | path(
42 | 'secrets//metadata',
43 | views.secret_metadata,
44 | name='secrets.secret-metadata',
45 | ),
46 | path(
47 | 'secrets//restore',
48 | views.secret_restore,
49 | name='secrets.secret-restore',
50 | ),
51 | path(
52 | 'secrets//share',
53 | views.secret_share_list,
54 | name='secrets.secret-share',
55 | ),
56 | path(
57 | 'secrets//share//delete',
58 | views.secret_share_delete,
59 | name='secrets.secret-share-delete',
60 | ),
61 | path(
62 | 'secrets/add/',
63 | views.secret_add,
64 | name='secrets.secret-add',
65 | ),
66 | path(
67 | 'secrets/live-search',
68 | views.secret_search,
69 | name='secrets.secret-search',
70 | ),
71 | )
72 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/utils.py:
--------------------------------------------------------------------------------
1 | import secrets
2 | import string
3 | from urllib.parse import urlparse, parse_qs
4 |
5 | from django.core.files.uploadhandler import MemoryFileUploadHandler, SkipFile
6 |
7 | from .models import Secret
8 |
9 |
10 | def extract_url_and_params(data):
11 | data_as_url = urlparse(data)
12 | data_params = parse_qs(data_as_url.query)
13 | for key, value in data_params.items():
14 | data_params[key] = value[0]
15 | return data_as_url, data_params
16 |
17 |
18 | def serialize_add_edit_data(cleaned_data, secret):
19 | plaintext_data = {}
20 | if secret.content_type == Secret.CONTENT_PASSWORD:
21 | cleaned_data_as_url, data_params = extract_url_and_params(cleaned_data["otp_key_data"])
22 | if cleaned_data.get("password"):
23 | plaintext_data['password'] = cleaned_data['password']
24 | for attr in ['secret', 'digits', 'algorithm']:
25 | if data_params.get(attr):
26 | plaintext_data[attr] = data_params[attr]
27 | elif secret.content_type == Secret.CONTENT_FILE:
28 | plaintext_data["file_content"] = cleaned_data['file'].read().decode("utf-8")
29 | secret.filename = cleaned_data['file'].name
30 | secret.save()
31 | elif secret.content_type == Secret.CONTENT_CC:
32 | plaintext_data = {
33 | 'holder': cleaned_data['holder'],
34 | 'number': cleaned_data['number'],
35 | 'expiration_month': str(cleaned_data['expiration_month']),
36 | 'expiration_year': str(cleaned_data['expiration_year']),
37 | 'security_code': str(cleaned_data['security_code']),
38 | 'password': cleaned_data['password'],
39 | }
40 | return plaintext_data
41 |
42 |
43 | def generate_password(length, digits, upper, lower, special):
44 | characters = string.ascii_letters + string.digits + string.punctuation
45 | password = []
46 | password.extend(secrets.choice(string.digits) for _ in range(digits))
47 | password.extend(secrets.choice(string.ascii_lowercase) for _ in range(lower))
48 | password.extend(secrets.choice(string.ascii_uppercase) for _ in range(upper))
49 | password.extend(secrets.choice(string.punctuation) for _ in range(special))
50 |
51 | # Fill the rest of the lenght with random characters from all types
52 | password.extend(secrets.choice(characters) for _ in range(length - len(password)))
53 |
54 | # Randomly shuffle the characters, so they're not grouped by type
55 | secrets.SystemRandom().shuffle(password)
56 |
57 | return ''.join(password)
58 |
59 |
60 | class CappedMemoryFileUploadHandler(MemoryFileUploadHandler):
61 | def receive_data_chunk(self, raw_data, start):
62 | if not self.activated: # if the file size is too big, this handler will not be activated
63 | # if we use StopUpload here, forms will not get fully validated,
64 | # which leads to more form errors than we prefer
65 | # raise StopUpload(connection_reset=True)
66 | raise SkipFile()
67 | super(CappedMemoryFileUploadHandler, self).receive_data_chunk(raw_data, start)
68 |
--------------------------------------------------------------------------------
/teamvault/apps/secrets/validators.py:
--------------------------------------------------------------------------------
1 | from base64 import b32decode
2 |
3 | from django.core.exceptions import ValidationError
4 | from django.utils.translation import gettext_lazy as _
5 |
6 |
7 | def is_valid_b32_string(value, casefold=False):
8 | try:
9 | b32decode(value, casefold=casefold)
10 | except Exception:
11 | raise ValidationError(_('OTP key has wrong format. Please enter a valid OTP key.'))
12 |
--------------------------------------------------------------------------------
/teamvault/apps/settings/__init__.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SettingsConfig(AppConfig):
5 | name = 'teamvault.apps.settings'
6 |
7 | def ready(self):
8 | from django.conf import settings
9 | from . import config, webpack
10 | parsed_config = config.get_config()
11 | config.configure_base_url(parsed_config, settings)
12 | config.configure_debugging(parsed_config, settings)
13 | config.configure_ldap_auth(parsed_config, settings)
14 | config.configure_google_auth(parsed_config, settings)
15 | config.configure_max_file_size(parsed_config, settings)
16 | config.configure_password_generator(parsed_config, settings)
17 | config.configure_superuser_reads(parsed_config, settings)
18 | config.configure_teamvault_secret_key(parsed_config, settings)
19 | config.configure_password_update_alert(parsed_config, settings)
20 | config.configure_whitenoise(settings)
21 | webpack.configure_webpack(settings)
22 |
--------------------------------------------------------------------------------
/teamvault/apps/settings/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Setting',
15 | fields=[
16 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
17 | ('key', models.CharField(unique=True, max_length=64)),
18 | ('value', models.CharField(max_length=255)),
19 | ],
20 | options={
21 | 'ordering': ('key',),
22 | },
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/teamvault/apps/settings/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seibert-media/teamvault/9e857487675b27ed8a018a665b72a55aa253ec13/teamvault/apps/settings/migrations/__init__.py
--------------------------------------------------------------------------------
/teamvault/apps/settings/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class Setting(models.Model):
6 | key = models.CharField(
7 | max_length=64,
8 | unique=True,
9 | )
10 | value = models.CharField(
11 | max_length=255,
12 | )
13 |
14 | class Meta:
15 | ordering = ('key',)
16 |
17 | @classmethod
18 | def get(cls, key, **kwargs):
19 | try:
20 | return cls.objects.get(key=key).value
21 | except cls.DoesNotExist:
22 | try:
23 | return kwargs['default']
24 | except KeyError:
25 | raise KeyError(_("value for '{}' not set").format(key))
26 |
27 | @classmethod
28 | def set(cls, key, value):
29 | try:
30 | setting = cls.objects.get(key=key)
31 | except cls.DoesNotExist:
32 | setting = cls()
33 | setting.key = key
34 | setting.value = value
35 | setting.save()
36 |
--------------------------------------------------------------------------------
/teamvault/apps/settings/webpack.py:
--------------------------------------------------------------------------------
1 | from os.path import join
2 |
3 |
4 | def configure_webpack(settings):
5 | settings.WEBPACK_LOADER = {
6 | 'DEFAULT': {
7 | 'CACHE': not settings.DEBUG,
8 | 'BUNDLE_DIR_NAME': 'bundled/', # must end with slash
9 | 'STATS_FILE': join(settings.PROJECT_ROOT, 'webpack-stats.json'),
10 | 'POLL_INTERVAL': 0.1,
11 | 'TIMEOUT': None,
12 | 'IGNORE': [r'.+\.hot-update.js', r'.+\.map'],
13 | 'LOADER_CLASS': 'webpack_loader.loader.WebpackLoader',
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/teamvault/cli.py:
--------------------------------------------------------------------------------
1 | from argparse import ArgumentParser, REMAINDER
2 | from gettext import gettext as _
3 | from hashlib import sha1
4 | from os import environ, mkdir
5 | from shutil import rmtree
6 | from subprocess import Popen
7 | from sys import argv
8 |
9 | import django
10 | from django.core.management import execute_from_command_line, get_commands
11 |
12 | from teamvault.__version__ import __version__
13 | from teamvault.apps.settings.config import create_default_config, UnconfiguredSettingsError
14 |
15 |
16 | def build_parser():
17 | parser = ArgumentParser(prog="teamvault")
18 | parser.add_argument(
19 | "--version",
20 | action='version',
21 | version=__version__,
22 | )
23 | subparsers = parser.add_subparsers(
24 | title=_("subcommands"),
25 | help=_("use 'teamvault --help' for more info"),
26 | )
27 |
28 | environ['DJANGO_SETTINGS_MODULE'] = 'teamvault.settings'
29 | environ.setdefault("TEAMVAULT_CONFIG_FILE", "/etc/teamvault.cfg")
30 |
31 | # teamvault plumbing
32 | unconfigured_settings = False
33 | try:
34 | django.setup()
35 | except UnconfiguredSettingsError:
36 | unconfigured_settings = True
37 |
38 | commands = [k for k in get_commands()]
39 | plumbing_help = f'One of: {",".join(commands)}'
40 | if unconfigured_settings:
41 | plumbing_help += " - To see all available commands, configure teamvault settings with \"teamvault setup\""
42 |
43 | parser_plumbing = subparsers.add_parser("plumbing")
44 | parser_plumbing.add_argument('plumbing_command', nargs=REMAINDER, help=plumbing_help)
45 | parser_plumbing.set_defaults(func=plumbing)
46 |
47 | # teamvault run
48 | parser_run = subparsers.add_parser("run")
49 | parser_run.add_argument('--bind', nargs='?', help='define bind, default is 127.0.0.1:8000')
50 | parser_run.set_defaults(func=run)
51 |
52 | # teamvault run_huey
53 | parser_run = subparsers.add_parser("run_huey")
54 | parser_run.set_defaults(func=run_huey)
55 |
56 | # teamvault setup
57 | parser_setup = subparsers.add_parser("setup")
58 | parser_setup.set_defaults(func=setup)
59 |
60 | # teamvault upgrade
61 | parser_upgrade = subparsers.add_parser("upgrade")
62 | parser_upgrade.set_defaults(func=upgrade)
63 | return parser
64 |
65 |
66 | def main(*args):
67 | """
68 | Entry point for the 'teamvault' command line utility.
69 |
70 | args: used for integration tests
71 | """
72 | if not args:
73 | args = argv[1:]
74 |
75 | parser = build_parser()
76 | pargs = parser.parse_args(args)
77 | if not hasattr(pargs, 'func'):
78 | parser.print_help()
79 | exit(2)
80 | pargs.func(pargs)
81 |
82 |
83 | def plumbing(pargs):
84 | execute_from_command_line([""] + pargs.plumbing_command)
85 |
86 |
87 | def run(pargs):
88 | cmd = "gunicorn --preload teamvault.wsgi:application"
89 | if pargs.bind:
90 | cmd += ' -b ' + pargs.bind
91 |
92 | print("Now open http://localhost:8000")
93 | gunicorn = Popen(
94 | cmd,
95 | shell=True,
96 | )
97 | gunicorn.communicate()
98 |
99 |
100 | def run_huey(pargs):
101 | execute_from_command_line(["", "run_huey"])
102 |
103 |
104 | def setup(pargs):
105 | environ.setdefault("TEAMVAULT_CONFIG_FILE", "/etc/teamvault.cfg")
106 | create_default_config(environ['TEAMVAULT_CONFIG_FILE'])
107 |
108 |
109 | def upgrade(pargs):
110 | environ['DJANGO_SETTINGS_MODULE'] = 'teamvault.settings'
111 | environ.setdefault("TEAMVAULT_CONFIG_FILE", "/etc/teamvault.cfg")
112 |
113 | print("\n### Running migrations...\n")
114 | execute_from_command_line(["", "migrate", "--noinput", "-v", "3", "--traceback"])
115 |
116 | from django.conf import settings
117 | from .apps.settings.models import Setting
118 |
119 | if Setting.get("fernet_key_hash", default=None) is None:
120 | print("\n### Storing fernet_key hash in database...\n")
121 | key_hash = sha1(settings.TEAMVAULT_SECRET_KEY.encode('utf-8')).hexdigest()
122 | Setting.set("fernet_key_hash", key_hash)
123 |
124 | print("\n### Gathering static files...\n")
125 | try:
126 | rmtree(settings.STATIC_ROOT)
127 | except FileNotFoundError:
128 | pass
129 | mkdir(settings.STATIC_ROOT)
130 | execute_from_command_line(["", "collectstatic", "--noinput"])
131 |
132 | print("\n### Updating search index...\n")
133 | execute_from_command_line(["", "update_search_index"])
134 |
--------------------------------------------------------------------------------
/teamvault/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 | import sys
4 | from os.path import dirname, join, realpath
5 |
6 | if __name__ == "__main__":
7 | sys.path.append(join(realpath(dirname(dirname(__file__)))))
8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "teamvault.settings")
9 |
10 | from django.core.management import execute_from_command_line
11 |
12 | execute_from_command_line(sys.argv)
13 |
--------------------------------------------------------------------------------
/teamvault/middleware.py:
--------------------------------------------------------------------------------
1 | from django.contrib import messages
2 | from django.contrib.messages.storage.base import BaseStorage, Message
3 | from django_htmx.http import trigger_client_event
4 |
5 |
6 | def htmx_message_middleware(get_response):
7 | # One-time configuration and initialization.
8 |
9 | def middleware(request):
10 | # Code to be executed for each request before
11 | # the view (and later middleware) are called.
12 | response = get_response(request)
13 |
14 | # Ignore non-HTMX requests
15 | if "HX-Request" not in request.headers:
16 | return response
17 |
18 | # HTMX will not read HX headers in redirects but the subsequent GET response.
19 | if 300 <= response.status_code < 400:
20 | return response
21 |
22 | storage: BaseStorage = messages.get_messages(request)
23 | msg_list = []
24 | for msg in storage:
25 | msg: Message
26 | msg_list.append({
27 | 'message': msg.message,
28 | # debug|info|success|warning|error
29 | 'level': msg.level_tag,
30 | })
31 |
32 | trigger_client_event(response, 'django.contrib.messages', {'message_list': msg_list})
33 | return response
34 |
35 | return middleware
36 |
--------------------------------------------------------------------------------
/teamvault/settings.py:
--------------------------------------------------------------------------------
1 | from os.path import dirname, join, realpath
2 |
3 | from huey import SqliteHuey
4 |
5 | from teamvault.apps.settings.config import (
6 | configure_database,
7 | configure_django_secret_key,
8 | configure_hashid,
9 | configure_huey,
10 | configure_logging,
11 | configure_password_generator,
12 | configure_session,
13 | configure_time_zone,
14 | get_config,
15 | get_from_config,
16 | )
17 |
18 | CONFIG = get_config()
19 | PROJECT_ROOT = realpath(dirname(__file__))
20 |
21 | # Django
22 |
23 | AUTHENTICATION_BACKENDS = [
24 | 'django.contrib.auth.backends.ModelBackend',
25 | ]
26 |
27 | DATABASES = configure_database(CONFIG)
28 |
29 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
30 |
31 | FILE_UPLOAD_HANDLERS = (
32 | "teamvault.apps.secrets.utils.CappedMemoryFileUploadHandler",
33 | )
34 |
35 | FIXTURE_DIRS = (
36 | join(PROJECT_ROOT, "fixtures"),
37 | )
38 |
39 | INSTALLED_APPS = [
40 | 'django.contrib.auth',
41 | 'django.contrib.contenttypes',
42 | 'django.contrib.humanize',
43 | 'django.contrib.sessions',
44 | 'django.contrib.messages',
45 | 'django.contrib.staticfiles',
46 | 'django_bootstrap5',
47 | 'huey.contrib.djhuey',
48 | 'rest_framework',
49 | 'social_django',
50 | 'teamvault.apps.accounts.AccountsConfig',
51 | 'teamvault.apps.audit.AuditConfig',
52 | 'teamvault.apps.secrets.SecretsConfig',
53 | 'teamvault.apps.settings.SettingsConfig',
54 | 'webpack_loader',
55 | ]
56 |
57 | LANGUAGE_CODE = "en-us"
58 |
59 | LOCALE_PATHS = (PROJECT_ROOT + "/locale",)
60 |
61 | LOGIN_REDIRECT_URL = "/"
62 | LOGIN_URL = 'accounts.login'
63 | LOGOUT_URL = 'accounts.logout'
64 |
65 | LOGGING = configure_logging(CONFIG)
66 |
67 | MIDDLEWARE = [
68 | 'django.middleware.security.SecurityMiddleware',
69 | 'whitenoise.middleware.WhiteNoiseMiddleware',
70 | 'django.contrib.sessions.middleware.SessionMiddleware',
71 | 'django.middleware.locale.LocaleMiddleware',
72 | 'django.middleware.common.CommonMiddleware',
73 | 'django.middleware.csrf.CsrfViewMiddleware',
74 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
75 | 'django.contrib.messages.middleware.MessageMiddleware',
76 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
77 | 'teamvault.middleware.htmx_message_middleware',
78 | ]
79 |
80 | ROOT_URLCONF = "teamvault.urls"
81 |
82 | SECRET_KEY = configure_django_secret_key(CONFIG)
83 |
84 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
85 |
86 | SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
87 | SESSION_COOKIE_AGE, SESSION_EXPIRE_AT_BROWSER_CLOSE, SESSION_COOKIE_SECURE = \
88 | configure_session(CONFIG)
89 |
90 | STATIC_ROOT = join(PROJECT_ROOT, "static_collected")
91 |
92 | # remember this is hardcoded in the error page templates (e.g. 500.html)
93 | STATIC_URL = "/static/"
94 |
95 | STATICFILES_DIRS = (
96 | join(PROJECT_ROOT, "static"),
97 | )
98 |
99 | TEMPLATES = [
100 | {
101 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
102 | 'DIRS': [join(PROJECT_ROOT, "templates"), ],
103 | 'APP_DIRS': True,
104 | 'OPTIONS': {
105 | 'context_processors': [
106 | 'django.contrib.auth.context_processors.auth',
107 | 'django.contrib.messages.context_processors.messages',
108 | 'django.template.context_processors.csrf',
109 | 'django.template.context_processors.debug',
110 | 'django.template.context_processors.i18n',
111 | 'django.template.context_processors.media',
112 | 'django.template.context_processors.request',
113 | 'django.template.context_processors.static',
114 | 'teamvault.apps.accounts.context_processors.google_auth_enabled',
115 | 'teamvault.apps.secrets.context_processors.version',
116 | ],
117 | },
118 | },
119 | ]
120 |
121 | TEST_RUNNER = 'django.test.runner.DiscoverRunner'
122 |
123 | TIME_ZONE = configure_time_zone(CONFIG)
124 |
125 | USE_I18N = False
126 | USE_THOUSAND_SEPARATOR = False
127 | USE_TZ = True
128 |
129 | # HashID
130 | HASHID_MIN_LENGTH, HASHID_SALT = configure_hashid(CONFIG)
131 |
132 | # Django REST Framework
133 | REST_FRAMEWORK = {
134 | 'DEFAULT_MODEL_SERIALIZER_CLASS':
135 | 'rest_framework.serializers.HyperlinkedModelSerializer',
136 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
137 | 'DEFAULT_PERMISSION_CLASSES': (
138 | 'rest_framework.permissions.IsAuthenticated',
139 | ),
140 | 'PAGE_SIZE': 25,
141 | }
142 |
143 | # Social Auth
144 | SOCIAL_AUTH_JSONFIELD_ENABLED = True
145 |
146 | # Django-Bootstrap5
147 | BOOTSTRAP5 = {
148 | 'horizontal_field_class': 'col-xl-8',
149 | 'horizontal_label_class': 'col-xl-2',
150 | 'required_css_class': 'required',
151 | 'success_css_class': 'is-valid',
152 | 'error_css_class': 'is-invalid',
153 | }
154 |
155 | HUEY = SqliteHuey('teamvault')
156 | HUEY_TASKS = configure_huey(CONFIG)
157 |
--------------------------------------------------------------------------------
/teamvault/static/js/index.js:
--------------------------------------------------------------------------------
1 | // Import our custom CSS
2 | import '../scss/base.scss'
3 |
4 | import * as bootstrap from 'bootstrap' // TODO: Specify which plugins we really need
5 | import $ from 'jquery'
6 | import {Notyf} from 'notyf';
7 | import 'notyf/notyf.min.css'
8 | import autoCompleteJS from '@tarekraafat/autocomplete.js';
9 | import ClipboardJS from "clipboard";
10 | import DOMPurify from 'dompurify';
11 | import {TempusDominus} from '@eonasdan/tempus-dominus'
12 |
13 | import {zxcvbn, zxcvbnOptions} from '@zxcvbn-ts/core'
14 | import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common'
15 | import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en'
16 |
17 | import * as teamvault from './utils'
18 | import * as otp from './otp'
19 |
20 | window.otp = otp
21 | window.teamvault = teamvault
22 |
23 | // Bootstrap
24 | window.bootstrap = bootstrap
25 |
26 | // HTMX
27 | window.htmx = require('htmx.org')
28 |
29 | // jQuery
30 | window.$ = $
31 | window.jQuery = $
32 |
33 | //js qr scanner
34 | window.qrScanner = require("jsqr")
35 |
36 | // Bigtext
37 | require('bigtext');
38 |
39 | // Notyf
40 | document.addEventListener('DOMContentLoaded', () => {
41 | // Notyf tries to hook to a body, which we don't have in this context yet.
42 | window.notyf = new Notyf({position: {x: 'right', y: 'top'}})
43 | })
44 |
45 | // Card
46 | window.Card = require('card')
47 |
48 | // ClipboardJS
49 | window.ClipboardJS = ClipboardJS
50 |
51 | // autocomplete.js
52 | window.autoCompleteJS = autoCompleteJS
53 |
54 | // DOMPurify (needed for autocompleteJS ajax queries)
55 | window.DOMPurify = DOMPurify
56 |
57 | // Tempus Dominus
58 | window.TempusDominus = TempusDominus
59 |
60 |
61 | // zxcvbn
62 | zxcvbnOptions.setOptions({
63 | translations: zxcvbnEnPackage.translations,
64 | graphs: zxcvbnCommonPackage.adjacencyGraphs,
65 | dictionary: {
66 | ...zxcvbnCommonPackage.dictionary,
67 | ...zxcvbnEnPackage.dictionary,
68 | },
69 | })
70 | window.zxcvbn = zxcvbn
71 |
72 | // Select2
73 | require('select2');
74 | $.fn.select2.defaults.set("theme", "bootstrap-5")
75 | $.fn.select2.defaults.set("width", "100%") // https://github.com/select2/select2/issues/3278
76 | $.fn.select2.amd.require(['select2/selection/search'], function (Search) {
77 | // Patch backspace on select2 4.X. See https://github.com/select2/select2/issues/3354
78 | Search.prototype.searchRemoveChoice = function (decorated, item) {
79 | this.trigger('unselect', {
80 | data: item
81 | });
82 |
83 | this.$search.val('');
84 | this.handleSearch();
85 | };
86 | }, null, true);
87 |
--------------------------------------------------------------------------------
/teamvault/static/js/otp.js:
--------------------------------------------------------------------------------
1 | export async function refreshOtpEvery30Sec(inputElement, secret_url, bigElement) {
2 | const response = await fetch(secret_url+"/otp");
3 | const otp = await response.json();
4 | let newFieldData = otp.slice(0, 3) + " " + otp.slice(3);
5 | inputElement.innerHTML = newFieldData;
6 | if ( !bigElement.classList.contains("invisible")) {
7 | bigElement.children[1].innerHTML = newFieldData.replace('mx-1', 'mx-3');
8 | }
9 | }
10 |
11 | export async function otpCountdown(countdownContainerEl, countdownNumberEl, inputField, secret_url, bigElement) {
12 | const countdownTime = getcountdownTime();
13 | setCircleParams(document.getElementById("progress-circle"), countdownTime);
14 | countdownContainerEl.style.setProperty('--progress', countdownTime);
15 | countdownNumberEl.textContent = countdownTime;
16 | if ( !bigElement.children[0].classList.contains("invisible")) {
17 | const bigCountdownElement = bigElement.children[0].children[0];
18 | const bigCountdownNumberElement = bigCountdownElement.children[0];
19 | const bigCountdownCircleElement = bigCountdownElement.children[1].children[0];
20 | bigCountdownNumberElement.textContent = countdownTime;
21 | bigCountdownElement.style.setProperty('--progress', countdownTime);
22 | bigCountdownCircleElement.setAttribute("r", 50);
23 | bigCountdownCircleElement.setAttribute("cx", 100);
24 | bigCountdownCircleElement.setAttribute("cy", 100);
25 | setCircleParams(bigCountdownCircleElement, countdownTime);
26 | }
27 | if (countdownTime+1 === 30) {
28 | await refreshOtpEvery30Sec(inputField, secret_url, bigElement).then();
29 | }
30 | }
31 |
32 | function getcountdownTime(interval = 30) {
33 | const curUnixTime = Math.floor(Date.now() / 1000);
34 | const intervalStartTime = Math.floor(curUnixTime / 30) * 30;
35 | return intervalStartTime + 30 - curUnixTime - 1;
36 | }
37 |
38 |
39 | function setCircleParams(circleElement, progress){
40 | const radius = circleElement.getAttribute("r");
41 | const circleSize = (2 * Math.PI) * radius;
42 | const progressOffset = circleSize - ((progress/30)*circleSize);
43 | circleElement.style.setProperty("stroke-dasharray", circleSize+'px');
44 | circleElement.style.setProperty("stroke-dashoffset", progressOffset+'px');
45 | }
46 |
--------------------------------------------------------------------------------
/teamvault/static/js/utils.js:
--------------------------------------------------------------------------------
1 | export function scrollIfNeeded(el, containerEl) {
2 | const rect = el.getBoundingClientRect()
3 | const containerRect = containerEl.getBoundingClientRect()
4 | if (rect.top < containerRect.top) {
5 | if (!(el.previousElementSibling)) {
6 | el.scrollIntoView({block: 'end'})
7 | } else {
8 | el.scrollIntoView({block: 'start'})
9 | }
10 | } else if (rect.bottom > containerRect.bottom) {
11 | if (!(el.nextElementSibling)) {
12 | el.scrollIntoView({block: 'start'})
13 | } else {
14 | el.scrollIntoView({block: 'end'})
15 | }
16 | }
17 | }
18 |
19 | /**
20 | * @param {string} password
21 | * @returns {HTMLSpanElement[]} div containing the colored password
22 | */
23 | export const getColorfulPasswordHTML = (password) =>
24 | password.split("").map(character => {
25 | const newSpan = document.createElement("span");
26 | newSpan.innerText = character;
27 |
28 | if ("a" <= character && character <= "z") newSpan.classList.add("pw-lowercase");
29 | else if ("A" <= character && character <= "Z") newSpan.classList.add("pw-uppercase");
30 | else if ("0" <= character && character <= "9") newSpan.classList.add("pw-number");
31 | else newSpan.classList.add("pw-symbol");
32 |
33 | return newSpan;
34 | });
35 |
--------------------------------------------------------------------------------
/teamvault/static/scss/avatars.scss:
--------------------------------------------------------------------------------
1 | .avatar {
2 | --tv-avatar-border-radius: 100%;
3 | --tv-avatar-opacity: 100%;
4 | --tv-avatar-size-default: 2rem;
5 | --tv-avatar-size-sm: 1.25rem;
6 | --tv-avatar-size-lg: 2.5rem;
7 | --tv-avatar-size: var(--tv-avatar-size-default);
8 |
9 | border-color: var(--bs-border-color-translucent);
10 | border-radius: var(--tv-avatar-border-radius);
11 | opacity: var(--tv-avatar-opacity);
12 | height: var(--tv-avatar-size);
13 | width: var(--tv-avatar-size);
14 |
15 | &.avatar-sm {
16 | --tv-avatar-size: var(--tv-avatar-size-sm);
17 | }
18 |
19 | &.avatar-lg {
20 | --tv-avatar-size: var(--tv-avatar-size-lg);
21 | }
22 | }
23 |
24 | svg.avatar {
25 | background-color: var(--bs-gray-500);
26 | font-size: calc(var(--tv-avatar-size) * 0.4);
27 | }
28 |
29 | a {
30 | &:hover, &:focus {
31 | > .avatar {
32 | border-style: solid;
33 | border-width: 1px;
34 | }
35 | }
36 |
37 | &:hover > .avatar {
38 | --tv-avatar-opacity: 80% !important;
39 | }
40 |
41 | &:focus > .avatar {
42 | --tv-avatar-opacity: 90%;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/teamvault/static/scss/base.scss:
--------------------------------------------------------------------------------
1 | @import "@fontsource/poppins/100.css";
2 | @import "@fontsource/poppins/200.css";
3 | @import "@fontsource/poppins/300.css";
4 | @import "@fontsource/poppins/400.css";
5 | @import "@fontsource/poppins/500.css";
6 | @import "@fontsource/poppins/600.css";
7 | @import "@fontsource/poppins/700.css";
8 | @import "@fontsource/poppins/800.css";
9 | @import "@fontsource/poppins/900.css";
10 |
11 | @import "./theme";
12 | @import "./avatars";
13 | @import "./scrollbar";
14 | @import "./search";
15 | @import "./secrets";
16 | @import "./card.scss";
17 | @import "./circularProgressbar";
18 |
19 | @import "./fontawesome";
20 | @import "./select2";
21 |
22 | @import '@eonasdan/tempus-dominus/src/scss/tempus-dominus.scss';
23 |
24 | body {
25 | font-family: 'Poppins', serif;
26 | }
27 |
28 | a {
29 | text-decoration: none;
30 | color: var(--bs-info);
31 | }
32 |
33 | .background {
34 | position: fixed;
35 | top: 0;
36 | right: 0;
37 | bottom: 0;
38 | left: 0;
39 | background-position: 50% 50%;
40 | background: var(--bs-body-bg);
41 | z-index: -999;
42 | }
43 |
44 | .dropdown-menu i {
45 | color: #92A6B2;
46 | }
47 |
48 | h1 {
49 | color: var(--text-body);
50 |
51 | > .badge {
52 | vertical-align: top;
53 | }
54 | }
55 |
56 | .card {
57 | table {
58 | margin-bottom: 0;
59 | }
60 |
61 | &.shadow {
62 | box-shadow: 0 0.5rem 0.5rem rgba(0, 0, 0, 0.05);
63 | }
64 | }
65 |
66 | .card .card-body .table tr:last-child {
67 | border-bottom-style: hidden;
68 | }
69 |
70 | kbd {
71 | font-family: monospace;
72 | text-align: center;
73 | border-color: #303030;
74 | font-size: inherit;
75 |
76 | &:not(kbd:first-of-type) {
77 | margin-left: 0.1rem;
78 | }
79 | }
80 |
81 | .list-group-hover .list-group-item {
82 | &:hover {
83 | background-color: var(--bs-list-group-action-active-bg);
84 | }
85 | }
86 |
87 | .list-group-item {
88 | --bs-list-group-item-padding-y: 0.75rem;
89 | }
90 |
91 | @include media-breakpoint-down(lg) {
92 | .modal-lg {
93 | --bs-modal-width: 88%
94 | }
95 | }
96 |
97 | @include media-breakpoint-down(xl) {
98 | .modal-xl {
99 | --bs-modal-width: 88%
100 | }
101 | }
102 |
103 | [data-bs-theme="light"] {
104 | .lt-otp {
105 | color: white;
106 | }
107 | }
108 |
109 | [data-bs-theme="dark"] {
110 | .btn-light {
111 | color: var(--bs-light);
112 | background-color: var(--bs-secondary-bg);
113 | border-color: var(--bs-secondary-bg);
114 | border-width: 1px;
115 | transition: filter .15s ease-in-out;
116 |
117 | &:hover {
118 | filter: brightness(1.2);
119 | }
120 | &:active {
121 | filter: brightness(1.4);
122 | background-color: var(--bs-secondary-bg);
123 | border-color: var(--bs-secondary-bg);
124 | color: var(--bs-light);
125 | }
126 | }
127 |
128 | // bg-color for filter chips
129 | .bg-secondary-subtle {
130 | background-color: #37383b!important;
131 | }
132 | }
133 |
134 | .separator {
135 | height: 0.3em;
136 | width: 0.3em;
137 | margin-bottom: 0.2em;
138 | background-color: var(--bs-secondary);
139 | border-radius: 50%;
140 | display: inline-block;
141 | }
142 |
143 | .bg-otp {
144 | background-color: var(--bs-tertiary-bg);
145 | }
146 |
--------------------------------------------------------------------------------
/teamvault/static/scss/card.scss:
--------------------------------------------------------------------------------
1 | // custom dark credit card background
2 | [data-bs-theme="dark"] .jp-card-front {
3 | background-color: $secondary !important;
4 | }
5 |
--------------------------------------------------------------------------------
/teamvault/static/scss/circularProgressbar.scss:
--------------------------------------------------------------------------------
1 | $circle_size: 88;
2 | $max_progress: 30;
3 |
4 |
5 |
6 | #countdown {
7 | --progress: $max_progress;
8 | position: relative;
9 | height: 40px;
10 | width: 40px;
11 | text-align: center;
12 | }
13 |
14 | #countdown-number {
15 | font-family: monospace;
16 | line-height: 40px;
17 | display: inline-block;
18 | font-size: small;
19 | margin-left: -1px;
20 | }
21 |
22 | svg {
23 | position: absolute;
24 | top: 0;
25 | right: 0;
26 | width: 40px;
27 | height: 40px;
28 | transform: rotateY(-180deg) rotateZ(-90deg);
29 | }
30 |
31 | svg circle {
32 | stroke-linecap: round;
33 | stroke-width: 2px;
34 | stroke: rgb(min(255, 255 - ((var(--progress) * (10/3)) * 2.55)),
35 | max(0, ((var(--progress) * (10/3))*2.55)),
36 | 0);
37 | fill: none;
38 | }
39 |
--------------------------------------------------------------------------------
/teamvault/static/scss/fontawesome.scss:
--------------------------------------------------------------------------------
1 | $fa-font-path: '@fortawesome/fontawesome-free/webfonts';
2 | @import '@fortawesome/fontawesome-free/scss/fontawesome';
3 | @import "@fortawesome/fontawesome-free/scss/brands";
4 | @import '@fortawesome/fontawesome-free/scss/solid';
5 | @import '@fortawesome/fontawesome-free/scss/regular';
6 |
7 | .list-group-item .fa {
8 | vertical-align: middle
9 | }
10 |
--------------------------------------------------------------------------------
/teamvault/static/scss/scrollbar.scss:
--------------------------------------------------------------------------------
1 | body, .modal-dialog-scrollable .modal-body {
2 | &::-webkit-scrollbar {
3 | width: 20px;
4 | }
5 |
6 | &::-webkit-scrollbar-track {
7 | background-color: transparent;
8 | }
9 |
10 | &::-webkit-scrollbar-thumb {
11 | background-color: var(--bs-secondary);
12 | border-radius: 20px;
13 | border: 8px solid transparent;
14 | background-clip: content-box;
15 | min-height: 50px;
16 | }
17 |
18 | &::-webkit-scrollbar-thumb:hover {
19 | background-color: var(--bs-gray-500);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/teamvault/static/scss/search.scss:
--------------------------------------------------------------------------------
1 | @import "bootstrap/scss/variables";
2 |
3 | .searchbar {
4 | &.modal-header {
5 | border-bottom: 0;
6 | }
7 |
8 | div, form {
9 | cursor: auto;
10 | box-sizing: border-box;
11 | height: 56px;
12 | margin: 0;
13 | padding: 0 12px;
14 | position: relative;
15 | width: 100%;
16 | border-radius: var(--bs-border-radius);
17 | }
18 |
19 | label {
20 | margin: 0;
21 | padding: 0;
22 | align-items: center;
23 | display: flex;
24 | justify-content: center;
25 | color: var(--bs-custom-dark);
26 |
27 | svg {
28 | height: 24px;
29 | width: 24px;
30 | stroke-width: 1.6;
31 | }
32 | }
33 |
34 | .loading-indicator {
35 | display: none;
36 | margin: 0;
37 | padding: 0;
38 | }
39 |
40 | input {
41 | border: 0;
42 | box-shadow: none;
43 | background-color: var(--bs-body-bg);
44 |
45 | &:focus {
46 | border: 0;
47 | box-shadow: none;
48 | }
49 | }
50 |
51 | button[type="reset"] {
52 | animation: fade-in .1s ease-in forwards;
53 | visibility: hidden;
54 | background: none;
55 | border: 0;
56 | border-radius: 50%;
57 | padding: 2px;
58 | right: 0;
59 | stroke-width: 1.4;
60 | }
61 | }
62 |
63 | #searchbar-toggle {
64 | color: var(--tv-color-secondary-txt);
65 | min-width: 16rem;
66 |
67 | kbd {
68 | padding-inline: .5rem;
69 | font-size: $small-font-size;
70 | color: var(--tv-color-secondary-txt);
71 |
72 | &:first-of-type {
73 | border-top-left-radius: 1rem;
74 | border-bottom-left-radius: 1rem;
75 | }
76 |
77 | &:last-of-type {
78 | border-top-right-radius: 1rem;
79 | border-bottom-right-radius: 1rem;
80 | }
81 | }
82 | }
83 |
84 | #search-modal-results .list-group-item {
85 | height: 73px;
86 |
87 | &[aria-selected="true"], &:hover {
88 | background-color: var(--bs-list-group-action-active-bg);
89 |
90 | .search-modal-result-action > .search-modal-result-action-link {
91 | display: flex;
92 | }
93 | .search-modal-result-action > i:not(.search-modal-result-action-link) {
94 | display: none;
95 | }
96 | }
97 | }
98 |
99 | .search-modal-result-action-link {
100 | box-sizing: border-box;
101 |
102 | &:hover {
103 | background: var(--bs-secondary-bg-subtle);
104 | opacity: 80%;
105 | }
106 | }
107 |
108 |
109 | #search-modal-results {
110 | height: 90vh;
111 | }
112 |
113 | .search-modal-result-content {
114 | display: flex;
115 | flex: 1 1 auto;
116 | flex-direction: column;
117 | font-weight: 500;
118 | justify-content: center;
119 | line-height: 1.2em;
120 | margin: 0 8px;
121 | overflow-x: hidden;
122 | position: relative;
123 | white-space: nowrap;
124 | width: 80%;
125 | }
126 |
127 | .search-modal-result-content-title {
128 | font-size: .9em;
129 | }
130 |
131 | .search-modal-result-content-extras {
132 | font-size: 0.75em;
133 | }
134 |
135 | .search-modal-result-action {
136 | display: flex;
137 | justify-content: end;
138 | height: 2rem;
139 |
140 | > .search-modal-result-action-link {
141 | display: none;
142 | }
143 | }
144 |
145 | .search-modal-actions {
146 | list-style: none;
147 | margin: 0;
148 | padding: 0;
149 |
150 | li {
151 | align-items: center;
152 | display: flex;
153 |
154 | &:not(:last-of-type) {
155 | margin-right: 0.8rem
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/teamvault/static/scss/secrets.scss:
--------------------------------------------------------------------------------
1 | @import '@fortawesome/fontawesome-free/scss/functions';
2 | @import '@fortawesome/fontawesome-free/scss/mixins';
3 | @import '@fortawesome/fontawesome-free/scss/variables';
4 |
5 | $circle_size: 314;
6 | $max_progress: 30s;
7 |
8 | .pw-uppercase {
9 | color: #999999
10 | }
11 | .pw-lowercase {
12 | color: var(--bs-body-color);
13 | }
14 | .pw-number {
15 | color: #ff3333;
16 | }
17 | .pw-symbol {
18 | color: #5982ff;
19 | }
20 |
21 | #password-field {
22 | font-family: "Ubuntu Mono", monospace;
23 | text-align: center;
24 | display: block;
25 | }
26 |
27 | .large-type {
28 | --bs-body-color: var(--bs-white);
29 | background-color: black;
30 | display: table;
31 | font-family: "Ubuntu Mono", monospace;
32 | height: 100%;
33 | left: 0;
34 | opacity: 0.9;
35 | padding: 1%;
36 | position: fixed;
37 | text-align: center;
38 | top: 0;
39 | width: 100%;
40 | z-index: 9001;
41 |
42 | > div {
43 | display: table-cell;
44 | vertical-align: middle;
45 | }
46 |
47 | > .lt-otp-countdown {
48 | > #countdown {
49 | background-color: black;
50 | display: flex;
51 | align-items: center;
52 | height: 200px;
53 | width: 200px;
54 | margin: auto;
55 | > svg {
56 | height: 200px;
57 | width: 200px;
58 |
59 | > circle {
60 | stroke-width: 3;
61 | }
62 | }
63 |
64 | > #countdown-number {
65 | color: white;
66 | font-size: 50px;
67 | margin: auto;
68 | }
69 | }
70 | }
71 |
72 | > .lt-otp {
73 | background-color: transparent;
74 |
75 | > .separator {
76 | background-color: var(--bs-accent);
77 | height: 0.25em;
78 | width: 0.25em;
79 | }
80 | }
81 | }
82 |
83 | .secret-attributes {
84 | td {
85 | /* font-size: 16px; */
86 | padding: 0.25rem;
87 |
88 | a {
89 | color: var(--bs-info);
90 | }
91 |
92 | &:first-child {
93 | color: #708999;
94 | padding-right: 3%;
95 | text-align: right;
96 | width: 35%;
97 | }
98 | }
99 |
100 | tr {
101 | &:last-child td {
102 | vertical-align: baseline;
103 | }
104 |
105 | td:last-child button {
106 | margin-left: 0.5rem;
107 | }
108 | }
109 | }
110 |
111 | .secret-meta {
112 | td {
113 | color: var(--bs-body);
114 |
115 | &:first-child {
116 | color: #708999;
117 | padding-left: 3%;
118 | }
119 | }
120 |
121 | > .table > :not(caption) > * > * {
122 | /* Overwrite bootstrap defaults */
123 | padding: 0.3rem 0.3rem;
124 | }
125 | }
126 |
127 | .text-success-bright {
128 | color: #44aa44;
129 | }
130 |
131 | .text-warning-bright {
132 | color: #ffa800;
133 | }
134 |
135 | .text-danger-bright {
136 | color: #bb0000;
137 | }
138 |
139 | .text-muted-bright {
140 | color: #cccccc;
141 | }
142 |
143 | .secret-detail-toggle {
144 | float: right;
145 | color: var(--bs-black);
146 |
147 | &[aria-expanded="true"] {
148 | i {
149 | @include fa-icon-solid($fa-var-minus-square)
150 | }
151 | }
152 |
153 | &[aria-expanded="false"] {
154 | i {
155 | @include fa-icon-solid($fa-var-plus-square)
156 | }
157 | }
158 | }
159 |
160 | .secret-extra-icon {
161 | background-color: var(--bs-border-color);
162 | border-radius: 50%;
163 | border: 5px solid var(--bs-border-color);
164 | margin-left: 0.35rem;
165 | }
166 |
167 | .col-form-label, .form-label {
168 | font-weight: 500;
169 | }
170 |
171 | // custom hover for "generate random secret button"
172 | [data-bs-theme="light"] #id_pwgen:hover {
173 | background: #ddd;
174 | }
175 |
--------------------------------------------------------------------------------
/teamvault/static/scss/select2.scss:
--------------------------------------------------------------------------------
1 | @import "select2/src/scss/core.scss";
2 |
3 | /* See https://github.com/apalfrey/select2-bootstrap-5-theme/issues/75 */
4 | $s2bs5-border-color: $border-color;
5 | @import "select2-bootstrap-5-theme/src/include-all";
6 |
7 | /* Hide selected options in choices */
8 | .select2-results__option[aria-selected="true"] {
9 | display: none;
10 | }
11 |
12 | .select2-container--bootstrap-5 {
13 | .select2-selection {
14 | // Fix misaligned clear button
15 | &--single .select2-selection__clear {
16 | position: absolute;
17 | }
18 |
19 | &__choice__remove {
20 | cursor: pointer;
21 | }
22 |
23 | &.select2-selection--multiple {
24 | padding: 1rem;
25 |
26 | .select2-selection__rendered {
27 | display: flex;
28 | }
29 | }
30 | }
31 | &.select2-container--open .select2-selection {
32 | box-shadow: none;
33 | }
34 |
35 | .select2-results__option--highlighted {
36 | filter: brightness(0.8);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/teamvault/static/scss/theme.scss:
--------------------------------------------------------------------------------
1 | @use "sass:map";
2 |
3 | @import "bootstrap/scss/functions";
4 | @import "bootstrap/scss/variables";
5 |
6 | // disable RFS for more consistent styling
7 | $enable-rfs: false;
8 |
9 | $accent: #f8592c;
10 | $danger: #ff3333;
11 | $custom-dark: #2f2d35;
12 | $info: #289cdd;
13 | $main: #1a1c22;
14 | $success: #13ca5c;
15 | $tertiary: #424242;
16 | $warning: #ffbf3d;
17 |
18 | $custom-colors: (
19 | "accent": $accent,
20 | "danger": $danger,
21 | "custom-dark": $custom-dark,
22 | "info": $info,
23 | "success": $success,
24 | "tertiary": $tertiary,
25 | "warning": $warning,
26 | );
27 |
28 | // Merge the maps
29 | $theme-colors: map.merge($theme-colors, $custom-colors);
30 |
31 | $alert-border-width: 0;
32 | $badge-font-weight: 400;
33 | @import "bootstrap/scss/bootstrap";
34 |
35 | :root {
36 | --tv-color-secondary-txt: rgb(130, 129, 136);
37 | }
38 |
39 | .bg-gray-200 {
40 | background-color: $gray-200;
41 | }
42 |
43 | .fs-xs {
44 | font-size: 0.75rem;
45 | }
46 |
--------------------------------------------------------------------------------
/teamvault/templates/404_anon.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load i18n %}
3 | {% load static %}
4 | {% block navbar %}{% endblock %}
5 | {% block title %}{% trans "Logout" %}{% endblock %}
6 | {% block content %}
7 |
14 | {% endblock %}
15 | {% block footer %}{% endblock %}
16 |
--------------------------------------------------------------------------------
/teamvault/templates/404_loggedin.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 | {% load i18n %}
4 | {% block title %}{% trans "Page not found" %}{% endblock %}
5 | {% block nav_search %}active{% endblock %}
6 | {% block content %}
7 |
8 |
9 |
10 |
{% trans "404 - Page Not Found" %}
11 |
12 |
{% trans "Sorry, the page you requested couldn't be found." %}
13 |
{% trans "If you expected a secret here, you may need to ask someone to grant you access." %}
14 |
15 |
16 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/teamvault/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load render_bundle from webpack_loader %}
3 | {% load webpack_static from webpack_loader %}
4 |
5 | {% load static %}
6 |
7 |
8 |
9 |
10 |
11 |
12 | {% trans "TeamVault" %} · {% block title %}{% endblock %}
13 |
14 |
15 |
16 | {% render_bundle 'main' %}
17 |
18 | {% block head %}{% endblock %}
19 |
20 |
21 |
22 |
34 |
59 |
60 |
61 |
62 | {% block navbar %}{% include 'base_nav.html' %}{% endblock %}
63 |
64 | {% block super_content %}
65 |
66 | {% block content %}{% endblock %}
67 |
68 | {% endblock %}
69 |
70 | {% block footer %}
71 |
72 |
73 | TeamVault {{ version }} · © 2014-2024 Seibert Group
74 |
75 |
76 | {% endblock %}
77 |
78 | {% block additionalJS %}
79 | {% endblock %}
80 |
81 |
82 |
--------------------------------------------------------------------------------
/teamvault/templates/helpers/filter.html:
--------------------------------------------------------------------------------
1 | {% load django_bootstrap5 %}
2 | {% load i18n %}
3 |
4 |
5 |
6 |
8 | {% translate "Filters" %}
9 |
10 |
11 |
12 | {% for field, field_data in active_filters.items %}
13 | {% include 'helpers/filter_row.html' with filter_name=field filter_items=field_data %}
14 | {% endfor %}
15 |
16 |
17 |
40 |
--------------------------------------------------------------------------------
/teamvault/templates/helpers/filter_item.html:
--------------------------------------------------------------------------------
1 |
3 |
4 | {{ filter_item_name }}
5 |
6 |
7 |
--------------------------------------------------------------------------------
/teamvault/templates/helpers/filter_row.html:
--------------------------------------------------------------------------------
1 |
2 |
{{ filter_name|capfirst }}:
3 |
4 | {% for field_item in filter_items %}
5 | {% include "helpers/filter_item.html" with filter_item_name=field_item %}
6 | {% endfor %}
7 |
8 |
--------------------------------------------------------------------------------
/teamvault/templates/pagination.html:
--------------------------------------------------------------------------------
1 | {% load smart_pagination %}
2 | {% if is_paginated %}
3 |
50 | {% endif %}
51 |
--------------------------------------------------------------------------------
/teamvault/templates/rest_framework/api.html:
--------------------------------------------------------------------------------
1 | {% extends "rest_framework/base.html" %}
2 | {% load i18n %}
3 | {% block title %}TeamVault API{% endblock %}
4 | {% block branding %}
5 |
6 | TeamVault {{ version }}
7 |
8 | {% endblock %}
9 |
--------------------------------------------------------------------------------
/teamvault/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import include
2 | from django.urls import path
3 |
4 | handler404 = 'teamvault.views.handler404'
5 |
6 | urlpatterns = (
7 | path('api/', include('teamvault.apps.secrets.api.urls'), name='api'),
8 | path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
9 | path('audit', include('teamvault.apps.audit.urls'), name='audit'),
10 | path('', include('teamvault.apps.secrets.urls'), name='secrets'),
11 | path('', include('teamvault.apps.accounts.urls'), name='accounts'),
12 | path('', include('social_django.urls', namespace='social')),
13 | )
14 |
--------------------------------------------------------------------------------
/teamvault/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from django.views.generic.base import ContextMixin
3 |
4 |
5 | def handler404(request, exception, **kwargs):
6 | if request.user.is_authenticated:
7 | return render(request, "404_loggedin.html", status=404)
8 | else:
9 | return render(request, "404_anon.html", status=404)
10 |
11 |
12 | class FilterMixin(ContextMixin):
13 | _bound_filter = None
14 | filter_class = None
15 | request = None
16 |
17 | def get_filter(self, queryset):
18 | if self.filter_class is None:
19 | raise AttributeError('No filter class specified when using FilterMixin!')
20 |
21 | self._bound_filter = self.filter_class(self.request.GET, queryset)
22 | return self._bound_filter
23 |
24 | def get_filtered_queryset(self, queryset):
25 | return self.get_filter(queryset=queryset).qs
26 |
27 | @staticmethod
28 | def manipulate_filter_form(bound_data, filter_form):
29 | """
30 | Can be overwritten in subclasses to add custom behaviour for a single view
31 | Has to return a filter_form
32 | """
33 | return filter_form
34 |
35 | def get_context_data(self, **kwargs):
36 | context = super().get_context_data(**kwargs)
37 | bound_filter_data = self._bound_filter.form.cleaned_data
38 | new_filter_form = self._bound_filter.get_form_class()()
39 |
40 | active_filters = {}
41 | initial = {}
42 |
43 | for field, field_data in bound_filter_data.items():
44 | if field_data:
45 | field_label = new_filter_form.fields[field].label
46 | initial[field] = field_data
47 |
48 | values = field_data if isinstance(field_data, (list, tuple)) else [field_data]
49 |
50 | if hasattr(new_filter_form.fields[field], 'choices'):
51 | mapping = dict(new_filter_form.fields[field].choices)
52 | converted_values = []
53 | for val in values:
54 | try:
55 | key = int(val)
56 | except (ValueError, TypeError):
57 | key = val
58 | converted_values.append(mapping.get(key, val))
59 | active_filters[field_label] = converted_values
60 | else:
61 | active_filters[field_label] = values
62 |
63 | new_filter_form.initial = initial
64 | new_filter_form = self.manipulate_filter_form(bound_filter_data, new_filter_form)
65 | context.update({
66 | 'active_filters': active_filters,
67 | 'filter_form': new_filter_form,
68 | })
69 | return context
70 |
--------------------------------------------------------------------------------
/teamvault/wsgi.py:
--------------------------------------------------------------------------------
1 | from os import environ
2 |
3 | from django.core.wsgi import get_wsgi_application
4 |
5 |
6 | environ.setdefault("DJANGO_SETTINGS_MODULE", "teamvault.settings")
7 | environ.setdefault("TEAMVAULT_CONFIG_FILE", "/etc/teamvault.cfg")
8 |
9 | application = get_wsgi_application()
10 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const BundleTracker = require('webpack-bundle-tracker');
3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
4 |
5 | module.exports = {
6 | context: __dirname,
7 | entry: './teamvault/static/js/index.js',
8 | output: {
9 | path: path.resolve('./teamvault/static/bundled/'),
10 | filename: "[name]-[fullhash].js",
11 | chunkFilename: "[name]-[fullhash].js"
12 | },
13 | plugins: [
14 | new BundleTracker({path: __dirname + '/teamvault', filename: 'webpack-stats.json'}),
15 | ],
16 | resolve: {
17 | extensions: ['*', '.js']
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.(js|jsx)$/i,
23 | exclude: /node_modules/,
24 | loader: 'babel-loader',
25 | },
26 | {
27 | test: /\.css$/i,
28 | use: ['style-loader', 'css-loader'],
29 | },
30 | {
31 | test: /\.s[ac]ss$/i,
32 | use: [
33 | "style-loader",
34 | "css-loader",
35 | {
36 | loader: "sass-loader",
37 | options: {
38 | sassOptions: {
39 | api: "modern-compiler", // Future default - only use with sass-embedded
40 | quietDeps: true,
41 | silenceDeprecations: [
42 | "import",
43 | ],
44 | },
45 | },
46 | },
47 | ],
48 | },
49 | {
50 | test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
51 | type: 'asset',
52 | },
53 | ],
54 | },
55 | }
56 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const {merge} = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 | const path = require("path");
4 |
5 | module.exports = merge(common, {
6 | mode: 'development',
7 | output: {
8 | publicPath: 'http://localhost:3000/dist/',
9 | },
10 | optimization: {
11 | minimize: false,
12 | usedExports: false,
13 | },
14 | devServer: {
15 | static: path.resolve('./teamvault/static/bundled/'),
16 | hot: true,
17 | port: 3000,
18 | headers: {
19 | "Access-Control-Allow-Origin": "*",
20 | }
21 | },
22 |
23 | // Temporary fix until https://github.com/twbs/bootstrap/pull/39030 is merged
24 | ignoreWarnings: [{
25 | 'message': /Deprecation Passing percentage units to the global abs/,
26 | }]
27 | });
28 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'production',
6 | output: {
7 | publicPath: '/static/bundled/'
8 | },
9 | optimization: {
10 | minimize: true,
11 | usedExports: true,
12 | }
13 | });
14 |
--------------------------------------------------------------------------------