├── .circleci └── config.yml ├── .dockerignore ├── .flake8 ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── crypt.wsgi ├── docker-compose.yml ├── docker ├── Caddyfile ├── README.md ├── django │ └── management │ │ ├── __init__.py │ │ └── commands │ │ ├── __init__.py │ │ └── update_admin_user.py ├── gunicorn_config.py ├── nginx │ ├── crypt.conf │ ├── nginx-env.conf │ └── nginx.conf ├── postgres.sh ├── run.sh ├── run_docker.sh ├── run_docker_postgres.sh ├── settings.py ├── settings_import.py ├── setup_db.sh └── wsgi.py ├── docs ├── Development.md ├── Docker.md ├── Installation_and_upgrade_on_Ubuntu_1804.md ├── Installation_on_CentOS_7.md ├── Installation_on_Ubuntu_1404.md └── images │ ├── admin_computer_info.png │ ├── approve_request.png │ ├── home.png │ ├── key_retrieval.png │ ├── manage_requests.png │ ├── user_computer_info.png │ └── user_key_request.png ├── functional_tests ├── __init__.py ├── base.py └── test_simple_site_functionality.py ├── fvserver ├── __init__.py ├── context_processors.py ├── example_settings.py ├── system_settings.py ├── urls.py ├── version.plist └── wsgi.py ├── generate_keyczart.py ├── manage.py ├── remote_build.py ├── server ├── __init__.py ├── admin.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0001_squashed_0017_merge_20181217_1829.py │ ├── 0002_auto_20150713_1214.py │ ├── 0003_auto_20150713_1215.py │ ├── 0004_auto_20150713_1216.py │ ├── 0005_auto_20150713_1754.py │ ├── 0006_auto_20150714_0821.py │ ├── 0007_auto_20150714_0822.py │ ├── 0008_auto_20150814_2140.py │ ├── 0009_auto_20180430_2024.py │ ├── 0009_secret_rotation_required.py │ ├── 0010_auto_20180726_1700.py │ ├── 0011_manual_unique_serials.py │ ├── 0012_auto_20181128_2038.py │ ├── 0016_auto_20181213_2145.py │ ├── 0017_merge_20181217_1829.py │ ├── 0018_auto_20201029_2134.py │ ├── 0019_alter_request_approved_alter_secret_secret.py │ └── __init__.py ├── models.py ├── templates │ └── server │ │ ├── approve.html │ │ ├── computer_info.html │ │ ├── index.html │ │ ├── manage_requests.html │ │ ├── new_computer_form.html │ │ ├── new_secret_form.html │ │ ├── request.html │ │ ├── retrieve.html │ │ ├── secret_approved_button.html │ │ ├── secret_info.html │ │ └── secret_request_button.html ├── tests.py ├── urls.py └── views.py ├── set_build_no.py ├── setup └── requirements.txt ├── site_static ├── bootstrap │ ├── css │ │ ├── bootstrap-responsive.css │ │ ├── bootstrap-responsive.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js ├── css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── mixins.css │ ├── styles.css │ └── variables.css ├── dataTables │ ├── css │ │ ├── dataTables.bootstrap.css │ │ ├── dataTables.bootstrap.min.css │ │ ├── dataTables.bootstrap4.css │ │ ├── dataTables.bootstrap4.min.css │ │ ├── dataTables.foundation.css │ │ ├── dataTables.foundation.min.css │ │ ├── dataTables.jqueryui.css │ │ ├── dataTables.jqueryui.min.css │ │ ├── dataTables.semanticui.css │ │ ├── dataTables.semanticui.min.css │ │ ├── jquery.dataTables.css │ │ └── jquery.dataTables.min.css │ ├── images │ │ ├── sort_asc.png │ │ ├── sort_asc_disabled.png │ │ ├── sort_both.png │ │ ├── sort_desc.png │ │ └── sort_desc_disabled.png │ └── js │ │ ├── dataTables.bootstrap.js │ │ ├── dataTables.bootstrap.min.js │ │ ├── dataTables.bootstrap4.js │ │ ├── dataTables.bootstrap4.min.js │ │ ├── dataTables.foundation.js │ │ ├── dataTables.foundation.min.js │ │ ├── dataTables.jqueryui.js │ │ ├── dataTables.jqueryui.min.js │ │ ├── dataTables.semanticui.js │ │ ├── dataTables.semanticui.min.js │ │ ├── jquery.dataTables.js │ │ └── jquery.dataTables.min.js ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── img │ ├── atom-head-white.svg │ ├── sal-logo-white.svg │ ├── select2-spinner.gif │ ├── select2.png │ └── select2x2.png ├── js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── less-1.4.2.min.js │ └── main.js └── style.css ├── smtp.sh ├── static └── .gitignore └── templates ├── 404.html ├── 500.html ├── admin └── base_site.html ├── base.html └── registration ├── login.html ├── password_change_done.html └── password_change_form.html /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | test: 4 | docker: 5 | - image: circleci/python:3.10 6 | 7 | working_directory: ~/repo 8 | 9 | steps: 10 | - checkout 11 | 12 | # Download and cache dependencies 13 | - restore_cache: 14 | keys: 15 | - v2-dependencies-{{ checksum "setup/requirements.txt" }} 16 | # fallback to using the latest cache if no exact match is found 17 | - v2-dependencies- 18 | 19 | - run: 20 | name: install dependencies 21 | command: | 22 | python -m venv .venv 23 | . .venv/bin/activate 24 | pip install -r setup/requirements.txt 25 | 26 | - save_cache: 27 | paths: 28 | - .venv 29 | key: v2-dependencies-{{ checksum "setup/requirements.txt" }} 30 | - run: 31 | name: run tests 32 | command: | 33 | . .venv/bin/activate 34 | cp fvserver/example_settings.py fvserver/settings.py 35 | # python manage.py test 36 | python manage.py migrate 37 | - run: 38 | name: run linting 39 | command: | 40 | . .venv/bin/activate 41 | black --check . 42 | 43 | - store_artifacts: 44 | path: test-reports 45 | destination: test-reports 46 | build_latest: 47 | docker: 48 | - image: docker:18.06.1-ce-git 49 | 50 | steps: 51 | - checkout 52 | - setup_remote_docker 53 | - run: docker build -t macadmins/crypt-server:latest . 54 | - run: docker login -u $DOCKER_USER -p $DOCKER_PASS 55 | - run: docker push macadmins/crypt-server:latest 56 | - run: apk add python2 py2-pip 57 | - run: pip install requests 58 | - run: python remote_build.py latest 59 | 60 | build_branch: 61 | docker: 62 | - image: docker:18.06.1-ce-git 63 | 64 | steps: 65 | - checkout 66 | - setup_remote_docker: 67 | docker_layer_caching: true 68 | - run: docker build -t macadmins/crypt-server:$CIRCLE_BRANCH . 69 | - run: docker login -u $DOCKER_USER -p $DOCKER_PASS 70 | - run: docker push macadmins/crypt-server:$CIRCLE_BRANCH 71 | - run: apk update 72 | - run: apk add python2 py2-pip 73 | - run: pip install requests 74 | - run: python remote_build.py $CIRCLE_BRANCH 75 | 76 | build_tag: 77 | docker: 78 | - image: docker:18.06.1-ce-git 79 | 80 | steps: 81 | - checkout 82 | - setup_remote_docker: 83 | docker_layer_caching: true 84 | - run: docker build -t macadmins/crypt-server:$CIRCLE_TAG . 85 | - run: docker login -u $DOCKER_USER -p $DOCKER_PASS 86 | - run: docker push macadmins/crypt-server:$CIRCLE_TAG 87 | - run: apk add python2 py2-pip 88 | - run: pip install requests 89 | - run: python remote_build.py $CIRCLE_TAG 90 | 91 | workflows: 92 | version: 2 93 | build_and_test: 94 | jobs: 95 | - test: 96 | filters: 97 | tags: 98 | only: /.*/ 99 | - build_latest: 100 | requires: 101 | - test 102 | filters: 103 | branches: 104 | only: master 105 | - build_branch: 106 | requires: 107 | - test 108 | filters: 109 | branches: 110 | ignore: master 111 | - build_tag: 112 | requires: 113 | - test 114 | filters: 115 | tags: 116 | only: /.*/ 117 | branches: 118 | ignore: /.*/ 119 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | keyset/ 2 | *.db 3 | *.pyc 4 | crypt.db* -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = F401,F405,E402,F403 3 | max-line-length = 100 4 | max-complexity = 100 5 | exclude = venv/*,*/migrations/* 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pot 3 | *.pyc 4 | local_settings.py 5 | fvserver/settings.py 6 | crypt.db 7 | fvserver.db 8 | keyset 9 | *.db copy 10 | *.db 11 | 12 | .vscode 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/adamchainz/django-upgrade 3 | rev: "1.10.0" 4 | hooks: 5 | - id: django-upgrade 6 | args: [--target-version, "4.1"] 7 | - repo: https://github.com/psf/black 8 | rev: 21.12b0 9 | hooks: 10 | - id: black -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.11-alpine3.16 2 | 3 | LABEL maintainer="graham@grahamgilbert.com" 4 | 5 | ENV APP_DIR /home/docker/crypt 6 | ENV DEBUG false 7 | ENV LANG en 8 | ENV TZ Etc/UTC 9 | ENV LC_ALL en_US.UTF-8 10 | 11 | 12 | 13 | RUN set -ex \ 14 | && apk add --no-cache --virtual .build-deps \ 15 | gcc \ 16 | git \ 17 | openssl-dev \ 18 | build-base \ 19 | libffi-dev \ 20 | libc-dev \ 21 | musl-dev \ 22 | linux-headers \ 23 | pcre-dev \ 24 | postgresql-dev \ 25 | xmlsec-dev \ 26 | tzdata \ 27 | postgresql-libs \ 28 | libpq 29 | 30 | COPY setup/requirements.txt /tmp/requirements.txt 31 | 32 | RUN set -ex \ 33 | && LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pip install --no-cache-dir -r /tmp/requirements.txt" \ 34 | && rm /tmp/requirements.txt 35 | 36 | COPY / $APP_DIR 37 | COPY docker/settings.py $APP_DIR/fvserver/ 38 | COPY docker/settings_import.py $APP_DIR/fvserver/ 39 | COPY docker/gunicorn_config.py $APP_DIR/ 40 | COPY docker/django/management/ $APP_DIR/server/management/ 41 | COPY docker/run.sh /run.sh 42 | 43 | RUN chmod +x /run.sh \ 44 | && mkdir -p /home/app \ 45 | && ln -s ${APP_DIR} /home/app/crypt 46 | 47 | WORKDIR ${APP_DIR} 48 | # don't use this key anywhere else, this is just for collectstatic to run 49 | RUN export FIELD_ENCRYPTION_KEY="jKAv1Sde8m6jCYFnmps0iXkUfAilweNVjbvoebBrDwg="; python manage.py collectstatic --noinput; export FIELD_ENCRYPTION_KEY="" 50 | 51 | EXPOSE 8000 52 | 53 | VOLUME $APP_DIR/keyset 54 | 55 | CMD ["/run.sh"] 56 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2012-2016 Graham Gilbert 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crypt-Server 2 | 3 | **[Crypt][1]** is a tool for securely storing secrets such as FileVault 2 recovery keys. It is made up of a client app, and a Django web app for storing the keys. 4 | 5 | This Docker image contains the fully configured Crypt Django web app. A default admin user has been preconfigured, use admin/password to login. 6 | If you intend on using the server for anything semi-serious it is a good idea to change the password or add a new admin user and delete the default one. 7 | 8 | ## Features 9 | 10 | - Secrets are encrypted in the database 11 | - All access is audited - all reasons for retrieval and approval are logged along side the users performing the actions 12 | - Two step approval for retrieval of secrets is enabled by default 13 | - Approval permission can be given to all users (so just any two users need to approve the retrieval) or a specific group of users 14 | 15 | [1]: https://github.com/grahamgilbert/Crypt 16 | 17 | ## Installation instructions 18 | 19 | It is recommended that you use [Docker](https://github.com/grahamgilbert/Crypt-Server/blob/master/docs/Docker.md) to run this, but if you wish to run directly on a host, installation instructions are over in the [docs directory](https://github.com/grahamgilbert/Crypt-Server/blob/master/docs/Installation_on_Ubuntu_1404.md) 20 | 21 | ### Migrating from versions earlier than Crypt 3.0 22 | 23 | Crypt 3 changed it's encryption backend, so when migrating from versions earlier than Crypt 3.0, you should first run Crypt 3.2.0 to perform the migration, and then upgrade to the latest version. The last version to support legacy migrations was Crypt 3.2. 24 | 25 | ## Settings 26 | 27 | All settings that would be entered into `settings.py` can also be passed into the Docker container as environment variables. 28 | 29 | - `FIELD_ENCRYPTION_KEY` - The key to use when encrypting the secrets. This is required. 30 | 31 | - `SEND_EMAIL` - Crypt Server can send email notifcations when secrets are requested and approved. Set `SEND_EMAIL` to True, and set `HOST_NAME` to your server's host and URL scheme (e.g. `https://crypt.example.com`). For configuring your email settings, see the [Django documentation](https://docs.djangoproject.com/en/3.1/ref/settings/#std:setting-EMAIL_HOST). 32 | 33 | - `EMAIL_SENDER` - The email address to send emaiil notifications from when secrets are requests and approved. Ensure this is verified if you are using SES. Does nothing unless `SEND_EMAIIL` is True. 34 | 35 | - `APPROVE_OWN` - By default, users with approval permissons can approve their own key requests. By setting this to False in settings.py (or by using the `APPROVE_OWN` environment variable with Docker), users cannot approve their own requests. 36 | 37 | - `ALL_APPROVE` - By default, users need to be explicitly given approval permissions to approve key retrieval requests. By setting this to True in `settings.py`, all users are given this permission when they log in. 38 | 39 | - `ROTATE_VIEWED_SECRETS` - With a compatible client (such as Crypt 3.2.0 and greater), Crypt Server can instruct the client to rotate the secret and re-escrow it when the secret has been viewed. Enable by setting this to `True` or by using `ROTATE_VIEWED_SECRETS` and setting to `true`. 40 | 41 | - `HOST_NAME` - Set the host name of your instance - required if you do not have control over the load balancer or proxy in front of your Crypt server (see [the Django documentation](https://docs.djangoproject.com/en/4.1/ref/settings/#csrf-trusted-origins)). 42 | 43 | - `CSRF_TRUSTED_ORIGINS` - Is a list of trusted origins expected to make requests to your Crypt instance, normally this is the hostname 44 | ## Screenshots 45 | 46 | Main Page: 47 | ![Crypt Main Page](https://raw.github.com/grahamgilbert/Crypt-Server/master/docs/images/home.png) 48 | 49 | Computer Info: 50 | ![Computer info](https://raw.github.com/grahamgilbert/Crypt-Server/master/docs/images/admin_computer_info.png) 51 | 52 | User Key Request: 53 | ![Userkey request](https://raw.github.com/grahamgilbert/Crypt-Server/master/docs/images/user_key_request.png) 54 | 55 | Manage Requests: 56 | ![Manage Requests](https://raw.github.com/grahamgilbert/Crypt-Server/master/docs/images/manage_requests.png) 57 | 58 | Approve Request: 59 | ![Approve Request](https://raw.github.com/grahamgilbert/Crypt-Server/master/docs/images/approve_request.png) 60 | 61 | Key Retrieval: 62 | ![Key Retrieval](https://raw.github.com/grahamgilbert/Crypt-Server/master/docs/images/key_retrieval.png) 63 | -------------------------------------------------------------------------------- /crypt.wsgi: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import site 3 | 4 | CRYPT_ENV_DIR = '/usr/local/crypt_env' 5 | 6 | # Use site to load the site-packages directory of our virtualenv 7 | site.addsitedir(os.path.join(CRYPT_ENV_DIR, 'lib/python2.7/site-packages')) 8 | # 9 | # # Make sure we have the virtualenv and the Django app itself added to our path 10 | sys.path.append(CRYPT_ENV_DIR) 11 | sys.path.append(os.path.join(CRYPT_ENV_DIR, 'crypt')) 12 | 13 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fvserver.settings') 14 | 15 | from django.core.wsgi import get_wsgi_application 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # Uncomment here if you want to use Caddy 3 | # caddy: 4 | # image: "wemakeservices/caddy-docker:latest" 5 | # volumes: 6 | # - ./docker/Caddyfile:/etc/Caddyfile # to mount custom Caddyfile 7 | # ports: 8 | # - "80:80" 9 | # # - "443:443" # uncomment this for https. Make sure you edit the Caddyfile above to reflect your hostname 10 | # depends_on: 11 | # - crypt 12 | # restart: always 13 | 14 | crypt: 15 | image: macadmins/crypt-server 16 | # OR "crypt-server" for local build using documentation in /docs/Docker.md 17 | # build: . # uncomment this to build your own image through Docker Compose 18 | environment: 19 | - FIELD_ENCRYPTION_KEY=jKAv1Sde8m6jCYFnmps0iXkUfAilweNVjbvoebBrDwg= # please change this 20 | - ADMIN_PASS=password 21 | - DEBUG=false 22 | ports: 23 | - "8000:8000" 24 | volumes: 25 | - ${PWD}/crypt.db:/home/app/crypt/crypt.db # This will do a local database. For production you should use postgresql 26 | - ${PWD}/fvserver/settings.py:/home/app/crypt/fvserver/settings.py # Load in your own settings file 27 | restart: always 28 | -------------------------------------------------------------------------------- /docker/Caddyfile: -------------------------------------------------------------------------------- 1 | *:80 { 2 | proxy / http://crypt:8000 3 | } -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | __[Crypt][1]__ is a system for centrally storing FileVault 2 recovery keys. It is made up of a client app, and a Django web app for storing the keys. 2 | 3 | This Docker image contains the fully configured Crypt Django web app. A default admin user has been preconfigured, use admin/password to login. 4 | If you intend on using the server for anything semi-serious it is a good idea to change the password or add a new admin user and delete the default one. 5 | 6 | The secrets are encrypted, with the encryption keys stored at ``/home/docker/crypt/keyset``. You should mount this on your host to preserve the keys: 7 | 8 | ``` 9 | -v /somewhere/on/the/host:/home/docker/crypt/keyset 10 | ``` 11 | 12 | __Changes in this version__ 13 | ================= 14 | 15 | - 10.7 is no longer supported. 16 | - Improved logging on errors. 17 | - Improved user feedback during long operations (such as enabling FileVault). 18 | 19 | __Client__ 20 | ==== 21 | The client is written in Pyobjc, and makes use of the built in fdesetup on OS X 10.8 and higher. An example login hook is provided to see how this could be implemented in your organisation. 22 | 23 | __Features__ 24 | ======= 25 | - If escrow fails for some reason, the recovery key is stored on disk and a Launch Daemon will attempt to escrow the key periodically. 26 | - If the app cannot contact the server, it can optionally quit. 27 | - If FileVault is already enabled, the app will quit. 28 | 29 | 30 | [1]: https://github.com/grahamgilbert/Crypt 31 | -------------------------------------------------------------------------------- /docker/django/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/docker/django/management/__init__.py -------------------------------------------------------------------------------- /docker/django/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/docker/django/management/commands/__init__.py -------------------------------------------------------------------------------- /docker/django/management/commands/update_admin_user.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates an admin user if there aren't any existing superusers 3 | """ 4 | 5 | from django.core.management.base import BaseCommand, CommandError 6 | from django.contrib.auth.models import User 7 | from optparse import make_option 8 | 9 | 10 | class Command(BaseCommand): 11 | help = "Creates/Updates an Admin user" 12 | 13 | # option_list = BaseCommand.option_list + ( 14 | # make_option('--username', 15 | # action='store', 16 | # dest='username', 17 | # default=None, 18 | # help='Admin username'), 19 | # ) + ( 20 | # make_option('--password', 21 | # action='store', 22 | # dest='password', 23 | # default=None, 24 | # help='Admin password'), 25 | # ) 26 | def add_arguments(self, parser): 27 | parser.add_argument( 28 | "--username", 29 | action="store", 30 | dest="username", 31 | default=None, 32 | help="Admin username", 33 | ) 34 | 35 | parser.add_argument( 36 | "--password", 37 | action="store", 38 | dest="password", 39 | default=None, 40 | help="Admin password", 41 | ) 42 | 43 | def handle(self, *args, **options): 44 | username = options.get("username") 45 | password = options.get("password") 46 | if not username or not password: 47 | raise StandardError("You must specify a username and password") 48 | # Get the current superusers 49 | su_count = User.objects.filter(is_superuser=True).count() 50 | if su_count == 0: 51 | # there aren't any superusers, create one 52 | user, created = User.objects.get_or_create(username=username) 53 | user.set_password(password) 54 | user.is_staff = True 55 | user.is_superuser = True 56 | user.save() 57 | print("{0} updated".format(username)) 58 | else: 59 | print("There are already {0} superusers".format(su_count)) 60 | -------------------------------------------------------------------------------- /docker/gunicorn_config.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | bind = "0.0.0.0:8000" 4 | workers = multiprocessing.cpu_count() * 2 + 1 5 | errorlog = "-" 6 | accesslog = "-" 7 | -------------------------------------------------------------------------------- /docker/nginx/crypt.conf: -------------------------------------------------------------------------------- 1 | # Crypt.conf: 2 | server { 3 | listen 8000; 4 | server_name crypt.local; 5 | root /home/docker/crypt/static/; 6 | # Redirect requests for static files 7 | location /static/ { 8 | alias /home/docker/crypt/static/; 9 | } 10 | 11 | error_log /var/log/nginx/crypt-error.log warn; 12 | 13 | location / { 14 | proxy_pass http://127.0.0.1:8001; 15 | proxy_set_header X-Forwarded-Host $server_name; 16 | proxy_set_header Host $http_host; 17 | proxy_redirect off; 18 | proxy_set_header X-Real-IP $remote_addr; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"'; 21 | port_in_redirect off; 22 | add_header X-Frame-Options sameorigin; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docker/nginx/nginx-env.conf: -------------------------------------------------------------------------------- 1 | # Environment Variables for settings.py 2 | env DB_NAME; 3 | env DB_USER; 4 | env DB_PASS; 5 | env DB_PORT_5432_TCP_ADDR; 6 | env DB_PORT_5432_TCP_PORT; 7 | env DEBIAN_FRONTEND; 8 | env APP_DIR; 9 | env DOCKER_CRYPT_TZ; 10 | env DOCKER_CRYPT_ADMINS; 11 | env DOCKER_CRYPT_ALLOWED; 12 | env DOCKER_CRYPT_LANG; 13 | env DOCKER_CRYPT_PLUGIN_ORDER; 14 | env DOCKER_CRYPT_DISPLAY_NAME; 15 | env APPNAME; 16 | -------------------------------------------------------------------------------- /docker/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 4; 3 | 4 | events { 5 | worker_connections 19000; 6 | } 7 | 8 | worker_rlimit_nofile 20000; 9 | pid /run/nginx.pid; 10 | daemon off; 11 | 12 | include /etc/nginx/main.d/*.conf; 13 | 14 | http { 15 | 16 | ## 17 | # Basic Settings 18 | ## 19 | 20 | sendfile on; 21 | tcp_nopush on; 22 | tcp_nodelay on; 23 | keepalive_timeout 65; 24 | types_hash_max_size 2048; 25 | # server_tokens off; 26 | 27 | # server_names_hash_bucket_size 64; 28 | # server_name_in_redirect off; 29 | 30 | include /etc/nginx/mime.types; 31 | default_type application/octet-stream; 32 | 33 | ## 34 | # Logging Settings 35 | ## 36 | 37 | access_log /var/log/nginx/access.log; 38 | error_log /var/log/nginx/error.log; 39 | 40 | ## 41 | # Gzip Settings 42 | ## 43 | 44 | gzip on; 45 | gzip_disable "msie6"; 46 | 47 | # gzip_vary on; 48 | # gzip_proxied any; 49 | # gzip_comp_level 6; 50 | # gzip_buffers 16 8k; 51 | # gzip_http_version 1.1; 52 | # gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 53 | 54 | ## 55 | # nginx-naxsi config 56 | ## 57 | # Uncomment it if you installed nginx-naxsi 58 | ## 59 | 60 | # include /etc/nginx/naxsi_core.rules; 61 | 62 | ## 63 | # Phusion Passenger config 64 | ## 65 | # Uncomment it if you installed passenger or passenger-enterprise 66 | ## 67 | 68 | # passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini; 69 | # passenger_ruby /usr/bin/ruby; 70 | 71 | ## 72 | # Virtual Host Configs 73 | ## 74 | 75 | include /etc/nginx/conf.d/*.conf; 76 | include /etc/nginx/sites-enabled/*; 77 | } 78 | 79 | 80 | # mail { 81 | # # See sample authentication script at: 82 | # # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript 83 | # 84 | # # auth_http localhost/auth.php; 85 | # # pop3_capabilities "TOP" "USER"; 86 | # # imap_capabilities "IMAP4rev1" "UIDPLUS"; 87 | # 88 | # server { 89 | # listen localhost:110; 90 | # protocol pop3; 91 | # proxy on; 92 | # } 93 | # 94 | # server { 95 | # listen localhost:143; 96 | # protocol imap; 97 | # proxy on; 98 | # } 99 | # } -------------------------------------------------------------------------------- /docker/postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker rm -f postgres-crypt 4 | 5 | docker run -d --name="postgres-crypt" \ 6 | -e DB_NAME=crypt \ 7 | -e DB_USER=admin \ 8 | -e DB_PASS=password \ 9 | -v /Users/Shared/test-pg-db:/var/lib/postgresql/data \ 10 | -p 5432:5432 \ 11 | grahamgilbert/postgres:9.4.5 12 | 13 | sleep 30 14 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd $APP_DIR 6 | ADMIN_PASS=${ADMIN_PASS:-} 7 | # python3 generate_keyczart.py 8 | python3 manage.py migrate --noinput 9 | 10 | if [ ! -z "$ADMIN_PASS" ] ; then 11 | python3 manage.py update_admin_user --username=admin --password=$ADMIN_PASS 12 | else 13 | python3 manage.py update_admin_user --username=admin --password=password 14 | fi 15 | 16 | 17 | export PYTHONPATH=$PYTHONPATH:$APP_DIR 18 | export DJANGO_SETTINGS_MODULE='fvserver.settings' 19 | 20 | if [ "${DOCKER_CRYPT_DEBUG}" = "true" ] || [ "${DOCKER_CRYPT_DEBUG}" = "True" ] || [ "${DOCKER_CRYPT_DEBUG}" = "TRUE" ] ; then 21 | echo "RUNNING IN DEBUG MODE" 22 | python3 manage.py runserver 0.0.0.0:8000 23 | else 24 | gunicorn -c $APP_DIR/gunicorn_config.py fvserver.wsgi:application 25 | fi 26 | -------------------------------------------------------------------------------- /docker/run_docker.sh: -------------------------------------------------------------------------------- 1 | CWD=`pwd` 2 | docker rm -f crypt 3 | 4 | docker build -t macadmins/crypt . 5 | docker run -d \ 6 | -e ADMIN_PASS=pass \ 7 | -e DEBUG=false \ 8 | -e PROMETHEUS=true \ 9 | --name=crypt \ 10 | --restart="always" \ 11 | -v "$CWD/crypt.db":/home/docker/crypt/crypt.db \ 12 | -e FIELD_ENCRYPTION_KEY=jKAv1Sde8m6jCYFnmps0iXkUfAilweNVjbvoebBrDwg= \ 13 | -p 8000-8050:8000-8050 \ 14 | macadmins/crypt 15 | -------------------------------------------------------------------------------- /docker/run_docker_postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CWD=`pwd` 4 | docker rm -f crypt 5 | docker build -t macadmins/crypt --no-cache . 6 | docker run -d \ 7 | -e ADMIN_PASS=pass \ 8 | -e DEBUG=false \ 9 | -e DB_NAME=crypt \ 10 | -e DB_USER=admin \ 11 | -e DB_PASS=password \ 12 | --name=crypt \ 13 | --link postgres-crypt:db \ 14 | --restart="always" \ 15 | -v "$CWD/keyset":/home/docker/crypt/keyset \ 16 | -e FIELD_ENCRYPTION_KEY=jKAv1Sde8m6jCYFnmps0iXkUfAilweNVjbvoebBrDwg= \ 17 | -p 8000-8050:8000-8050 \ 18 | macadmins/crypt 19 | -------------------------------------------------------------------------------- /docker/settings.py: -------------------------------------------------------------------------------- 1 | from fvserver.system_settings import * 2 | from fvserver.settings_import import * 3 | from django.utils.log import DEFAULT_LOGGING 4 | import os 5 | 6 | 7 | # Django settings for fvserver project. 8 | 9 | DATABASES = { 10 | "default": { 11 | "ENGINE": "django.db.backends.sqlite3", # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 12 | "NAME": os.path.join( 13 | PROJECT_DIR, "crypt.db" 14 | ), # Or path to database file if using sqlite3. 15 | "USER": "", # Not used with sqlite3. 16 | "PASSWORD": "", # Not used with sqlite3. 17 | "HOST": "", # Set to empty string for localhost. Not used with sqlite3. 18 | "PORT": "", # Set to empty string for default. Not used with sqlite3. 19 | } 20 | } 21 | 22 | host = None 23 | port = None 24 | 25 | if "DB_HOST" in os.environ: 26 | host = os.environ.get("DB_HOST") 27 | port = os.environ.get("DB_PORT") 28 | 29 | elif "DB_PORT_5432_TCP_ADDR" in os.environ: 30 | host = os.environ.get("DB_PORT_5432_TCP_ADDR") 31 | port = os.environ.get("DB_PORT_5432_TCP_PORT", "5432") 32 | 33 | if host and port: 34 | DATABASES = { 35 | "default": { 36 | "ENGINE": "django.db.backends.postgresql_psycopg2", 37 | "NAME": os.environ["DB_NAME"], 38 | "USER": os.environ["DB_USER"], 39 | "PASSWORD": os.environ["DB_PASS"], 40 | "HOST": host, 41 | "PORT": port, 42 | } 43 | } 44 | 45 | if "AWS_IAM" in os.environ: 46 | import requests 47 | 48 | cert_bundle_url = ( 49 | "https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem" 50 | ) 51 | cert_target_path = "/etc/ssl/certs/global-bundle.pem" 52 | 53 | response = requests.get(cert_bundle_url) 54 | if response.status_code == 200: 55 | os.makedirs(os.path.dirname(cert_target_path), exist_ok=True) 56 | 57 | with open(cert_target_path, "wb") as file: 58 | file.write(response.content) 59 | print( 60 | f"AWS RDS cert bundle successfully downloaded and saved to {cert_target_path}" 61 | ) 62 | else: 63 | print( 64 | f"Failed to download AWS RDS cert bundle, status code: {response.status_code}" 65 | ) 66 | DATABASES = { 67 | "default": { 68 | "ENGINE": "django_iam_dbauth.aws.postgresql", 69 | "NAME": os.environ["DB_NAME"], 70 | "USER": os.environ["DB_USER"], 71 | "HOST": os.environ["DB_HOST"], 72 | "PORT": os.environ["DB_PORT"], 73 | "OPTIONS": { 74 | "region_name": os.environ["AWS_RDS_REGION"], 75 | "sslmode": "verify-full", 76 | "sslrootcert": "/etc/ssl/certs/global-bundle.pem", 77 | "use_iam_auth": True, 78 | }, 79 | } 80 | } 81 | 82 | # Don't filter anything going to console 83 | DEFAULT_LOGGING["handlers"]["console"]["filters"] = [] 84 | 85 | DEFAULT_LOGGING["loggers"][""] = { 86 | "handlers": ["console"], 87 | "level": "INFO", 88 | "propagate": True, 89 | } 90 | -------------------------------------------------------------------------------- /docker/settings_import.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from os import getenv 3 | import locale 4 | 5 | # Read the DEBUG setting from env var 6 | try: 7 | if getenv("DEBUG").lower() == "true": 8 | DEBUG = True 9 | else: 10 | DEBUG = False 11 | except: 12 | DEBUG = False 13 | 14 | try: 15 | if getenv("APPROVE_OWN").lower() == "false": 16 | APPROVE_OWN = False 17 | else: 18 | APPROVE_OWN = True 19 | except: 20 | APPROVE_OWN = True 21 | 22 | try: 23 | if getenv("ROTATE_VIEWED_SECRETS").lower() == "false": 24 | ROTATE_VIEWED_SECRETS = False 25 | else: 26 | ROTATE_VIEWED_SECRETS = True 27 | except: 28 | ROTATE_VIEWED_SECRETS = True 29 | 30 | try: 31 | if getenv("ALL_APPROVE").lower() == "true": 32 | ALL_APPROVE = True 33 | else: 34 | ALL_APPROVE = False 35 | except: 36 | ALL_APPROVE = False 37 | 38 | # Read list of admins from $ADMINS env var 39 | admin_list = [] 40 | if getenv("ADMINS"): 41 | admins_var = getenv("ADMINS") 42 | if "," in admins_var and ":" in admins_var: 43 | for admin in admins_var.split(":"): 44 | admin_list.append(tuple(admin.split(","))) 45 | ADMINS = admin_list 46 | elif "," in admins_var: 47 | admin_list.append(tuple(admins_var.split(","))) 48 | ADMINS = admin_list 49 | else: 50 | ADMINS = [("Admin User", "admin@test.com")] 51 | 52 | # Read the preferred time zone from $TZ, use system locale or 53 | # set to 'America/New_York' if neither are set 54 | if getenv("TZ"): 55 | if "/" in getenv("TZ"): 56 | TIME_ZONE = getenv("TZ") 57 | else: 58 | TIME_ZONE = "America/New_York" 59 | elif getenv("TZ"): 60 | TIME_ZONE = getenv("TZ") 61 | else: 62 | TIME_ZONE = "America/New_York" 63 | 64 | # Read the preferred language code from $LANG & default to en-us if not set 65 | # note django does not support locale-format for LANG 66 | if getenv("LANG"): 67 | LANGUAGE_CODE = getenv("LANG") 68 | else: 69 | LANGUAGE_CODE = "en-us" 70 | 71 | # Set the display name from the $DISPLAY_NAME env var, or 72 | # use the default 73 | if getenv("DISPLAY_NAME"): 74 | DISPLAY_NAME = getenv("DISPLAY_NAME") 75 | else: 76 | DISPLAY_NAME = "Crypt" 77 | 78 | if getenv("EMAIL_HOST"): 79 | EMAIL_HOST = getenv("EMAIL_HOST") 80 | 81 | if getenv("EMAIL_PORT"): 82 | EMAIL_PORT = getenv("EMAIL_PORT") 83 | 84 | if getenv("EMAIL_USER"): 85 | EMAIL_USER = getenv("EMAIL_USER") 86 | 87 | if getenv("EMAIL_PASSWORD"): 88 | EMAIL_PASSWORD = getenv("EMAIL_PASSWORD") 89 | 90 | if getenv("CSRF_TRUSTED_ORIGINS"): 91 | CSRF_TRUSTED_ORIGINS = getenv("CSRF_TRUSTED_ORIGINS").split(",") 92 | else: 93 | CSRF_TRUSTED_ORIGINS = [] 94 | 95 | if getenv("HOST_NAME"): 96 | HOST_NAME = getenv("HOST_NAME") 97 | else: 98 | HOST_NAME = "https://cryptexample.com" 99 | 100 | if getenv("EMAIL_SENDER"): 101 | EMAIL_SENDER = getenv("EMAIL_SENDER") 102 | else: 103 | EMAIL_SENDER = "crypt@cryptexample.com" 104 | 105 | # Read the list of allowed hosts from the $DOCKER_CRYPT_ALLOWED env var, or 106 | # allow all hosts if none was set. 107 | if getenv("ALLOWED_HOSTS"): 108 | ALLOWED_HOSTS = getenv("ALLOWED_HOSTS").split(",") 109 | else: 110 | ALLOWED_HOSTS = ["*"] 111 | 112 | if getenv("SEND_EMAIL") and getenv("SEND_EMAIL").lower() == "true": 113 | SEND_EMAIL = True 114 | else: 115 | SEND_EMAIL = False 116 | 117 | if getenv("EMAIL_USE_TLS") and getenv("EMAIL_USE_TLS").lower() == "true": 118 | EMAIL_USE_TLS = True 119 | 120 | if getenv("EMAIL_USE_SSL") and getenv("EMAIL_USE_SSL").lower() == "true": 121 | EMAIL_USE_SSL = True 122 | -------------------------------------------------------------------------------- /docker/setup_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | DB_NAME=crypt 3 | DB_USER=admin 4 | DB_PASS=password 5 | 6 | echo "CREATE ROLE $DB_USER WITH LOGIN ENCRYPTED PASSWORD '${DB_PASS}' CREATEDB;" | docker run \ 7 | --rm \ 8 | --interactive \ 9 | --link postgres-crypt:postgres \ 10 | postgres:9.3.4 \ 11 | bash -c 'exec psql -h "$POSTGRES_PORT_5432_TCP_ADDR" -p "$POSTGRES_PORT_5432_TCP_PORT" -U postgres' 12 | 13 | echo "CREATE DATABASE $DB_NAME WITH OWNER $DB_USER TEMPLATE template0 ENCODING 'UTF8';" | docker run \ 14 | --rm \ 15 | --interactive \ 16 | --link postgres-crypt:postgres \ 17 | postgres:9.3.4 \ 18 | bash -c 'exec psql -h "$POSTGRES_PORT_5432_TCP_ADDR" -p "$POSTGRES_PORT_5432_TCP_PORT" -U postgres' 19 | 20 | echo "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;" | docker run \ 21 | --rm \ 22 | --interactive \ 23 | --link postgres-crypt:postgres \ 24 | postgres:9.3.4 \ 25 | bash -c 'exec psql -h "$POSTGRES_PORT_5432_TCP_ADDR" -p "$POSTGRES_PORT_5432_TCP_PORT" -U postgres' 26 | 27 | 28 | -------------------------------------------------------------------------------- /docker/wsgi.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import site 3 | 4 | CRYPT_ENV_DIR = "/home/docker/crypt" 5 | 6 | # Use site to load the site-packages directory of our virtualenv 7 | site.addsitedir(os.path.join(CRYPT_ENV_DIR, "lib/python2.7/site-packages")) 8 | 9 | # Make sure we have the virtualenv and the Django app itself added to our path 10 | sys.path.append(CRYPT_ENV_DIR) 11 | sys.path.append(os.path.join(CRYPT_ENV_DIR, "fvserver")) 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fvserver.settings") 13 | from django.core.wsgi import get_wsgi_application 14 | 15 | application = get_wsgi_application() 16 | -------------------------------------------------------------------------------- /docs/Development.md: -------------------------------------------------------------------------------- 1 | ## Git setup 2 | 3 | Add the following to `.git/hooks/pre-commit` and make executable 4 | 5 | ``` 6 | #!/bin/bash 7 | ROOT=`git rev-parse --show-toplevel` 8 | $ROOT/set_build_no.py 9 | git add fvserver/version.plist 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/Docker.md: -------------------------------------------------------------------------------- 1 | # Using Docker 2 | 3 | ## Server Initialization 4 | This was last tested on Ubuntu 24.04 x86. This process may need to be modified for older installations. 5 | 6 | ``` bash 7 | git clone https://github.com/grahamgilbert/Crypt-Server.git 8 | ``` 9 | 10 | Install Docker and Docker Compose plugin following instructions here: 11 | ``` bash 12 | https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository 13 | ``` 14 | 15 | Restart the Docker services 16 | ``` bash 17 | sudo systemctl restart docker 18 | ``` 19 | 20 | Ensure docker permissions are set. Log out then back in after running this command: 21 | ``` bash 22 | sudo usermod -aG docker $USER 23 | ``` 24 | 25 | ## Prepare for first use 26 | When starting from scratch, create a new empty file on the docker host to hold the sqlite3 secrets database 27 | ``` bash 28 | touch /somewhere/else/on/the/host 29 | ``` 30 | 31 | ## Basic usage 32 | ``` bash 33 | docker run -d --name="Crypt" \ 34 | --restart="always" \ 35 | -v /somewhere/else/on/the/host:/home/docker/crypt/crypt.db \ 36 | -e FIELD_ENCRYPTION_KEY='yourencryptionkey' \ 37 | -p 8000:8000 \ 38 | macadmins/crypt-server 39 | ``` 40 | 41 | ## Verify Operation 42 | ``` bash 43 | docker logs Crypt 44 | ``` 45 | 46 | ## Upgrading from Crypt Server 2 47 | 48 | The encryption method has changed in Crypt Server. You should pass in both your old encryption keys (e.g. `-v /somewhere/on/the/host:/home/docker/crypt/keyset`) and the new one (see below) for the first run to migrate your keys. After the migration you no longer need your old encryption keys. Crypt 3 is a major update, you should ensure any custom settings you pass are still valid. 49 | 50 | 51 | 52 | The secrets are encrypted, with the encryption key passed in as an environment variable. You should back this up as the keys are not recoverable without them. 53 | 54 | ### Generating an encryption key 55 | 56 | Run the following command to generate an encryption key (you should specify the string only): 57 | 58 | ``` 59 | docker run --rm -ti macadmins/crypt-server \ 60 | python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key())" 61 | ``` 62 | 63 | ## Backing up the database with a data dump 64 | ``` bash 65 | docker exec -it Crypt bash 66 | cd /home/docker/crypt/ 67 | python manage.py dumpdata > db.json 68 | exit 69 | docker cp Crypt:/home/docker/crypt/db.json . 70 | ``` 71 | Optionally 72 | ``` bash 73 | docker exec -it Crypt bash 74 | rm /home/docker/crypt/db.json 75 | exit 76 | ``` 77 | 78 | ## Using Postgres as an external database 79 | 80 | Crypt, by default, uses a sqlite3 database for the django db backend. Crypt also supports using Postgres as the django db backend. If you would like to use an external Postgres server, you need to set the following environment variables: 81 | 82 | ``` 83 | docker run -d --name="Crypt" \ 84 | --restart="always" \ 85 | -p 8000:8000 \ 86 | -e DB_HOST='db.example.com' \ 87 | -e DB_PORT='5432' \ 88 | -e DB_NAME='postgres_dbname' \ 89 | -e DB_USER='postgres_user' \ 90 | -e DB_PASS='postgres_user_pass' \ 91 | -e FIELD_ENCRYPTION_KEY='yourencryptionkey' \ 92 | -e CSRF_TRUSTED_ORIGINS='https://FirstServer.com,https://SecondServer.com' \ 93 | macadmins/crypt-server 94 | ``` 95 | 96 | ## Emails 97 | 98 | If you would like Crypt to send emails when keys are requested and approved, you should set the following environment variables: 99 | 100 | ``` 101 | docker run -d --name="Crypt" \ 102 | --restart="always" \ 103 | -v /somewhere/on/the/host:/home/docker/crypt/keyset \ 104 | -v /somewhere/else/on/the/host:/home/docker/crypt/crypt.db \ 105 | -p 8000:8000 \ 106 | -e EMAIL_HOST='mail.yourdomain.com' \ 107 | -e EMAIL_PORT='25' \ 108 | -e EMAIL_USER='youruser' \ 109 | -e EMAIL_PASSWORD='yourpassword' \ 110 | -e HOST_NAME='https://crypt.myorg.com' \ 111 | -e FIELD_ENCRYPTION_KEY='yourencryptionkey' \ 112 | -e CSRF_TRUSTED_ORIGINS='https://FirstServer.com,https://SecondServer.com' \ 113 | macadmins/crypt-server 114 | ``` 115 | 116 | If your SMTP server doesn't need a setting (username and password for example), you should omit it. The `HOST_NAME` setting should be the hostname of your server - this will be used to generate links in emails. 117 | 118 | ## SSL 119 | 120 | It is recommended to use either an Nginx proxy in front of the Crypt app for SSL termination (outside of the scope of this document, see [here](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-18-04) and [here](https://www.linode.com/docs/web-servers/nginx/use-nginx-reverse-proxy/) for more information), or to use Caddy. Caddy will also handle setting up letsencrypt SSL certificates for you. An example Caddyfile is included in `docker/Caddyfile`. Using Crypt without SSL __will__ result in your secrets being compromised. 121 | 122 | _Note Caddy is only free for personal use. For commercial deployments you should build from source yourself or use Nginx._ 123 | 124 | ## X-Frame-Options 125 | 126 | The nginx config included with the docker container configures the X-Frame-Options as sameorigin. This protects against a potential attacker using iframes to do bad stuff with Crypt. 127 | 128 | Depending on your environment you may need to also configure X-Frame-Options on any proxies in front of Crypt. 129 | 130 | ## docker-compose 131 | 132 | An example `docker-compose.yml` is included. For basic usuage, you should only need to edit the `FIELD_ENCRYPTION_KEY`. 133 | -------------------------------------------------------------------------------- /docs/Installation_on_CentOS_7.md: -------------------------------------------------------------------------------- 1 | # Installation on CentOS 7 2 | 3 | This document has not been updated for several years and should only be used for version 2 of Crypt server. Pull requests to update this are gratefully accepted. 4 | 5 | All commands should be run as root, unless specified. 6 | 7 | ## Install Prerequisites 8 | 9 | ### Setup and Virtual Environment 10 | 11 | Install needed packages: 12 | 13 | `yum install git python-setuptools gcc libffi-devel python-devel openssl-devel 14 | postgresql-libs postgresql-devel` 15 | 16 | Check if `virtualenv` is installed via `virtualenv --version` and install it if 17 | needed: 18 | 19 | `easy_install virtualenv` 20 | 21 | ### Create a non-admin service account and group 22 | 23 | Create a new group: 24 | 25 | `groupadd cryptgroup` 26 | 27 | and add a new user in the cryptgroup with a home directory: 28 | 29 | `useradd -g cryptgroup -m cryptuser` 30 | 31 | ### Create the virtual environment 32 | 33 | When a virtualenv is created, pip will also be installed to manage a 34 | virtualenv's local packages. Create a virtualenv which will handle installing 35 | Django in a contained environment. In this example we'll create a virtualenv for 36 | Crypt at /usr/local. This should be run from Bash, as this is what the 37 | virtualenv activate script expects. 38 | 39 | Switch to bash if needed: `/usr/bin/bash` and get into the local folder: 40 | 41 | `cd /usr/local` 42 | 43 | Create the virtialenv for Crypt `virtualenv crypt_env` and change folder 44 | permissions: `chown -R cryptuser:cryptgroup crypt_env`. 45 | 46 | Switch to the newly created service account `su cryptuser` and make sure to use 47 | the bash shell: `bash`. 48 | 49 | Now let's activate the virtualenv: 50 | 51 | ``` 52 | cd crypt_env 53 | source bin/activate 54 | ``` 55 | 56 | ### Copy and configure Crypt 57 | 58 | Still inside the crypt_env virtualenv, use git to clone the current version of 59 | Crypt-Server: 60 | 61 | `git clone https://github.com/grahamgilbert/Crypt-Server.git crypt` 62 | 63 | 64 | We could also get the 1.6.8 version via git without touching 65 | the `requirements.txt`-file: `pip install git+https://github.com/django-extensions/django-extensions@243abe93451c3b53a5f562023afcd809b79c9b7f`. 66 | 67 | Also install these aditional packages: 68 | 69 | ``` 70 | pip install psycopg2==2.5.3 71 | pip install gunicorn 72 | pip install setproctitle 73 | ``` 74 | 75 | Now we need to get the other missing components for Crypt via pip: 76 | 77 | `pip install -r crypt/setup/requirements.txt` 78 | 79 | Now we need to generate some encryption keys (dont forget to change directory!): 80 | 81 | ``` 82 | cd crypt 83 | python ./generate_keyczart.py 84 | ``` 85 | 86 | Next we need to make a copy of the example_settings.py file and put in your 87 | info: 88 | 89 | ``` 90 | cd fvserver 91 | cp example_settings.py settings.py 92 | vim settings.py 93 | ``` 94 | 95 | Atleast change the following: 96 | - Set ADMINS to an administrative name and email 97 | - Set TIME_ZONE to the appropriate timezone 98 | - Change ALLOWED_HOSTS to be a list of hosts that the server will be accessible 99 | from. 100 | - Take a look at the `DATABASES` and email settings. 101 | 102 | ### DB Setup 103 | 104 | We need to use Django's manage.py to initialise the app's database and create an 105 | admin user. Running the syncdb command will ask you to create an admin user - 106 | make sure you do this! 107 | 108 | ``` 109 | cd .. 110 | python manage.py syncdb 111 | python manage.py migrate 112 | ``` 113 | 114 | If you used an external DB like Postgres you dont need to run `pyton manage.py syncdb`. 115 | 116 | And stage the static files (type yes when prompted): 117 | 118 | ``` 119 | python manage.py collectstatic 120 | ``` 121 | 122 | Also create a new superuser to auth on the webinterface: 123 | 124 | ``` 125 | python manage.py createsuperuser --username $USERNAME 126 | ``` 127 | 128 | ## Set up an Apache virtualhost 129 | 130 | Exit out of the virtualenv and also switch back to root user. After that install 131 | the Apache Modification `mod_wsgi`: `yum install mod_wsgi`. 132 | 133 | Create the wsgi directory and give the cryptuser the needed rights: 134 | 135 | Create a new VirtualHost `vim /etc/httpd/conf.d/crypt.conf`: 136 | 137 | ``` 138 | 139 | ServerName crypt.yourdomain.com 140 | WSGIScriptAlias / /home/app/crypt_env/crypt/crypt.wsgi 141 | WSGIDaemonProcess cryptuser user=cryptuser group=cryptgroup 142 | Alias /static/ /home/app/crypt_env/crypt/static/ 143 | SSLEngine on 144 | SSLCertificateFile "/etc/puppetlabs/puppet/ssl/certs/cryptserver.yourdomain.com.pem" 145 | SSLCertificateKeyFile "/etc/puppetlabs/puppet/ssl/private_keys/cryptserver.yourdomain.com.pem" 146 | SSLCACertificatePath "/etc/puppetlabs/puppet/ssl/certs" 147 | SSLCACertificateFile "/etc/puppetlabs/puppet/ssl/certs/ca.pem" 148 | SSLCARevocationFile "/etc/puppetlabs/puppet/ssl/crl.pem" 149 | SSLProtocol +TLSv1 150 | SSLCipherSuite ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA 151 | SSLHonorCipherOrder On 152 | 153 | WSGIProcessGroup cryptuser 154 | WSGIApplicationGroup %{GLOBAL} 155 | Require all granted 156 | 157 | 158 | WSGISocketPrefix /var/run/wsgi 159 | WSGIPythonHome /home/app/crypt_env 160 | ``` 161 | 162 | ### Configure SELinux to work with Apache 163 | 164 | On CentOS SELinux is activated and needs to be configured so Apache can do it's work: 165 | 166 | ``` 167 | yum install -y policycoreutils-python 168 | semanage fcontext -a -t httpd_sys_content_t "/usr/local/crypt_env/crypt(/.*)?" 169 | semanage fcontext -a -t httpd_sys_rw_content_t "/usr/local/crypt_env/crypt(/.*)?" 170 | setsebool -P httpd_can_sendmail on 171 | setsebool -P httpd_can_network_connect_db on 172 | restorecon -Rv /usr/local/crypt_env/crypt 173 | ``` 174 | 175 | If you enabled SSL also grant access to the key files: 176 | 177 | ``` 178 | semanage fcontext -a -t httpd_sys_rw_content_t "/etc/pki/tls/private/KEY.key" 179 | restorecon -Rv /etc/pki/tls/private/KEY.key 180 | ``` 181 | 182 | ### Open needed ports in the firewall 183 | 184 | ``` 185 | firewall-cmd --zone=public --add-service=https --permanent 186 | firewall-cmd --reload 187 | ``` 188 | 189 | ### Activate Apache and start the httpd-server 190 | 191 | ``` 192 | systemctl enable httpd 193 | systemctl start httpd 194 | ``` 195 | -------------------------------------------------------------------------------- /docs/Installation_on_Ubuntu_1404.md: -------------------------------------------------------------------------------- 1 | Installation on Ubuntu 14.04 LTS 2 | ===================== 3 | This document assumes a bare install of Ubuntu 14.04 LTS server. This document has not been updated for Crypt Server 3.0. Pull requests to update this (and to update to Ubuntu 18) are gratefully accepted. 4 | 5 | All commands should be run as root, unless specified 6 | 7 | ##Install Prerequisites 8 | ###Install Apache and the Apache modules 9 | 10 | apt-get install apache2 libapache2-mod-wsgi 11 | 12 | ###Install GCC (Needed for the encryption library) 13 | 14 | apt-get install gcc 15 | 16 | ###Install git 17 | 18 | apt-get install git 19 | 20 | ###Install the python C headers (so you can compile the encryption library) 21 | 22 | apt-get install python-dev 23 | 24 | ###If you want to use MySQL, you the following 25 | 26 | apt-get install libmysqlclient-dev python-mysqldb mysql-client 27 | 28 | ###Install the python dev tools 29 | 30 | apt-get install python-setuptools 31 | 32 | ###Verify virtual env is installed 33 | 34 | virtualenv --version 35 | 36 | ###If is isn't, install it with 37 | 38 | easy_install virtualenv 39 | 40 | ##Create a non-admin service account and group 41 | Create the Crypt user: 42 | 43 | useradd -d /usr/local/crypt_env cryptuser 44 | 45 | Create the Crypt group: 46 | 47 | groupadd cryptgroup 48 | 49 | Add cryptuser to the cryptgroup group: 50 | 51 | usermod -g cryptgroup cryptuser 52 | 53 | ##Create the virtual environment 54 | When a virtualenv is created, pip will also be installed to manage a 55 | virtualenv's local packages. Create a virtualenv which will handle 56 | installing Django in a contained environment. In this example we'll 57 | create a virtualenv for Crypt at /usr/local. This should be run from 58 | Bash, as this is what the virtualenv activate script expects. 59 | 60 | Go to where we're going to install the virtualenv: 61 | 62 | cd /usr/local 63 | 64 | Create the virtualenv for Crypt: 65 | 66 | virtualenv crypt_env 67 | 68 | Make sure cryptuser has permissions to the new crypt_env folder: 69 | 70 | chown -R cryptuser crypt_env 71 | 72 | Switch to the service account: 73 | 74 | su cryptuser 75 | 76 | Virtualenv needs to be run from a bash prompt, so let's switch to one: 77 | 78 | bash 79 | 80 | Now we can activate the virtualenv: 81 | 82 | cd crypt_env 83 | source bin/activate 84 | 85 | ##Install and configure Crypt 86 | Still inside the crypt_env virtualenv, use git to clone the current 87 | version of Crypt-Server 88 | 89 | git clone https://github.com/grahamgilbert/Crypt-Server.git crypt 90 | 91 | Now we need to get the other components for Crypt 92 | 93 | pip install -r crypt/setup/requirements.txt 94 | 95 | Now we need to generate some encryption keys (make sure these go in crypt/keyset): 96 | 97 | cd crypt 98 | python ./generate_keyczart.py 99 | 100 | Next we need to make a copy of the example_settings.py file and put 101 | in your info: 102 | 103 | cd ./fvserver 104 | cp example_settings.py settings.py 105 | 106 | Edit settings.py: 107 | 108 | * Set ADMINS to an administrative name and email 109 | * Set TIME_ZONE to the appropriate timezone 110 | * Change ALLOWED_HOSTS to be a list of hosts that the server will be 111 | accessible from (e.g. ``ALLOWED_HOSTS=['crypt.grahamgilbert.dev']`` 112 | 113 | If you wish to use email notifications, add the following to your settings.py: 114 | 115 | ``` python 116 | # This is the host and port you are sending email on 117 | EMAIL_HOST = 'localhost' 118 | EMAIL_PORT = '25' 119 | 120 | # If your email server requires Authentication 121 | EMAIL_HOST_USER = 'youruser' 122 | EMAIL_HOST_PASSWORD = 'yourpassword' 123 | # This is the URL at the front of any links in the emails 124 | HOST_NAME = 'http://localhost' 125 | ``` 126 | 127 | ## Using with MySQL 128 | In order to use Crypt-Server with MySQL, you need to configure it to connect to 129 | a MySQL server instead of the default sqlite3. To do this, locate the DATABASES 130 | section of settings.py, and change ENGINE to 'django.db.backends.mysql'. Set the 131 | NAME as the database name, USER and PASSWORD to your user and password, and 132 | either leave HOST as blank for localhost, or insert an IP or hostname of your 133 | MySQL server. You will also need to install the correct python and apt packages. 134 | 135 | apt-get install libmysqlclient-dev python-dev mysql-client 136 | pip install mysql-python 137 | 138 | 139 | ## More Setup 140 | We need to use Django's manage.py to initialise the app's database and 141 | create an admin user. Running the syncdb command will ask you to create 142 | an admin user - make sure you do this! 143 | 144 | cd .. 145 | python manage.py syncdb 146 | python manage.py migrate 147 | 148 | Stage the static files (type yes when prompted) 149 | 150 | python manage.py collectstatic 151 | 152 | ##Installing mod_wsgi and configuring Apache 153 | To run Crypt in a production environment, we need to set up a suitable 154 | webserver. Make sure you exit out of the crypt_env virtualenv and the 155 | cryptuser user (back to root) before continuing). 156 | 157 | ##Set up an Apache virtualhost 158 | You will probably need to edit most of these bits to suit your 159 | environment, especially to add SSL encryption. There are many different 160 | options, especially if you prefer nginx, the below example is for apache 161 | with an internal puppet CA. Make a new file at 162 | /etc/apache2/sites-available (call it whatever you want) 163 | 164 | vim /etc/apache2/sites-available/crypt.conf 165 | 166 | And then enter something like: 167 | 168 | 169 | ServerName crypt.yourdomain.com 170 | WSGIScriptAlias / /usr/local/crypt_env/crypt/crypt.wsgi 171 | WSGIDaemonProcess cryptuser user=cryptuser group=cryptgroup 172 | Alias /static/ /usr/local/crypt_env/crypt/static/ 173 | SSLEngine on 174 | SSLCertificateFile "/etc/puppetlabs/puppet/ssl/certs/cryptserver.yourdomain.com.pem" 175 | SSLCertificateKeyFile "/etc/puppetlabs/puppet/ssl/private_keys/cryptserver.yourdomain.com.pem" 176 | SSLCACertificatePath "/etc/puppetlabs/puppet/ssl/certs" 177 | SSLCACertificateFile "/etc/puppetlabs/puppet/ssl/certs/ca.pem" 178 | SSLCARevocationFile "/etc/puppetlabs/puppet/ssl/crl.pem" 179 | SSLProtocol +TLSv1 180 | SSLCipherSuite ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA 181 | SSLHonorCipherOrder On 182 | 183 | WSGIProcessGroup cryptuser 184 | WSGIApplicationGroup %{GLOBAL} 185 | Options FollowSymLinks 186 | AllowOverride None 187 | Require all granted 188 | 189 | 190 | WSGISocketPrefix /var/run/wsgi 191 | WSGIPythonHome /usr/local/crypt_env 192 | 193 | Now we just need to enable our site, and then your can go and configure 194 | your clients: 195 | 196 | a2ensite crypt.conf 197 | service apache2 reload 198 | -------------------------------------------------------------------------------- /docs/images/admin_computer_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/docs/images/admin_computer_info.png -------------------------------------------------------------------------------- /docs/images/approve_request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/docs/images/approve_request.png -------------------------------------------------------------------------------- /docs/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/docs/images/home.png -------------------------------------------------------------------------------- /docs/images/key_retrieval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/docs/images/key_retrieval.png -------------------------------------------------------------------------------- /docs/images/manage_requests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/docs/images/manage_requests.png -------------------------------------------------------------------------------- /docs/images/user_computer_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/docs/images/user_computer_info.png -------------------------------------------------------------------------------- /docs/images/user_key_request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/docs/images/user_key_request.png -------------------------------------------------------------------------------- /functional_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/functional_tests/__init__.py -------------------------------------------------------------------------------- /functional_tests/base.py: -------------------------------------------------------------------------------- 1 | from django.contrib.staticfiles.testing import StaticLiveServerTestCase 2 | from django.contrib.auth.models import User 3 | from selenium import webdriver 4 | from server.models import Computer, Secret 5 | from datetime import datetime 6 | 7 | 8 | class FunctionalTest(StaticLiveServerTestCase): 9 | def setUp(self): 10 | self.browser = webdriver.Firefox() 11 | User.objects.create_superuser("admin", "a@a.com", "sekrit") 12 | User.objects._create_user( 13 | "tech", "t@a.com", "password", is_staff=True, is_superuser=False 14 | ) 15 | tech_test_computer = Computer(serial="TECHSERIAL") 16 | tech_test_computer.username = "Daft Tech" 17 | tech_test_computer.computername = "compy587" 18 | tech_test_computer.save() 19 | secret = Secret( 20 | computer=tech_test_computer, 21 | secret="SHHH-DONT-TELL", 22 | date_escrowed=datetime.now(), 23 | ) 24 | secret.save() 25 | 26 | def tearDown(self): 27 | self.browser.quit() 28 | 29 | # currently doesn't work to find entered elements 30 | # def check_for_row_in_list_table(self, row_value): 31 | # table = self.browser.find_element_by_id('id_list_table') 32 | # rows = table.find_elements_by_tag_name('tr') 33 | # value = rows.find_elements_by_tag_name('td') 34 | # self.assertIn(row_value, [value.text for row in rows]) 35 | -------------------------------------------------------------------------------- /functional_tests/test_simple_site_functionality.py: -------------------------------------------------------------------------------- 1 | import time 2 | from .base import FunctionalTest 3 | from selenium.webdriver.common.keys import Keys 4 | 5 | 6 | class LoginAndBasicFunctionality(FunctionalTest): 7 | def test_admin_can_create_and_browse(self): 8 | # Admin goes to fv2 key mgmt site, sees it's named Crypt post-redirect to a login 9 | self.browser.get(self.live_server_url) 10 | self.assertIn("Crypt", self.browser.title) 11 | username_box = self.browser.find_element_by_id("id_username") 12 | password_box = self.browser.find_element_by_id("id_password") 13 | username_box.send_keys("admin") 14 | password_box.send_keys("sekrit") 15 | password_box.send_keys(Keys.ENTER) 16 | time.sleep(1) 17 | # After putting in creds, admin can create a computer from the hamburger menu, and is redirected to details 18 | self.browser.find_element_by_id("dLabel").click() 19 | self.browser.find_element_by_link_text("New computer").click() 20 | inputbox = self.browser.find_element_by_id("id_serial") 21 | self.assertEqual(inputbox.get_attribute("placeholder"), "Serial Number") 22 | inputbox.send_keys("MYSERIAL") 23 | username = self.browser.find_element_by_id("id_username") 24 | username.send_keys("Mr. Admin") 25 | computername = self.browser.find_element_by_id("id_computername") 26 | computername.send_keys("compy486") 27 | self.browser.find_element_by_css_selector("button.btn.btn-primary").click() 28 | detail_url = self.browser.current_url 29 | self.assertRegexpMatches(detail_url, "/info/.+") 30 | # When viewing details of computer, admin can create a secret for it 31 | self.browser.find_element_by_class_name("dropdown-toggle").click() 32 | self.browser.find_element_by_css_selector( 33 | "span.glyphicon.glyphicon-plus" 34 | ).click() 35 | secretbox = self.browser.find_element_by_name("secret") 36 | self.assertEqual(secretbox.get_attribute("placeholder"), "Secret") 37 | secretbox.send_keys("LICE-NSEP-LATE") 38 | self.browser.find_element_by_css_selector("button.btn.btn-primary").click() 39 | # The newly created secret shows up on the page, and you can click info 40 | self.browser.find_element_by_css_selector("a.btn.btn-info.btn-xs").click() 41 | # You're taken to the secret's info page, and you can start a request and provide a reason 42 | self.browser.find_element_by_css_selector("a.btn.btn-large.btn-info").click() 43 | requestbox = self.browser.find_element_by_name("reason_for_request") 44 | self.assertEqual(requestbox.get_attribute("placeholder"), "Reason for request") 45 | requestbox.send_keys("Pretty Please Gimme") 46 | self.browser.find_element_by_css_selector( 47 | "button.btn.primary.btn-default" 48 | ).click() 49 | # As the admin is all-powerful, they are automatically approved and can find the secret in the page text 50 | key = self.browser.find_element_by_tag_name("code").text 51 | self.assertEqual(key, "LICE-NSEP-LATE") 52 | 53 | def test_standard_user_can_request_and_admin_can_approve(self): 54 | # Standard tech user can log in and finds previously-created computer+secret 55 | self.browser.get(self.live_server_url) 56 | self.assertIn("Crypt", self.browser.title) 57 | username_box = self.browser.find_element_by_id("id_username") 58 | password_box = self.browser.find_element_by_id("id_password") 59 | username_box.send_keys("tech") 60 | password_box.send_keys("password") 61 | password_box.send_keys(Keys.ENTER) 62 | self.browser.find_element_by_link_text("Info").click() 63 | self.browser.find_element_by_link_text("Info / Request").click() 64 | secret_url = self.browser.current_url 65 | self.assertRegexpMatches(secret_url, "/info/secret/.+") 66 | self.browser.find_element_by_link_text("Request Key").click() 67 | requestbox = self.browser.find_element_by_name("reason_for_request") 68 | requestbox.send_keys("With sugar on top") 69 | self.browser.find_element_by_css_selector( 70 | "button.btn.primary.btn-default" 71 | ).click() 72 | # Standard users live in a world ruled by gravity, and must wait for approval 73 | disabled_button = self.browser.find_element_by_css_selector( 74 | "button.btn.btn-disabled.btn-info" 75 | ).text 76 | self.assertEqual(disabled_button, "Request Pending") 77 | # Let's log out and let the admin do their approval magic 78 | self.browser.find_element_by_id("dLabel").click() 79 | self.browser.find_element_by_link_text("Log out").click() 80 | username_box = self.browser.find_element_by_id("id_username") 81 | password_box = self.browser.find_element_by_id("id_password") 82 | username_box.send_keys("admin") 83 | password_box.send_keys("sekrit") 84 | password_box.send_keys(Keys.ENTER) 85 | self.browser.find_element_by_link_text("Approve requests").click() 86 | # This should fail, as per https://github.com/grahamgilbert/Crypt-Server/issues/12 87 | self.browser.find_element_by_link_text("Manage").click() 88 | -------------------------------------------------------------------------------- /fvserver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/fvserver/__init__.py -------------------------------------------------------------------------------- /fvserver/context_processors.py: -------------------------------------------------------------------------------- 1 | import plistlib 2 | import os 3 | 4 | 5 | def crypt_version(request): 6 | # return the value you want as a dictionary. you may add multiple values in there. 7 | current_dir = os.path.dirname(os.path.realpath(__file__)) 8 | with open( 9 | os.path.join(os.path.dirname(current_dir), "fvserver", "version.plist"), "rb" 10 | ) as f: 11 | version = plistlib.load(f) 12 | return {"CRYPT_VERSION": version["version"]} 13 | -------------------------------------------------------------------------------- /fvserver/example_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fvserver.system_settings import * 3 | 4 | DATABASES = { 5 | "default": { 6 | "ENGINE": "django.db.backends.sqlite3", # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 7 | "NAME": os.path.join( 8 | PROJECT_DIR, "crypt.db" 9 | ), # Or path to database file if using sqlite3. 10 | "USER": "", # Not used with sqlite3. 11 | "PASSWORD": "", # Not used with sqlite3. 12 | "HOST": "", # Set to empty string for localhost. Not used with sqlite3. 13 | "PORT": "", # Set to empty string for default. Not used with sqlite3. 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fvserver/system_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Django settings for fvserver project. 4 | 5 | PROJECT_DIR = os.path.abspath( 6 | os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.pardir) 7 | ) 8 | ENCRYPTED_FIELD_KEYS_DIR = os.path.join(PROJECT_DIR, "keyset") 9 | DEBUG = False 10 | 11 | ROTATE_VIEWED_SECRETS = True 12 | 13 | DATE_FORMAT = "Y-m-d H:i:s" 14 | DATETIME_FORMAT = "Y-m-d H:i:s" 15 | 16 | ADMINS = [ 17 | ( 18 | # ('Your Name', 'your_email@example.com'), 19 | ) 20 | ] 21 | 22 | FIELD_ENCRYPTION_KEY = os.environ.get("FIELD_ENCRYPTION_KEY", "") 23 | 24 | MANAGERS = ADMINS 25 | 26 | # Local time zone for this installation. Choices can be found here: 27 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 28 | # although not all choices may be available on all operating systems. 29 | # In a Windows environment this must be set to your system time zone. 30 | TIME_ZONE = "Europe/London" 31 | 32 | # Language code for this installation. All choices can be found here: 33 | # http://www.i18nguy.com/unicode/language-identifiers.html 34 | LANGUAGE_CODE = "en-us" 35 | 36 | SITE_ID = 1 37 | 38 | # If you set this to False, Django will make some optimizations so as not 39 | # to load the internationalization machinery. 40 | USE_I18N = True 41 | 42 | # If you set this to False, Django will not format dates, numbers and 43 | # calendars according to the current locale. 44 | USE_L10N = False 45 | 46 | # If you set this to False, Django will not use timezone-aware datetimes. 47 | USE_TZ = True 48 | 49 | # Absolute filesystem path to the directory that will hold user-uploaded files. 50 | # Example: "/home/media/media.lawrence.com/media/" 51 | MEDIA_ROOT = "" 52 | 53 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 54 | # trailing slash. 55 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 56 | MEDIA_URL = "" 57 | 58 | # Absolute path to the directory static files should be collected to. 59 | # Don't put anything in this directory yourself; store your static files 60 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 61 | # Example: "/home/media/media.lawrence.com/static/" 62 | STATIC_ROOT = os.path.join(PROJECT_DIR, "static") 63 | 64 | # URL prefix for static files. 65 | # Example: "http://media.lawrence.com/static/" 66 | STATIC_URL = "/static/" 67 | 68 | # URL prefix for admin static files -- CSS, JavaScript and images. 69 | # Make sure to use a trailing slash. 70 | # Examples: "http://foo.com/static/admin/", "/static/admin/". 71 | # deprecated in Django 1.4, but django_wsgiserver still looks for it 72 | # when serving admin media 73 | ADMIN_MEDIA_PREFIX = "/static_admin/" 74 | 75 | # Additional locations of static files 76 | STATICFILES_DIRS = ( 77 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 78 | # Always use forward slashes, even on Windows. 79 | # Don't forget to use absolute paths, not relative paths. 80 | os.path.join(PROJECT_DIR, "site_static"), 81 | ) 82 | 83 | LOGIN_URL = "/login/" 84 | LOGIN_REDIRECT_URL = "/" 85 | 86 | ALLOWED_HOSTS = ["*"] 87 | 88 | # List of finder classes that know how to find static files in 89 | # various locations. 90 | STATICFILES_FINDERS = ( 91 | "django.contrib.staticfiles.finders.FileSystemFinder", 92 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 93 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 94 | ) 95 | 96 | # Make this unique, and don't share it with anybody. 97 | SECRET_KEY = "6%y8=x5(#ufxd*+d+-ohwy0b$5z^cla@7tvl@n55_h_cex0qat" 98 | 99 | TEMPLATES = [ 100 | { 101 | "BACKEND": "django.template.backends.django.DjangoTemplates", 102 | "DIRS": [ 103 | # insert your TEMPLATE_DIRS here 104 | os.path.join(PROJECT_DIR, "templates") 105 | ], 106 | "APP_DIRS": True, 107 | "OPTIONS": { 108 | "context_processors": [ 109 | # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this 110 | # list if you haven't customized them: 111 | "django.contrib.auth.context_processors.auth", 112 | "django.template.context_processors.debug", 113 | "django.template.context_processors.i18n", 114 | "django.contrib.messages.context_processors.messages", 115 | "django.template.context_processors.media", 116 | "django.template.context_processors.static", 117 | "django.template.context_processors.tz", 118 | "django.template.context_processors.request", 119 | "fvserver.context_processors.crypt_version", 120 | ], 121 | "debug": DEBUG, 122 | }, 123 | } 124 | ] 125 | 126 | MIDDLEWARE = [ 127 | "django.middleware.security.SecurityMiddleware", 128 | "whitenoise.middleware.WhiteNoiseMiddleware", 129 | "django.contrib.sessions.middleware.SessionMiddleware", 130 | "django.middleware.common.CommonMiddleware", 131 | "django.middleware.csrf.CsrfViewMiddleware", 132 | "django.contrib.auth.middleware.AuthenticationMiddleware", 133 | "django.contrib.messages.middleware.MessageMiddleware", 134 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 135 | ] 136 | 137 | 138 | ROOT_URLCONF = "fvserver.urls" 139 | 140 | # Python dotted path to the WSGI application used by Django's runserver. 141 | WSGI_APPLICATION = "fvserver.wsgi.application" 142 | 143 | 144 | INSTALLED_APPS = ( 145 | "whitenoise.runserver_nostatic", 146 | "django.contrib.auth", 147 | "django.contrib.contenttypes", 148 | "django.contrib.sessions", 149 | "django.contrib.sites", 150 | "django.contrib.messages", 151 | "django.contrib.staticfiles", 152 | # Uncomment the next line to enable the admin: 153 | "django.contrib.admin", 154 | # Uncomment the next line to enable admin documentation: 155 | "django.contrib.admindocs", 156 | "server", 157 | "bootstrap4", 158 | "django_extensions", 159 | ) 160 | 161 | LOGGING = { 162 | "version": 1, 163 | "disable_existing_loggers": False, 164 | "formatters": { 165 | "default": { 166 | "format": "[DJANGO] %(levelname)s %(asctime)s %(module)s " 167 | "%(name)s.%(funcName)s:%(lineno)s: %(message)s" 168 | }, 169 | }, 170 | "handlers": { 171 | "console": { 172 | "level": "DEBUG", 173 | "class": "logging.StreamHandler", 174 | "formatter": "default", 175 | } 176 | }, 177 | "loggers": { 178 | "*": { 179 | "handlers": ["console"], 180 | "level": "DEBUG", 181 | "propagate": True, 182 | } 183 | }, 184 | } 185 | 186 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 187 | -------------------------------------------------------------------------------- /fvserver/urls.py: -------------------------------------------------------------------------------- 1 | # from django.conf.urls import include, url 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | from django.contrib import admin 5 | 6 | # admin.autodiscover() 7 | import django.contrib.auth.views as auth_views 8 | import django.contrib.admindocs.urls as admindocs_urls 9 | from django.urls import path, include 10 | 11 | app_name = "fvserver" 12 | 13 | urlpatterns = [ 14 | path("login/", auth_views.LoginView.as_view(), name="login"), 15 | path("logout/", auth_views.logout_then_login, name="logout"), 16 | path( 17 | "changepassword/", 18 | auth_views.PasswordChangeView.as_view(), 19 | name="password_change", 20 | ), 21 | path( 22 | "changepassword/done/", 23 | auth_views.PasswordChangeDoneView.as_view(), 24 | name="password_change_done", 25 | ), 26 | path("", include("server.urls")), 27 | # Uncomment the admin/doc line below to enable admin documentation: 28 | path("admin/doc/", include(admindocs_urls)), 29 | # Uncomment the next line to enable the admin: 30 | path("admin/", admin.site.urls), 31 | ] 32 | -------------------------------------------------------------------------------- /fvserver/version.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | version 6 | 3.4.1.378 7 | 8 | 9 | -------------------------------------------------------------------------------- /fvserver/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for fvserver project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | 17 | import os 18 | 19 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fvserver.settings") 20 | 21 | # This application object is used by any WSGI server configured to use this 22 | # file. This includes Django's development server, if the WSGI_APPLICATION 23 | # setting points here. 24 | from django.core.wsgi import get_wsgi_application 25 | 26 | application = get_wsgi_application() 27 | 28 | # Apply WSGI middleware here. 29 | # from helloworld.wsgi import HelloWorldApplication 30 | # application = HelloWorldApplication(application) 31 | -------------------------------------------------------------------------------- /generate_keyczart.py: -------------------------------------------------------------------------------- 1 | import keyczar 2 | import subprocess 3 | import os 4 | 5 | directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), "keyset") 6 | 7 | if not os.path.exists(directory): 8 | os.makedirs(directory) 9 | 10 | if not os.listdir(directory): 11 | location_string = "--location={}".format(directory) 12 | cmd = ["keyczart", "create", location_string, "--purpose=crypt", "--name=crypt"] 13 | subprocess.check_call(cmd) 14 | cmd = ["keyczart", "addkey", location_string, "--status=primary"] 15 | subprocess.check_call(cmd) 16 | else: 17 | print("Keyset directory already has something in there. Skipping key generation.") 18 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fvserver.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /remote_build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import subprocess 4 | import requests 5 | import os 6 | import argparse 7 | 8 | parser = argparse.ArgumentParser(description="Process a build.") 9 | parser.add_argument("build_tag", type=str, help="The tag to build.") 10 | 11 | args = parser.parse_args() 12 | 13 | api_user_token = os.getenv("CIRCLE_API_USER_TOKEN") 14 | project_reponame = "crypt-server-saml" 15 | project_username = "grahamgilbert" 16 | 17 | post_data = {} 18 | post_data["build_parameters"] = {"TAG": args.build_tag} 19 | 20 | url = "https://circleci.com/api/v1.1/project/github/{}/{}/tree/master".format( 21 | project_username, project_reponame 22 | ) 23 | 24 | the_request = requests.post(url, json=post_data, auth=(api_user_token, "")) 25 | if the_request.status_code == requests.codes.ok: 26 | print(the_request.json) 27 | else: 28 | print(the_request.text) 29 | -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/server/__init__.py -------------------------------------------------------------------------------- /server/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from server.models import * 3 | 4 | admin.site.register(Computer) 5 | admin.site.register(Secret) 6 | admin.site.register(Request) 7 | -------------------------------------------------------------------------------- /server/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import * 3 | 4 | 5 | class RequestForm(forms.ModelForm): 6 | class Meta: 7 | model = Request 8 | fields = ("reason_for_request",) 9 | 10 | 11 | class ApproveForm(forms.ModelForm): 12 | # approved = forms.BooleanField() 13 | approved = forms.TypedChoiceField( 14 | coerce=lambda x: bool(int(x)), 15 | choices=((1, "Approved"), (0, "Denied")), 16 | widget=forms.RadioSelect, 17 | label="Approved?", 18 | ) 19 | 20 | class Meta: 21 | model = Request 22 | fields = ("approved", "reason_for_approval") 23 | 24 | 25 | class ComputerForm(forms.ModelForm): 26 | class Meta: 27 | model = Computer 28 | fields = ("serial", "username", "computername") 29 | 30 | 31 | class SecretForm(forms.ModelForm): 32 | class Meta: 33 | model = Secret 34 | fields = ("secret_type", "secret", "computer") 35 | widgets = {"computer": forms.HiddenInput()} 36 | -------------------------------------------------------------------------------- /server/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 = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Computer", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | verbose_name="ID", 20 | serialize=False, 21 | auto_created=True, 22 | primary_key=True, 23 | ), 24 | ), 25 | ( 26 | "recovery_key", 27 | models.CharField(max_length=200, verbose_name=b"Recovery Key"), 28 | ), 29 | ( 30 | "serial", 31 | models.CharField(max_length=200, verbose_name=b"Serial Number"), 32 | ), 33 | ( 34 | "username", 35 | models.CharField(max_length=200, verbose_name=b"User Name"), 36 | ), 37 | ( 38 | "computername", 39 | models.CharField(max_length=200, verbose_name=b"Computer Name"), 40 | ), 41 | ("last_checkin", models.DateTimeField(null=True, blank=True)), 42 | ], 43 | options={ 44 | "ordering": ["serial"], 45 | "permissions": ( 46 | ("can_approve", "Can approve requests to see encryption keys"), 47 | ), 48 | }, 49 | ), 50 | migrations.CreateModel( 51 | name="Request", 52 | fields=[ 53 | ( 54 | "id", 55 | models.AutoField( 56 | verbose_name="ID", 57 | serialize=False, 58 | auto_created=True, 59 | primary_key=True, 60 | ), 61 | ), 62 | ("approved", models.NullBooleanField(verbose_name=b"Approved?")), 63 | ("reason_for_request", models.TextField()), 64 | ( 65 | "reason_for_approval", 66 | models.TextField( 67 | null=True, verbose_name=b"Approval Notes", blank=True 68 | ), 69 | ), 70 | ("date_requested", models.DateTimeField(auto_now_add=True)), 71 | ("date_approved", models.DateTimeField(null=True, blank=True)), 72 | ("current", models.BooleanField(default=True)), 73 | ( 74 | "auth_user", 75 | models.ForeignKey( 76 | related_name="auth_user", 77 | to=settings.AUTH_USER_MODEL, 78 | null=True, 79 | on_delete=models.CASCADE, 80 | ), 81 | ), 82 | ( 83 | "computer", 84 | models.ForeignKey(to="server.Computer", on_delete=models.CASCADE), 85 | ), 86 | ( 87 | "requesting_user", 88 | models.ForeignKey( 89 | related_name="requesting_user", 90 | to=settings.AUTH_USER_MODEL, 91 | on_delete=models.CASCADE, 92 | ), 93 | ), 94 | ], 95 | ), 96 | ] 97 | -------------------------------------------------------------------------------- /server/migrations/0002_auto_20150713_1214.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from django.shortcuts import get_object_or_404 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("server", "0001_initial")] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Secret", 14 | fields=[ 15 | ( 16 | "id", 17 | models.AutoField( 18 | verbose_name="ID", 19 | serialize=False, 20 | auto_created=True, 21 | primary_key=True, 22 | ), 23 | ), 24 | ("secret", models.CharField(max_length=256)), 25 | ( 26 | "secret_type", 27 | models.CharField( 28 | default=b"recovery_key", 29 | max_length=256, 30 | choices=[ 31 | (b"recovery_key", b"Recovery Key"), 32 | (b"password", b"Password"), 33 | ], 34 | ), 35 | ), 36 | ("date_escrowed", models.DateTimeField(auto_now_add=True)), 37 | ], 38 | ), 39 | migrations.AddField( 40 | model_name="secret", 41 | name="computer", 42 | field=models.ForeignKey(to="server.Computer", on_delete=models.CASCADE), 43 | ), 44 | migrations.AlterField( 45 | model_name="request", 46 | name="computer", 47 | field=models.ForeignKey( 48 | related_name="computers", to="server.Computer", on_delete=models.CASCADE 49 | ), 50 | ), 51 | migrations.AddField( 52 | model_name="request", 53 | name="secret", 54 | field=models.ForeignKey( 55 | null=True, 56 | related_name="secrets", 57 | to="server.Secret", 58 | on_delete=models.CASCADE, 59 | ), 60 | preserve_default=False, 61 | ), 62 | ] 63 | -------------------------------------------------------------------------------- /server/migrations/0003_auto_20150713_1215.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from django.shortcuts import get_object_or_404 4 | from django.db import models, migrations 5 | 6 | 7 | def move_keys_and_requests(apps, schema_editor): 8 | seen_serials = [("dummy_serial", "dummy_id")] 9 | Computer = apps.get_model("server", "Computer") 10 | Secret = apps.get_model("server", "Secret") 11 | Request = apps.get_model("server", "Request") 12 | for computer in Computer.objects.all(): 13 | # if we've seen the serial before, get the computer that we saw before 14 | target_id = None 15 | for serial, id in seen_serials: 16 | if computer.serial == serial: 17 | target_id = id 18 | break 19 | if target_id == None: 20 | target_id = computer.id 21 | 22 | target_computer = get_object_or_404(Computer, pk=target_id) 23 | # create a new secret 24 | secret = Secret( 25 | computer=target_computer, 26 | secret=computer.recovery_key, 27 | date_escrowed=computer.last_checkin, 28 | ) 29 | secret.save() 30 | 31 | requests = Request.objects.filter(computer=computer) 32 | for request in requests: 33 | request.secret = secret 34 | request.save() 35 | 36 | if target_computer.id != computer.id: 37 | # Dupe computer, bin it 38 | computer.delete() 39 | 40 | 41 | class Migration(migrations.Migration): 42 | 43 | dependencies = [("server", "0002_auto_20150713_1214")] 44 | 45 | operations = [migrations.RunPython(move_keys_and_requests)] 46 | -------------------------------------------------------------------------------- /server/migrations/0004_auto_20150713_1216.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from django.shortcuts import get_object_or_404 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("server", "0003_auto_20150713_1215")] 10 | 11 | operations = [ 12 | migrations.RemoveField(model_name="computer", name="recovery_key"), 13 | migrations.RemoveField(model_name="request", name="computer"), 14 | ] 15 | -------------------------------------------------------------------------------- /server/migrations/0005_auto_20150713_1754.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 = [("server", "0004_auto_20150713_1216")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="request", 14 | name="secret", 15 | field=models.ForeignKey(to="server.Secret", on_delete=models.CASCADE), 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /server/migrations/0006_auto_20150714_0821.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | # import django_extensions.db.fields.encrypted 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [("server", "0005_auto_20150713_1754")] 12 | 13 | operations = [ 14 | # migrations.AlterField( 15 | # model_name="secret", 16 | # name="secret", 17 | # field=django_extensions.db.fields.encrypted.EncryptedCharField( 18 | # max_length=256 19 | # ), 20 | # ) 21 | ] 22 | -------------------------------------------------------------------------------- /server/migrations/0007_auto_20150714_0822.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from django.shortcuts import get_object_or_404 4 | from django.db import models, migrations 5 | 6 | 7 | def encrypt_secrets(apps, schema_editor): 8 | 9 | Secret = apps.get_model("server", "Secret") 10 | 11 | for secret in Secret.objects.all(): 12 | secret.save() 13 | 14 | 15 | class Migration(migrations.Migration): 16 | 17 | dependencies = [("server", "0006_auto_20150714_0821")] 18 | 19 | operations = [migrations.RunPython(encrypt_secrets)] 20 | -------------------------------------------------------------------------------- /server/migrations/0008_auto_20150814_2140.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 = [("server", "0007_auto_20150714_0822")] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="secret", options={"ordering": ["-date_escrowed"]} 14 | ) 15 | ] 16 | -------------------------------------------------------------------------------- /server/migrations/0009_auto_20180430_2024.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-30 19:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("server", "0008_auto_20150814_2140")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="computer", 13 | name="computername", 14 | field=models.CharField(max_length=200, verbose_name="Computer Name"), 15 | ), 16 | migrations.AlterField( 17 | model_name="computer", 18 | name="serial", 19 | field=models.CharField(max_length=200, verbose_name="Serial Number"), 20 | ), 21 | migrations.AlterField( 22 | model_name="computer", 23 | name="username", 24 | field=models.CharField(max_length=200, verbose_name="User Name"), 25 | ), 26 | migrations.AlterField( 27 | model_name="request", 28 | name="approved", 29 | field=models.NullBooleanField(verbose_name="Approved?"), 30 | ), 31 | migrations.AlterField( 32 | model_name="request", 33 | name="reason_for_approval", 34 | field=models.TextField( 35 | blank=True, null=True, verbose_name="Approval Notes" 36 | ), 37 | ), 38 | migrations.AlterField( 39 | model_name="secret", 40 | name="secret_type", 41 | field=models.CharField( 42 | choices=[("recovery_key", "Recovery Key"), ("password", "Password")], 43 | default="recovery_key", 44 | max_length=256, 45 | ), 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /server/migrations/0009_secret_rotation_required.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-04-30 21:37 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("server", "0008_auto_20150814_2140")] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="secret", 15 | name="rotation_required", 16 | field=models.BooleanField(default=False), 17 | ) 18 | ] 19 | -------------------------------------------------------------------------------- /server/migrations/0010_auto_20180726_1700.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-07-26 16:00 3 | from __future__ import unicode_literals 4 | 5 | from server.models import * 6 | from django.db import migrations, models 7 | 8 | 9 | def unique_serials(apps, schema_editor): 10 | """ 11 | Make sure serial numbers are unique 12 | """ 13 | seen_serials = [] 14 | Computer = apps.get_model("server", "Computer") 15 | Secret = apps.get_model("server", "Secret") 16 | all_computers = Computer.objects.all() 17 | for computer in all_computers: 18 | if computer.serial not in seen_serials: 19 | # not seen it before, add it to the list of devices we've seen 20 | seen_serials.append(computer.serial) 21 | else: 22 | # we've seen it before, select all the secrets for the 23 | # machine and move them to the first instance of the serial number 24 | secrets = Secret.objects.filter(computer=computer) 25 | # reselect here so we don't get bit when we delete the computer 26 | first_computer = Computer.objects.all().first() 27 | for secret in secrets: 28 | secret.computer = first_computer 29 | secret.save() 30 | computer.delete() 31 | 32 | 33 | class Migration(migrations.Migration): 34 | 35 | dependencies = [("server", "0009_secret_rotation_required")] 36 | 37 | operations = [migrations.RunPython(unique_serials)] 38 | -------------------------------------------------------------------------------- /server/migrations/0011_manual_unique_serials.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-07-26 16:00 3 | from __future__ import unicode_literals 4 | 5 | from server.models import * 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [("server", "0010_auto_20180726_1700")] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="computer", 16 | name="serial", 17 | field=models.CharField( 18 | max_length=200, unique=True, verbose_name=b"Serial Number" 19 | ), 20 | ) 21 | ] 22 | -------------------------------------------------------------------------------- /server/migrations/0012_auto_20181128_2038.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-11-28 20:38 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("server", "0011_manual_unique_serials")] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="computer", 15 | name="computername", 16 | field=models.CharField(max_length=200, verbose_name="Computer Name"), 17 | ), 18 | migrations.AlterField( 19 | model_name="computer", 20 | name="serial", 21 | field=models.CharField( 22 | max_length=200, unique=True, verbose_name="Serial Number" 23 | ), 24 | ), 25 | migrations.AlterField( 26 | model_name="computer", 27 | name="username", 28 | field=models.CharField(max_length=200, verbose_name="User Name"), 29 | ), 30 | migrations.AlterField( 31 | model_name="request", 32 | name="approved", 33 | field=models.NullBooleanField(verbose_name="Approved?"), 34 | ), 35 | migrations.AlterField( 36 | model_name="request", 37 | name="reason_for_approval", 38 | field=models.TextField( 39 | blank=True, null=True, verbose_name="Approval Notes" 40 | ), 41 | ), 42 | migrations.AlterField( 43 | model_name="secret", 44 | name="secret_type", 45 | field=models.CharField( 46 | choices=[("recovery_key", "Recovery Key"), ("password", "Password")], 47 | default="recovery_key", 48 | max_length=256, 49 | ), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /server/migrations/0016_auto_20181213_2145.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2018-12-13 21:45 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 | 10 | dependencies = [("server", "0012_auto_20181128_2038")] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="request", 15 | name="auth_user", 16 | field=models.ForeignKey( 17 | null=True, 18 | on_delete=django.db.models.deletion.PROTECT, 19 | related_name="auth_user", 20 | to=settings.AUTH_USER_MODEL, 21 | ), 22 | ), 23 | migrations.AlterField( 24 | model_name="request", 25 | name="secret", 26 | field=models.ForeignKey( 27 | on_delete=django.db.models.deletion.PROTECT, to="server.Secret" 28 | ), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /server/migrations/0017_merge_20181217_1829.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2018-12-17 18:29 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("server", "0009_auto_20180430_2024"), 10 | ("server", "0016_auto_20181213_2145"), 11 | ] 12 | 13 | operations = [] 14 | -------------------------------------------------------------------------------- /server/migrations/0018_auto_20201029_2134.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-10-29 21:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("server", "0017_merge_20181217_1829")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="computer", 13 | name="serial", 14 | field=models.CharField( 15 | max_length=200, unique=True, verbose_name="Serial Number" 16 | ), 17 | ), 18 | migrations.AlterField( 19 | model_name="secret", 20 | name="secret_type", 21 | field=models.CharField( 22 | choices=[ 23 | ("recovery_key", "Recovery Key"), 24 | ("password", "Password"), 25 | ("unlock_pin", "Unlock PIN"), 26 | ], 27 | default="recovery_key", 28 | max_length=256, 29 | ), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /server/migrations/0019_alter_request_approved_alter_secret_secret.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.2 on 2022-11-10 17:28 2 | 3 | from django.db import migrations, models 4 | import encrypted_model_fields.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("server", "0018_auto_20201029_2134"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="request", 16 | name="approved", 17 | field=models.BooleanField(null=True, verbose_name="Approved?"), 18 | ), 19 | migrations.AlterField( 20 | model_name="secret", 21 | name="secret", 22 | field=encrypted_model_fields.fields.EncryptedCharField(), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /server/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/server/migrations/__init__.py -------------------------------------------------------------------------------- /server/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User, Permission 3 | from django.contrib.contenttypes.models import ContentType 4 | from encrypted_model_fields.fields import EncryptedCharField 5 | 6 | from django.core.exceptions import ValidationError 7 | 8 | 9 | # Create your models here. 10 | class Computer(models.Model): 11 | # recovery_key = models.CharField(max_length=200, verbose_name="Recovery Key") 12 | serial = models.CharField(max_length=200, verbose_name="Serial Number", unique=True) 13 | username = models.CharField(max_length=200, verbose_name="User Name") 14 | computername = models.CharField(max_length=200, verbose_name="Computer Name") 15 | last_checkin = models.DateTimeField(blank=True, null=True) 16 | 17 | def __str__(self): 18 | return self.computername 19 | 20 | class Meta: 21 | ordering = ["serial"] 22 | permissions = ( 23 | ("can_approve", ("Can approve requests to see encryption keys")), 24 | ) 25 | 26 | 27 | SECRET_TYPES = ( 28 | ("recovery_key", "Recovery Key"), 29 | ("password", "Password"), 30 | ("unlock_pin", "Unlock PIN"), 31 | ) 32 | 33 | 34 | class Secret(models.Model): 35 | computer = models.ForeignKey(Computer, on_delete=models.CASCADE) 36 | secret = EncryptedCharField(max_length=256) 37 | secret_type = models.CharField( 38 | max_length=256, choices=SECRET_TYPES, default="recovery_key" 39 | ) 40 | date_escrowed = models.DateTimeField(auto_now_add=True) 41 | rotation_required = models.BooleanField(default=False) 42 | 43 | def validate_unique(self, *args, **kwargs): 44 | if ( 45 | self.secret 46 | in [ 47 | str(s) 48 | for s in self.__class__.objects.filter( 49 | secret_type=self.secret_type, computer=self.computer 50 | ) 51 | ] 52 | and not self.rotation_required 53 | ): 54 | raise ValidationError("already used") 55 | super(Secret, self).validate_unique(*args, **kwargs) 56 | 57 | def save(self, *args, **kwargs): 58 | self.validate_unique() 59 | super(Secret, self).save(*args, **kwargs) 60 | 61 | def __str__(self): 62 | return self.secret 63 | 64 | class Meta: 65 | ordering = ["-date_escrowed"] 66 | 67 | 68 | class Request(models.Model): 69 | secret = models.ForeignKey(Secret, on_delete=models.PROTECT) 70 | # computer = models.ForeignKey(Computer, null=True, related_name='computers') 71 | requesting_user = models.ForeignKey( 72 | User, related_name="requesting_user", on_delete=models.CASCADE 73 | ) 74 | approved = models.BooleanField(verbose_name="Approved?", null=True) 75 | auth_user = models.ForeignKey( 76 | User, null=True, related_name="auth_user", on_delete=models.PROTECT 77 | ) 78 | reason_for_request = models.TextField() 79 | reason_for_approval = models.TextField( 80 | blank=True, null=True, verbose_name="Approval Notes" 81 | ) 82 | date_requested = models.DateTimeField(auto_now_add=True) 83 | date_approved = models.DateTimeField(blank=True, null=True) 84 | current = models.BooleanField(default=True) 85 | 86 | def __str__(self): 87 | return "%s - %s" % (self.secret, self.requesting_user) 88 | -------------------------------------------------------------------------------- /server/templates/server/approve.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load bootstrap4 %} 4 | {% block content %} 5 |

Approve Request

6 |

{{ the_request.secret.computer.computername }} ({{ the_request.secret.computer.serial }})

7 | {% if error_message %}

{{ error_message }}

{% endif %} 8 |
{% csrf_token %} 9 | 10 | 11 | {% bootstrap_form form %} 12 | 13 |

14 | 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /server/templates/server/computer_info.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block script %} 2 | 17 | 18 | {% endblock %} {% block dropdown %} 19 | 24 | {% endblock %} {% block nav %} 25 | 30 | {% endblock %} {% block content %} 31 |
32 |
33 |

{{ computer.computername }}

34 |

{{ computer.serial }}

35 |
36 |
37 | 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
Username{{ computer.username }}
Computer Name{{ computer.computername }}
Serial Number{{ computer.serial }}
Last Checked In{{ computer.last_checkin}}
60 |
61 |
62 | {% block button %} {% endblock %} 63 |
64 |
65 | 66 |
67 |
68 |

Secrets

69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {% for secret in secrets %} 77 | 78 | 79 | 80 | 87 | 88 | 89 | {% endfor %} 90 | 91 |
Secret TypeEscrow Date
{{ secret.get_secret_type_display }}{{ secret.date_escrowed }} 81 | Info / Request 86 |
92 |
93 |
94 | 95 | {% endblock %} 96 | -------------------------------------------------------------------------------- /server/templates/server/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block script %} 4 | 36 | 37 | {% endblock %} 38 | 39 | {% block dropdown %} 40 | 41 | {% endblock %} 42 | {% block nav %} 43 | {% if perms.server.can_approve %} 44 | {% if outstanding.count > 0 %} 45 | 46 | {% else %} 47 | 48 | {% endif %} 49 | {% endif %} 50 | {% endblock %} 51 | {% block content %} 52 | {% if perms.server.can_approve %} 53 | {% if outstanding.count > 0 %} 54 |
55 | You have outstanding requests to approve. 56 |
57 | {% endif %} 58 | {% endif %} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | {% for computer in computers.all %} 75 | 76 | {% endfor %} 77 | 78 |
Serial NumberComputer NameUser NameLast Checked In
{{ computer.serial }}{{ computer.computername }}{{ computer.username }}{{ computer.last_checkin }}Info
79 | 80 | {% endblock %} 81 | -------------------------------------------------------------------------------- /server/templates/server/manage_requests.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block script %} 4 | 16 | 17 | {% endblock %} 18 | 19 | {% block nav %} 20 | 21 | {% endblock %} 22 | {% block content %} 23 |

Key Requests

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% for the_request in requests %} 37 | 38 | {% endfor %} 39 | 40 |
Serial NumberComputer NameRequested ByReason for RequestDate Requested
{{ the_request.secret.computer.serial }}{{ the_request.secret.computer.computername }}{{ the_request.requesting_user }}{{ the_request.reason_for_request }}{{ the_request.date_requested }}Manage
41 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /server/templates/server/new_computer_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load bootstrap4 %} 3 | {% block content %} 4 |
5 |
6 |

New Computer

7 | {% if error_message %}

{{ error_message }}

{% endif %} 8 |
{% csrf_token %} 9 | 10 | 11 | {% bootstrap_form form %} 12 | 13 |

14 | 15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /server/templates/server/new_secret_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load bootstrap4 %} 3 | {% block content %} 4 |
5 |
6 |

New Secret

7 |

Computer: {{computer}}

8 | {% if error_message %}

{{ error_message }}

{% endif %} 9 |
{% csrf_token %} 10 | 11 | 12 | {% bootstrap_form form %} 13 | 14 |

15 | 16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /server/templates/server/request.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load bootstrap4 %} 3 | {% block content %} 4 |

Request Secret

5 |

{{ secret.computer.computername }} ({{ secret.computer.serial }})

6 |

{{ secret.get_secret_type_display }} - {{ secret.date_escrowed }}

7 | {% if error_message %}

{{ error_message }}

{% endif %} 8 |
{% csrf_token %} 9 | 10 | 11 | {% bootstrap_form form %} 12 | {% if not perms.server.can_approve %} 13 |

14 | {% else %} 15 |

16 | {% endif %} 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /server/templates/server/retrieve.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block script %} 4 | 16 | 17 | {% endblock %} 18 | 19 | {% block nav %} 20 | 21 | {% endblock %} 22 | {% block content %} 23 |
24 |

{{ computer.computername }}

25 |

{{ computer.serial }}

26 |
27 | 28 |
29 |
30 |

{{ the_request.secret.get_secret_type_display }}:
31 | 32 | {% spaceless %} 33 | {% for char in the_request.secret.secret %} 34 | {% if char in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" %} 35 | {{ char }} 36 | {% elif char in "0123456789" %} 37 | {{ char }} 38 | {% else %} 39 | {{ char }} 40 | {% endif %} 41 | {% endfor %} 42 | {% endspaceless %} 43 | 44 |

45 | 46 |

This approval is valid for 7 days, after which you will need to submit another request for access.

47 |
48 |
49 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /server/templates/server/secret_approved_button.html: -------------------------------------------------------------------------------- 1 | {% extends "server/secret_info.html" %} 2 | {% block button %} 3 | {% for the_request in approved|slice:":1" %} 4 | Retrieve Key 5 | 6 | {% endfor %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /server/templates/server/secret_info.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block script %} 4 | 16 | 17 | {% endblock %} 18 | 19 | {% block nav %} 20 | 21 | {% endblock %} 22 | {% block content %} 23 |
24 |
25 |

{{ computer.computername }}

26 |

{{ computer.serial }}

27 |
28 |
29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
Username{{ computer.username }}
Computer Name{{ computer.computername }}
Serial Number{{ computer.serial }}
Secret Type{{ secret.get_secret_type_display }}
Escrow Date{{ secret.date_escrowed }}
41 |
42 |
43 | {% block button %} 44 | {% endblock %} 45 |
46 |
47 | 48 | {% if perms.server.can_approve %} 49 |
50 |
51 |

Requests

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {% for the_request in requests %} 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {% endfor %} 74 | 75 |
Requesting UserReason for RequestDate RequestedApproved ByApproval NotesDate Approved
{{ the_request.requesting_user }}{{ the_request.reason_for_request }}{{ the_request.date_requested }}{{ the_request.auth_user }}{{ the_request.reason_for_approval }}{{ the_request.date_approved }}
76 |
77 | 78 |
79 | {% endif %} 80 | 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /server/templates/server/secret_request_button.html: -------------------------------------------------------------------------------- 1 | {% extends "server/secret_info.html" %} 2 | {% block button %} 3 | {% if not perms.server.can_approve %} 4 | {% if can_request %} 5 | Request Key 6 | {% else %} 7 | 8 | {% endif %} 9 | {% else %} 10 | Get Key 11 | {% endif %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /server/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, Client 2 | from django.contrib.auth.models import User 3 | from datetime import datetime 4 | from server.models import Computer, Secret, Request 5 | 6 | 7 | class RequestProcess(TestCase): 8 | def test_request_passes_correct_data_to_template(self): 9 | admin = User.objects.create_superuser("admin", "a@a.com", "sekrit") 10 | tech = User.objects.create_user("tech", "a@a.com", "password") 11 | tech.save() 12 | tech_test_computer = Computer( 13 | serial="TECHSERIAL", username="Daft Tech", computername="compy587" 14 | ) 15 | tech_test_computer.save() 16 | test_secret = Secret( 17 | computer=tech_test_computer, 18 | secret="SHHH-DONT-TELL", 19 | date_escrowed=datetime.now(), 20 | ) 21 | test_secret.save() 22 | secret_request = Request(secret=test_secret, requesting_user=tech) 23 | secret_request.save() 24 | client = Client() 25 | login_response = self.client.post( 26 | "/login/", {"username": "admin", "password": "sekrit"}, follow=True 27 | ) 28 | response = self.client.get("/manage-requests/", follow=True) 29 | print(response) 30 | self.assertTrue(response.context["user"].is_authenticated) 31 | -------------------------------------------------------------------------------- /server/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | app_name = "server" 5 | 6 | urlpatterns = [ 7 | # front. page 8 | path("", views.index, name="home"), 9 | path("ajax/", views.tableajax, name="tableajax"), 10 | # Add computer 11 | path("new/computer/", views.new_computer, name="new_computer"), 12 | # Add secret 13 | path("new/secret//", views.new_secret, name="new_secret"), 14 | # secret info 15 | path("info/secret//", views.secret_info, name="secret_info"), 16 | # computerinfo 17 | path("info//", views.computer_info, name="computer_info"), 18 | path("info//", views.computer_info, name="computer_info_serial"), 19 | # request 20 | path("request//", views.request, name="request"), 21 | # retrieve 22 | path("retrieve//", views.retrieve, name="retrieve"), 23 | # approve 24 | path("approve//", views.approve, name="approve"), 25 | # verify 26 | path("verify///", views.verify, name="verify"), 27 | # checkin 28 | path("checkin/", views.checkin, name="checkin"), 29 | # manage 30 | path("manage-requests/", views.managerequests, name="managerequests"), 31 | ] 32 | -------------------------------------------------------------------------------- /set_build_no.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import plistlib 5 | import subprocess 6 | 7 | current_version = "3.4.1" 8 | script_path = os.path.dirname(os.path.realpath(__file__)) 9 | 10 | 11 | # based on http://tgoode.com/2014/06/05/sensible-way-increment-bundle-version-cfbundleversion-xcode 12 | 13 | print("Setting Version to Git rev-list --count") 14 | cmd = ["git", "rev-list", "HEAD", "--count"] 15 | build_number = subprocess.check_output(cmd) 16 | # This will always be one commit behind, so this makes it current 17 | build_number = int(build_number) + 1 18 | 19 | version_number = "{}.{}".format(current_version, build_number) 20 | 21 | data = {"version": version_number} 22 | plist_path = "{}/fvserver/version.plist".format(script_path) 23 | file_name = open(plist_path, "wb") 24 | plistlib.dump(data, file_name) 25 | file_name.close() 26 | -------------------------------------------------------------------------------- /setup/requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 2 | asgiref==3.5.2 3 | asn1crypto==1.5.1 4 | astroid==2.12.11 5 | attrs==22.1.0 6 | beautifulsoup4==4.11.1 7 | black==24.3.0 8 | cffi==1.15.0 9 | click==8.1.3 10 | cryptography==44.0.1 11 | Django==4.2.18 12 | django-bootstrap3==11.0.0 13 | django-bootstrap4==22.2 14 | django-debug-toolbar==3.7.0 15 | django-encrypted-model-fields==0.6.5 16 | django-extensions==3.2.1 17 | django-iam-dbauth==0.1.4 18 | docutils==0.19 19 | flake8==5.0.4 20 | gunicorn==22.0.0 21 | idna==3.7 22 | isort==5.10.1 23 | lazy-object-proxy==1.7.1 24 | mccabe==0.7.0 25 | meld3==2.0.1 26 | mypy-extensions==0.4.3 27 | pathspec==0.10.1 28 | psycopg2==2.9.4 29 | pyasn1==0.4.8 30 | pycodestyle==2.9.1 31 | pycparser==2.21 32 | pycrypto==2.6.1 33 | pyflakes==2.5.0 34 | pylint==2.15.4 35 | pytz==2022.5 36 | regex==2022.9.13 37 | selenium==4.15.1 38 | six==1.16.0 39 | soupsieve==2.3.2.post1 40 | sqlparse==0.5.0 41 | supervisor==4.2.4 42 | tomli==2.0.1 43 | whitenoise==6.2.0 44 | wrapt==1.14.1 -------------------------------------------------------------------------------- /site_static/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /site_static/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /site_static/css/mixins.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/css/mixins.css -------------------------------------------------------------------------------- /site_static/css/styles.css: -------------------------------------------------------------------------------- 1 | /* line 5, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 2 | html { 3 | font-family: sans-serif; 4 | -ms-text-size-adjust: 100%; 5 | -webkit-text-size-adjust: 100%; } 6 | 7 | /* line 12, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 8 | body { 9 | margin: 0; } 10 | 11 | /* line 31, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 12 | article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { 13 | display: block; } 14 | 15 | /* line 40, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 16 | audio, canvas, progress, video { 17 | display: inline-block; 18 | /* 1 */ 19 | vertical-align: baseline; 20 | /* 2 */ } 21 | 22 | /* line 47, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 23 | audio:not([controls]) { 24 | display: none; 25 | height: 0; } 26 | 27 | /* line 55, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 28 | [hidden], template { 29 | display: none; } 30 | 31 | /* line 63, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 32 | a { 33 | background: transparent; } 34 | 35 | /* line 69, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 36 | a:active, a:hover { 37 | outline: 0; } 38 | 39 | /* line 77, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 40 | abbr[title] { 41 | border-bottom: 1px dotted; } 42 | 43 | /* line 83, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 44 | b, strong { 45 | font-weight: bold; } 46 | 47 | /* line 88, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 48 | dfn { 49 | font-style: italic; } 50 | 51 | /* line 93, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 52 | h1 { 53 | font-size: 2em; 54 | margin: 0.67em 0; } 55 | 56 | /* line 99, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 57 | mark { 58 | background: #ff0; 59 | color: #000; } 60 | 61 | /* line 105, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 62 | small { 63 | font-size: 80%; } 64 | 65 | /* line 111, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 66 | sub, sup { 67 | font-size: 75%; 68 | line-height: 0; 69 | position: relative; 70 | vertical-align: baseline; } 71 | 72 | /* line 118, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 73 | sup { 74 | top: -0.5em; } 75 | 76 | /* line 122, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 77 | sub { 78 | bottom: -0.25em; } 79 | 80 | /* line 130, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 81 | img { 82 | border: 0; } 83 | 84 | /* line 135, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 85 | svg:not(:root) { 86 | overflow: hidden; } 87 | 88 | /* line 143, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 89 | figure { 90 | margin: 1em 40px; } 91 | 92 | /* line 148, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 93 | hr { 94 | -moz-box-sizing: content-box; 95 | -webkit-box-sizing: content-box; 96 | box-sizing: content-box; 97 | height: 0; } 98 | 99 | /* line 155, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 100 | pre { 101 | overflow: auto; } 102 | 103 | /* line 163, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 104 | code, kbd, pre, samp { 105 | font-family: monospace, monospace; 106 | font-size: 1em; } 107 | 108 | /* line 182, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 109 | button, input, optgroup, select, textarea { 110 | color: inherit; 111 | font: inherit; 112 | margin: 0; } 113 | 114 | /* line 189, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 115 | button { 116 | overflow: visible; } 117 | 118 | /* line 198, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 119 | button, select { 120 | text-transform: none; } 121 | 122 | /* line 208, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 123 | button, html input[type="button"], input[type="reset"], input[type="submit"] { 124 | -webkit-appearance: button; 125 | /* 2 */ 126 | cursor: pointer; 127 | /* 3 */ } 128 | 129 | /* line 215, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 130 | button[disabled], html input[disabled] { 131 | cursor: default; } 132 | 133 | /* line 221, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 134 | button::-moz-focus-inner, input::-moz-focus-inner { 135 | border: 0; 136 | padding: 0; } 137 | 138 | /* line 228, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 139 | input { 140 | line-height: normal; } 141 | 142 | /* line 238, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 143 | input[type="checkbox"], input[type="radio"] { 144 | -webkit-box-sizing: border-box; 145 | -moz-box-sizing: border-box; 146 | box-sizing: border-box; 147 | padding: 0; } 148 | 149 | /* line 247, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 150 | input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { 151 | height: auto; } 152 | 153 | /* line 253, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 154 | input[type="search"] { 155 | -webkit-appearance: textfield; 156 | -moz-box-sizing: content-box; 157 | -webkit-box-sizing: content-box; 158 | box-sizing: content-box; } 159 | 160 | /* line 263, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 161 | input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { 162 | -webkit-appearance: none; } 163 | 164 | /* line 268, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 165 | fieldset { 166 | border: 1px solid #c0c0c0; 167 | margin: 0 2px; 168 | padding: 0.35em 0.625em 0.75em; } 169 | 170 | /* line 276, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 171 | legend { 172 | border: 0; 173 | padding: 0; } 174 | 175 | /* line 282, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 176 | textarea { 177 | overflow: auto; } 178 | 179 | /* line 288, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 180 | optgroup { 181 | font-weight: bold; } 182 | 183 | /* line 296, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 184 | table { 185 | border-collapse: collapse; 186 | border-spacing: 0; } 187 | 188 | /* line 302, /Users/mark/repos/pmx-select-test/sass/_normalize.scss */ 189 | td, th { 190 | padding: 0; } 191 | 192 | /* line 22, /Users/mark/repos/pmx-select-test/sass/styles.scss */ 193 | *, *::before, *:after { 194 | -webkit-box-sizing: border-box; 195 | -moz-box-sizing: border-box; 196 | box-sizing: border-box; 197 | -webkit-font-smoothing: antialiased; } 198 | 199 | /* line 28, /Users/mark/repos/pmx-select-test/sass/styles.scss */ 200 | ul { 201 | padding: 0; 202 | list-style: none; } 203 | 204 | /* line 34, /Users/mark/repos/pmx-select-test/sass/styles.scss */ 205 | html, body { 206 | background-color: white; 207 | color: black; 208 | font-family: sans-serif; 209 | line-height: 1.4; } 210 | 211 | /* line 44, /Users/mark/repos/pmx-select-test/sass/styles.scss */ 212 | .select2-container { 213 | margin: 3em; 214 | width: 300px; } 215 | -------------------------------------------------------------------------------- /site_static/css/variables.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/css/variables.css -------------------------------------------------------------------------------- /site_static/dataTables/css/dataTables.bootstrap.css: -------------------------------------------------------------------------------- 1 | table.dataTable { 2 | clear: both; 3 | margin-top: 6px !important; 4 | margin-bottom: 6px !important; 5 | max-width: none !important; 6 | border-collapse: separate !important; 7 | } 8 | table.dataTable td, 9 | table.dataTable th { 10 | -webkit-box-sizing: content-box; 11 | box-sizing: content-box; 12 | } 13 | table.dataTable td.dataTables_empty, 14 | table.dataTable th.dataTables_empty { 15 | text-align: center; 16 | } 17 | table.dataTable.nowrap th, 18 | table.dataTable.nowrap td { 19 | white-space: nowrap; 20 | } 21 | 22 | div.dataTables_wrapper div.dataTables_length label { 23 | font-weight: normal; 24 | text-align: left; 25 | white-space: nowrap; 26 | } 27 | div.dataTables_wrapper div.dataTables_length select { 28 | width: 75px; 29 | display: inline-block; 30 | } 31 | div.dataTables_wrapper div.dataTables_filter { 32 | text-align: right; 33 | } 34 | div.dataTables_wrapper div.dataTables_filter label { 35 | font-weight: normal; 36 | white-space: nowrap; 37 | text-align: left; 38 | } 39 | div.dataTables_wrapper div.dataTables_filter input { 40 | margin-left: 0.5em; 41 | display: inline-block; 42 | width: auto; 43 | } 44 | div.dataTables_wrapper div.dataTables_info { 45 | padding-top: 8px; 46 | white-space: nowrap; 47 | } 48 | div.dataTables_wrapper div.dataTables_paginate { 49 | margin: 0; 50 | white-space: nowrap; 51 | text-align: right; 52 | } 53 | div.dataTables_wrapper div.dataTables_paginate ul.pagination { 54 | margin: 2px 0; 55 | white-space: nowrap; 56 | } 57 | div.dataTables_wrapper div.dataTables_processing { 58 | position: absolute; 59 | top: 50%; 60 | left: 50%; 61 | width: 200px; 62 | margin-left: -100px; 63 | margin-top: -26px; 64 | text-align: center; 65 | padding: 1em 0; 66 | } 67 | 68 | table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting, 69 | table.dataTable thead > tr > td.sorting_asc, 70 | table.dataTable thead > tr > td.sorting_desc, 71 | table.dataTable thead > tr > td.sorting { 72 | padding-right: 30px; 73 | } 74 | table.dataTable thead > tr > th:active, 75 | table.dataTable thead > tr > td:active { 76 | outline: none; 77 | } 78 | table.dataTable thead .sorting, 79 | table.dataTable thead .sorting_asc, 80 | table.dataTable thead .sorting_desc, 81 | table.dataTable thead .sorting_asc_disabled, 82 | table.dataTable thead .sorting_desc_disabled { 83 | cursor: pointer; 84 | position: relative; 85 | } 86 | table.dataTable thead .sorting:after, 87 | table.dataTable thead .sorting_asc:after, 88 | table.dataTable thead .sorting_desc:after, 89 | table.dataTable thead .sorting_asc_disabled:after, 90 | table.dataTable thead .sorting_desc_disabled:after { 91 | position: absolute; 92 | bottom: 8px; 93 | right: 8px; 94 | display: block; 95 | font-family: 'Glyphicons Halflings'; 96 | opacity: 0.5; 97 | } 98 | table.dataTable thead .sorting:after { 99 | opacity: 0.2; 100 | content: "\e150"; 101 | /* sort */ 102 | } 103 | table.dataTable thead .sorting_asc:after { 104 | content: "\e155"; 105 | /* sort-by-attributes */ 106 | } 107 | table.dataTable thead .sorting_desc:after { 108 | content: "\e156"; 109 | /* sort-by-attributes-alt */ 110 | } 111 | table.dataTable thead .sorting_asc_disabled:after, 112 | table.dataTable thead .sorting_desc_disabled:after { 113 | color: #eee; 114 | } 115 | 116 | div.dataTables_scrollHead table.dataTable { 117 | margin-bottom: 0 !important; 118 | } 119 | 120 | div.dataTables_scrollBody > table { 121 | border-top: none; 122 | margin-top: 0 !important; 123 | margin-bottom: 0 !important; 124 | } 125 | div.dataTables_scrollBody > table > thead .sorting:after, 126 | div.dataTables_scrollBody > table > thead .sorting_asc:after, 127 | div.dataTables_scrollBody > table > thead .sorting_desc:after { 128 | display: none; 129 | } 130 | div.dataTables_scrollBody > table > tbody > tr:first-child > th, 131 | div.dataTables_scrollBody > table > tbody > tr:first-child > td { 132 | border-top: none; 133 | } 134 | 135 | div.dataTables_scrollFoot > .dataTables_scrollFootInner { 136 | box-sizing: content-box; 137 | } 138 | div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { 139 | margin-top: 0 !important; 140 | border-top: none; 141 | } 142 | 143 | @media screen and (max-width: 767px) { 144 | div.dataTables_wrapper div.dataTables_length, 145 | div.dataTables_wrapper div.dataTables_filter, 146 | div.dataTables_wrapper div.dataTables_info, 147 | div.dataTables_wrapper div.dataTables_paginate { 148 | text-align: center; 149 | } 150 | } 151 | table.dataTable.table-condensed > thead > tr > th { 152 | padding-right: 20px; 153 | } 154 | table.dataTable.table-condensed .sorting:after, 155 | table.dataTable.table-condensed .sorting_asc:after, 156 | table.dataTable.table-condensed .sorting_desc:after { 157 | top: 6px; 158 | right: 6px; 159 | } 160 | 161 | table.table-bordered.dataTable th, 162 | table.table-bordered.dataTable td { 163 | border-left-width: 0; 164 | } 165 | table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, 166 | table.table-bordered.dataTable td:last-child, 167 | table.table-bordered.dataTable td:last-child { 168 | border-right-width: 0; 169 | } 170 | table.table-bordered.dataTable tbody th, 171 | table.table-bordered.dataTable tbody td { 172 | border-bottom-width: 0; 173 | } 174 | 175 | div.dataTables_scrollHead table.table-bordered { 176 | border-bottom-width: 0; 177 | } 178 | 179 | div.table-responsive > div.dataTables_wrapper > div.row { 180 | margin: 0; 181 | } 182 | div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child { 183 | padding-left: 0; 184 | } 185 | div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child { 186 | padding-right: 0; 187 | } 188 | -------------------------------------------------------------------------------- /site_static/dataTables/css/dataTables.bootstrap.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody>table>thead .sorting:after,div.dataTables_scrollBody>table>thead .sorting_asc:after,div.dataTables_scrollBody>table>thead .sorting_desc:after{display:none}div.dataTables_scrollBody>table>tbody>tr:first-child>th,div.dataTables_scrollBody>table>tbody>tr:first-child>td{border-top:none}div.dataTables_scrollFoot>.dataTables_scrollFootInner{box-sizing:content-box}div.dataTables_scrollFoot>.dataTables_scrollFootInner>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:0}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0} 2 | -------------------------------------------------------------------------------- /site_static/dataTables/css/dataTables.bootstrap4.css: -------------------------------------------------------------------------------- 1 | table.dataTable { 2 | clear: both; 3 | margin-top: 6px !important; 4 | margin-bottom: 6px !important; 5 | max-width: none !important; 6 | border-collapse: separate !important; 7 | border-spacing: 0; 8 | } 9 | table.dataTable td, 10 | table.dataTable th { 11 | -webkit-box-sizing: content-box; 12 | box-sizing: content-box; 13 | } 14 | table.dataTable td.dataTables_empty, 15 | table.dataTable th.dataTables_empty { 16 | text-align: center; 17 | } 18 | table.dataTable.nowrap th, 19 | table.dataTable.nowrap td { 20 | white-space: nowrap; 21 | } 22 | 23 | div.dataTables_wrapper div.dataTables_length label { 24 | font-weight: normal; 25 | text-align: left; 26 | white-space: nowrap; 27 | } 28 | div.dataTables_wrapper div.dataTables_length select { 29 | width: auto; 30 | display: inline-block; 31 | } 32 | div.dataTables_wrapper div.dataTables_filter { 33 | text-align: right; 34 | } 35 | div.dataTables_wrapper div.dataTables_filter label { 36 | font-weight: normal; 37 | white-space: nowrap; 38 | text-align: left; 39 | } 40 | div.dataTables_wrapper div.dataTables_filter input { 41 | margin-left: 0.5em; 42 | display: inline-block; 43 | width: auto; 44 | } 45 | div.dataTables_wrapper div.dataTables_info { 46 | padding-top: 0.85em; 47 | white-space: nowrap; 48 | } 49 | div.dataTables_wrapper div.dataTables_paginate { 50 | margin: 0; 51 | white-space: nowrap; 52 | text-align: right; 53 | } 54 | div.dataTables_wrapper div.dataTables_paginate ul.pagination { 55 | margin: 2px 0; 56 | white-space: nowrap; 57 | justify-content: flex-end; 58 | } 59 | div.dataTables_wrapper div.dataTables_processing { 60 | position: absolute; 61 | top: 50%; 62 | left: 50%; 63 | width: 200px; 64 | margin-left: -100px; 65 | margin-top: -26px; 66 | text-align: center; 67 | padding: 1em 0; 68 | } 69 | 70 | table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting, 71 | table.dataTable thead > tr > td.sorting_asc, 72 | table.dataTable thead > tr > td.sorting_desc, 73 | table.dataTable thead > tr > td.sorting { 74 | padding-right: 30px; 75 | } 76 | table.dataTable thead > tr > th:active, 77 | table.dataTable thead > tr > td:active { 78 | outline: none; 79 | } 80 | table.dataTable thead .sorting, 81 | table.dataTable thead .sorting_asc, 82 | table.dataTable thead .sorting_desc, 83 | table.dataTable thead .sorting_asc_disabled, 84 | table.dataTable thead .sorting_desc_disabled { 85 | cursor: pointer; 86 | position: relative; 87 | } 88 | table.dataTable thead .sorting:before, table.dataTable thead .sorting:after, 89 | table.dataTable thead .sorting_asc:before, 90 | table.dataTable thead .sorting_asc:after, 91 | table.dataTable thead .sorting_desc:before, 92 | table.dataTable thead .sorting_desc:after, 93 | table.dataTable thead .sorting_asc_disabled:before, 94 | table.dataTable thead .sorting_asc_disabled:after, 95 | table.dataTable thead .sorting_desc_disabled:before, 96 | table.dataTable thead .sorting_desc_disabled:after { 97 | position: absolute; 98 | bottom: 0.9em; 99 | display: block; 100 | opacity: 0.3; 101 | } 102 | table.dataTable thead .sorting:before, 103 | table.dataTable thead .sorting_asc:before, 104 | table.dataTable thead .sorting_desc:before, 105 | table.dataTable thead .sorting_asc_disabled:before, 106 | table.dataTable thead .sorting_desc_disabled:before { 107 | right: 1em; 108 | content: "\2191"; 109 | } 110 | table.dataTable thead .sorting:after, 111 | table.dataTable thead .sorting_asc:after, 112 | table.dataTable thead .sorting_desc:after, 113 | table.dataTable thead .sorting_asc_disabled:after, 114 | table.dataTable thead .sorting_desc_disabled:after { 115 | right: 0.5em; 116 | content: "\2193"; 117 | } 118 | table.dataTable thead .sorting_asc:before, 119 | table.dataTable thead .sorting_desc:after { 120 | opacity: 1; 121 | } 122 | table.dataTable thead .sorting_asc_disabled:before, 123 | table.dataTable thead .sorting_desc_disabled:after { 124 | opacity: 0; 125 | } 126 | 127 | div.dataTables_scrollHead table.dataTable { 128 | margin-bottom: 0 !important; 129 | } 130 | 131 | div.dataTables_scrollBody table { 132 | border-top: none; 133 | margin-top: 0 !important; 134 | margin-bottom: 0 !important; 135 | } 136 | div.dataTables_scrollBody table thead .sorting:before, 137 | div.dataTables_scrollBody table thead .sorting_asc:before, 138 | div.dataTables_scrollBody table thead .sorting_desc:before, 139 | div.dataTables_scrollBody table thead .sorting:after, 140 | div.dataTables_scrollBody table thead .sorting_asc:after, 141 | div.dataTables_scrollBody table thead .sorting_desc:after { 142 | display: none; 143 | } 144 | div.dataTables_scrollBody table tbody tr:first-child th, 145 | div.dataTables_scrollBody table tbody tr:first-child td { 146 | border-top: none; 147 | } 148 | 149 | div.dataTables_scrollFoot > .dataTables_scrollFootInner { 150 | box-sizing: content-box; 151 | } 152 | div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { 153 | margin-top: 0 !important; 154 | border-top: none; 155 | } 156 | 157 | @media screen and (max-width: 767px) { 158 | div.dataTables_wrapper div.dataTables_length, 159 | div.dataTables_wrapper div.dataTables_filter, 160 | div.dataTables_wrapper div.dataTables_info, 161 | div.dataTables_wrapper div.dataTables_paginate { 162 | text-align: center; 163 | } 164 | } 165 | table.dataTable.table-sm > thead > tr > th { 166 | padding-right: 20px; 167 | } 168 | table.dataTable.table-sm .sorting:before, 169 | table.dataTable.table-sm .sorting_asc:before, 170 | table.dataTable.table-sm .sorting_desc:before { 171 | top: 5px; 172 | right: 0.85em; 173 | } 174 | table.dataTable.table-sm .sorting:after, 175 | table.dataTable.table-sm .sorting_asc:after, 176 | table.dataTable.table-sm .sorting_desc:after { 177 | top: 5px; 178 | } 179 | 180 | table.table-bordered.dataTable th, 181 | table.table-bordered.dataTable td { 182 | border-left-width: 0; 183 | } 184 | table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, 185 | table.table-bordered.dataTable td:last-child, 186 | table.table-bordered.dataTable td:last-child { 187 | border-right-width: 0; 188 | } 189 | table.table-bordered.dataTable tbody th, 190 | table.table-bordered.dataTable tbody td { 191 | border-bottom-width: 0; 192 | } 193 | 194 | div.dataTables_scrollHead table.table-bordered { 195 | border-bottom-width: 0; 196 | } 197 | 198 | div.table-responsive > div.dataTables_wrapper > div.row { 199 | margin: 0; 200 | } 201 | div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child { 202 | padding-left: 0; 203 | } 204 | div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child { 205 | padding-right: 0; 206 | } 207 | -------------------------------------------------------------------------------- /site_static/dataTables/css/dataTables.bootstrap4.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important;border-spacing:0}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:auto;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:0.85em;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap;justify-content:flex-end}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:before,table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:before,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:before,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:before,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:before,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:0.9em;display:block;opacity:0.3}table.dataTable thead .sorting:before,table.dataTable thead .sorting_asc:before,table.dataTable thead .sorting_desc:before,table.dataTable thead .sorting_asc_disabled:before,table.dataTable thead .sorting_desc_disabled:before{right:1em;content:"\2191"}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{right:0.5em;content:"\2193"}table.dataTable thead .sorting_asc:before,table.dataTable thead .sorting_desc:after{opacity:1}table.dataTable thead .sorting_asc_disabled:before,table.dataTable thead .sorting_desc_disabled:after{opacity:0}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody table thead .sorting:before,div.dataTables_scrollBody table thead .sorting_asc:before,div.dataTables_scrollBody table thead .sorting_desc:before,div.dataTables_scrollBody table thead .sorting:after,div.dataTables_scrollBody table thead .sorting_asc:after,div.dataTables_scrollBody table thead .sorting_desc:after{display:none}div.dataTables_scrollBody table tbody tr:first-child th,div.dataTables_scrollBody table tbody tr:first-child td{border-top:none}div.dataTables_scrollFoot>.dataTables_scrollFootInner{box-sizing:content-box}div.dataTables_scrollFoot>.dataTables_scrollFootInner>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-sm>thead>tr>th{padding-right:20px}table.dataTable.table-sm .sorting:before,table.dataTable.table-sm .sorting_asc:before,table.dataTable.table-sm .sorting_desc:before{top:5px;right:0.85em}table.dataTable.table-sm .sorting:after,table.dataTable.table-sm .sorting_asc:after,table.dataTable.table-sm .sorting_desc:after{top:5px}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:0}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0} 2 | -------------------------------------------------------------------------------- /site_static/dataTables/css/dataTables.foundation.css: -------------------------------------------------------------------------------- 1 | table.dataTable { 2 | clear: both; 3 | margin: 0.5em 0 !important; 4 | max-width: none !important; 5 | width: 100%; 6 | } 7 | table.dataTable td, 8 | table.dataTable th { 9 | -webkit-box-sizing: content-box; 10 | box-sizing: content-box; 11 | } 12 | table.dataTable td.dataTables_empty, 13 | table.dataTable th.dataTables_empty { 14 | text-align: center; 15 | } 16 | table.dataTable.nowrap th, table.dataTable.nowrap td { 17 | white-space: nowrap; 18 | } 19 | 20 | div.dataTables_wrapper { 21 | position: relative; 22 | } 23 | div.dataTables_wrapper div.dataTables_length label { 24 | float: left; 25 | text-align: left; 26 | margin-bottom: 0; 27 | } 28 | div.dataTables_wrapper div.dataTables_length select { 29 | width: 75px; 30 | margin-bottom: 0; 31 | } 32 | div.dataTables_wrapper div.dataTables_filter label { 33 | float: right; 34 | margin-bottom: 0; 35 | } 36 | div.dataTables_wrapper div.dataTables_filter input { 37 | display: inline-block !important; 38 | width: auto !important; 39 | margin-bottom: 0; 40 | margin-left: 0.5em; 41 | } 42 | div.dataTables_wrapper div.dataTables_info { 43 | padding-top: 2px; 44 | } 45 | div.dataTables_wrapper div.dataTables_paginate { 46 | float: right; 47 | margin: 0; 48 | } 49 | div.dataTables_wrapper div.dataTables_processing { 50 | position: absolute; 51 | top: 50%; 52 | left: 50%; 53 | width: 200px; 54 | margin-left: -100px; 55 | margin-top: -26px; 56 | text-align: center; 57 | padding: 1rem 0; 58 | } 59 | 60 | table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting, 61 | table.dataTable thead > tr > td.sorting_asc, 62 | table.dataTable thead > tr > td.sorting_desc, 63 | table.dataTable thead > tr > td.sorting { 64 | padding-right: 1.5rem; 65 | } 66 | table.dataTable thead > tr > th:active, 67 | table.dataTable thead > tr > td:active { 68 | outline: none; 69 | } 70 | table.dataTable thead .sorting, 71 | table.dataTable thead .sorting_asc, 72 | table.dataTable thead .sorting_desc, 73 | table.dataTable thead .sorting_asc_disabled, 74 | table.dataTable thead .sorting_desc_disabled { 75 | cursor: pointer; 76 | } 77 | table.dataTable thead .sorting, 78 | table.dataTable thead .sorting_asc, 79 | table.dataTable thead .sorting_desc, 80 | table.dataTable thead .sorting_asc_disabled, 81 | table.dataTable thead .sorting_desc_disabled { 82 | background-repeat: no-repeat; 83 | background-position: center right; 84 | } 85 | table.dataTable thead .sorting { 86 | background-image: url("../images/sort_both.png"); 87 | } 88 | table.dataTable thead .sorting_asc { 89 | background-image: url("../images/sort_asc.png"); 90 | } 91 | table.dataTable thead .sorting_desc { 92 | background-image: url("../images/sort_desc.png"); 93 | } 94 | table.dataTable thead .sorting_asc_disabled { 95 | background-image: url("../images/sort_asc_disabled.png"); 96 | } 97 | table.dataTable thead .sorting_desc_disabled { 98 | background-image: url("../images/sort_desc_disabled.png"); 99 | } 100 | 101 | div.dataTables_scrollHead table { 102 | margin-bottom: 0 !important; 103 | } 104 | 105 | div.dataTables_scrollBody table { 106 | border-top: none; 107 | margin-top: 0 !important; 108 | margin-bottom: 0 !important; 109 | } 110 | div.dataTables_scrollBody table tbody tr:first-child th, 111 | div.dataTables_scrollBody table tbody tr:first-child td { 112 | border-top: none; 113 | } 114 | 115 | div.dataTables_scrollFoot table { 116 | margin-top: 0 !important; 117 | border-top: none; 118 | } 119 | -------------------------------------------------------------------------------- /site_static/dataTables/css/dataTables.foundation.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable{clear:both;margin:0.5em 0 !important;max-width:none !important;width:100%}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper{position:relative}div.dataTables_wrapper div.dataTables_length label{float:left;text-align:left;margin-bottom:0}div.dataTables_wrapper div.dataTables_length select{width:75px;margin-bottom:0}div.dataTables_wrapper div.dataTables_filter label{float:right;margin-bottom:0}div.dataTables_wrapper div.dataTables_filter input{display:inline-block !important;width:auto !important;margin-bottom:0;margin-left:0.5em}div.dataTables_wrapper div.dataTables_info{padding-top:2px}div.dataTables_wrapper div.dataTables_paginate{float:right;margin:0}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1rem 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:1.5rem}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("../images/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("../images/sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("../images/sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("../images/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("../images/sort_desc_disabled.png")}div.dataTables_scrollHead table{margin-bottom:0 !important}div.dataTables_scrollBody table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody table tbody tr:first-child th,div.dataTables_scrollBody table tbody tr:first-child td{border-top:none}div.dataTables_scrollFoot table{margin-top:0 !important;border-top:none} 2 | -------------------------------------------------------------------------------- /site_static/dataTables/css/dataTables.semanticui.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Styling for DataTables with Semantic UI 3 | */ 4 | table.dataTable.table { 5 | margin: 0; 6 | } 7 | table.dataTable.table thead th, 8 | table.dataTable.table thead td { 9 | position: relative; 10 | } 11 | table.dataTable.table thead th.sorting, table.dataTable.table thead th.sorting_asc, table.dataTable.table thead th.sorting_desc, 12 | table.dataTable.table thead td.sorting, 13 | table.dataTable.table thead td.sorting_asc, 14 | table.dataTable.table thead td.sorting_desc { 15 | padding-right: 20px; 16 | } 17 | table.dataTable.table thead th.sorting:after, table.dataTable.table thead th.sorting_asc:after, table.dataTable.table thead th.sorting_desc:after, 18 | table.dataTable.table thead td.sorting:after, 19 | table.dataTable.table thead td.sorting_asc:after, 20 | table.dataTable.table thead td.sorting_desc:after { 21 | position: absolute; 22 | top: 12px; 23 | right: 8px; 24 | display: block; 25 | font-family: Icons; 26 | } 27 | table.dataTable.table thead th.sorting:after, 28 | table.dataTable.table thead td.sorting:after { 29 | content: "\f0dc"; 30 | color: #ddd; 31 | font-size: 0.8em; 32 | } 33 | table.dataTable.table thead th.sorting_asc:after, 34 | table.dataTable.table thead td.sorting_asc:after { 35 | content: "\f0de"; 36 | } 37 | table.dataTable.table thead th.sorting_desc:after, 38 | table.dataTable.table thead td.sorting_desc:after { 39 | content: "\f0dd"; 40 | } 41 | table.dataTable.table td, 42 | table.dataTable.table th { 43 | -webkit-box-sizing: content-box; 44 | box-sizing: content-box; 45 | } 46 | table.dataTable.table td.dataTables_empty, 47 | table.dataTable.table th.dataTables_empty { 48 | text-align: center; 49 | } 50 | table.dataTable.table.nowrap th, 51 | table.dataTable.table.nowrap td { 52 | white-space: nowrap; 53 | } 54 | 55 | div.dataTables_wrapper div.dataTables_length select { 56 | vertical-align: middle; 57 | min-height: 2.7142em; 58 | } 59 | div.dataTables_wrapper div.dataTables_length .ui.selection.dropdown { 60 | min-width: 0; 61 | } 62 | div.dataTables_wrapper div.dataTables_filter span.input { 63 | margin-left: 0.5em; 64 | } 65 | div.dataTables_wrapper div.dataTables_info { 66 | padding-top: 13px; 67 | white-space: nowrap; 68 | } 69 | div.dataTables_wrapper div.dataTables_processing { 70 | position: absolute; 71 | top: 50%; 72 | left: 50%; 73 | width: 200px; 74 | margin-left: -100px; 75 | text-align: center; 76 | } 77 | div.dataTables_wrapper div.row.dt-table { 78 | padding: 0; 79 | } 80 | div.dataTables_wrapper div.dataTables_scrollHead table.dataTable { 81 | border-bottom-right-radius: 0; 82 | border-bottom-left-radius: 0; 83 | border-bottom: none; 84 | } 85 | div.dataTables_wrapper div.dataTables_scrollBody thead .sorting:after, 86 | div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_asc:after, 87 | div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_desc:after { 88 | display: none; 89 | } 90 | div.dataTables_wrapper div.dataTables_scrollBody table.dataTable { 91 | border-radius: 0; 92 | border-top: none; 93 | border-bottom-width: 0; 94 | } 95 | div.dataTables_wrapper div.dataTables_scrollBody table.dataTable.no-footer { 96 | border-bottom-width: 1px; 97 | } 98 | div.dataTables_wrapper div.dataTables_scrollFoot table.dataTable { 99 | border-top-right-radius: 0; 100 | border-top-left-radius: 0; 101 | border-top: none; 102 | } 103 | -------------------------------------------------------------------------------- /site_static/dataTables/css/dataTables.semanticui.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable.table{margin:0}table.dataTable.table thead th,table.dataTable.table thead td{position:relative}table.dataTable.table thead th.sorting,table.dataTable.table thead th.sorting_asc,table.dataTable.table thead th.sorting_desc,table.dataTable.table thead td.sorting,table.dataTable.table thead td.sorting_asc,table.dataTable.table thead td.sorting_desc{padding-right:20px}table.dataTable.table thead th.sorting:after,table.dataTable.table thead th.sorting_asc:after,table.dataTable.table thead th.sorting_desc:after,table.dataTable.table thead td.sorting:after,table.dataTable.table thead td.sorting_asc:after,table.dataTable.table thead td.sorting_desc:after{position:absolute;top:12px;right:8px;display:block;font-family:Icons}table.dataTable.table thead th.sorting:after,table.dataTable.table thead td.sorting:after{content:"\f0dc";color:#ddd;font-size:0.8em}table.dataTable.table thead th.sorting_asc:after,table.dataTable.table thead td.sorting_asc:after{content:"\f0de"}table.dataTable.table thead th.sorting_desc:after,table.dataTable.table thead td.sorting_desc:after{content:"\f0dd"}table.dataTable.table td,table.dataTable.table th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable.table td.dataTables_empty,table.dataTable.table th.dataTables_empty{text-align:center}table.dataTable.table.nowrap th,table.dataTable.table.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{vertical-align:middle;min-height:2.7142em}div.dataTables_wrapper div.dataTables_length .ui.selection.dropdown{min-width:0}div.dataTables_wrapper div.dataTables_filter span.input{margin-left:0.5em}div.dataTables_wrapper div.dataTables_info{padding-top:13px;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;text-align:center}div.dataTables_wrapper div.row.dt-table{padding:0}div.dataTables_wrapper div.dataTables_scrollHead table.dataTable{border-bottom-right-radius:0;border-bottom-left-radius:0;border-bottom:none}div.dataTables_wrapper div.dataTables_scrollBody thead .sorting:after,div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_asc:after,div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_desc:after{display:none}div.dataTables_wrapper div.dataTables_scrollBody table.dataTable{border-radius:0;border-top:none;border-bottom-width:0}div.dataTables_wrapper div.dataTables_scrollBody table.dataTable.no-footer{border-bottom-width:1px}div.dataTables_wrapper div.dataTables_scrollFoot table.dataTable{border-top-right-radius:0;border-top-left-radius:0;border-top:none} 2 | -------------------------------------------------------------------------------- /site_static/dataTables/images/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/dataTables/images/sort_asc.png -------------------------------------------------------------------------------- /site_static/dataTables/images/sort_asc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/dataTables/images/sort_asc_disabled.png -------------------------------------------------------------------------------- /site_static/dataTables/images/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/dataTables/images/sort_both.png -------------------------------------------------------------------------------- /site_static/dataTables/images/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/dataTables/images/sort_desc.png -------------------------------------------------------------------------------- /site_static/dataTables/images/sort_desc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/dataTables/images/sort_desc_disabled.png -------------------------------------------------------------------------------- /site_static/dataTables/js/dataTables.bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! DataTables Bootstrap 3 integration 2 | * ©2011-2015 SpryMedia Ltd - datatables.net/license 3 | */ 4 | 5 | /** 6 | * DataTables integration for Bootstrap 3. This requires Bootstrap 3 and 7 | * DataTables 1.10 or newer. 8 | * 9 | * This file sets the defaults and adds options to DataTables to style its 10 | * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap 11 | * for further information. 12 | */ 13 | (function( factory ){ 14 | if ( typeof define === 'function' && define.amd ) { 15 | // AMD 16 | define( ['jquery', 'datatables.net'], function ( $ ) { 17 | return factory( $, window, document ); 18 | } ); 19 | } 20 | else if ( typeof exports === 'object' ) { 21 | // CommonJS 22 | module.exports = function (root, $) { 23 | if ( ! root ) { 24 | root = window; 25 | } 26 | 27 | if ( ! $ || ! $.fn.dataTable ) { 28 | // Require DataTables, which attaches to jQuery, including 29 | // jQuery if needed and have a $ property so we can access the 30 | // jQuery object that is used 31 | $ = require('datatables.net')(root, $).$; 32 | } 33 | 34 | return factory( $, root, root.document ); 35 | }; 36 | } 37 | else { 38 | // Browser 39 | factory( jQuery, window, document ); 40 | } 41 | }(function( $, window, document, undefined ) { 42 | 'use strict'; 43 | var DataTable = $.fn.dataTable; 44 | 45 | 46 | /* Set the defaults for DataTables initialisation */ 47 | $.extend( true, DataTable.defaults, { 48 | dom: 49 | "<'row'<'col-sm-6'l><'col-sm-6'f>>" + 50 | "<'row'<'col-sm-12'tr>>" + 51 | "<'row'<'col-sm-5'i><'col-sm-7'p>>", 52 | renderer: 'bootstrap' 53 | } ); 54 | 55 | 56 | /* Default class modification */ 57 | $.extend( DataTable.ext.classes, { 58 | sWrapper: "dataTables_wrapper form-inline dt-bootstrap", 59 | sFilterInput: "form-control input-sm", 60 | sLengthSelect: "form-control input-sm", 61 | sProcessing: "dataTables_processing panel panel-default" 62 | } ); 63 | 64 | 65 | /* Bootstrap paging button renderer */ 66 | DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) { 67 | var api = new DataTable.Api( settings ); 68 | var classes = settings.oClasses; 69 | var lang = settings.oLanguage.oPaginate; 70 | var aria = settings.oLanguage.oAria.paginate || {}; 71 | var btnDisplay, btnClass, counter=0; 72 | 73 | var attach = function( container, buttons ) { 74 | var i, ien, node, button; 75 | var clickHandler = function ( e ) { 76 | e.preventDefault(); 77 | if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) { 78 | api.page( e.data.action ).draw( 'page' ); 79 | } 80 | }; 81 | 82 | for ( i=0, ien=buttons.length ; i 0 ? 101 | '' : ' disabled'); 102 | break; 103 | 104 | case 'previous': 105 | btnDisplay = lang.sPrevious; 106 | btnClass = button + (page > 0 ? 107 | '' : ' disabled'); 108 | break; 109 | 110 | case 'next': 111 | btnDisplay = lang.sNext; 112 | btnClass = button + (page < pages-1 ? 113 | '' : ' disabled'); 114 | break; 115 | 116 | case 'last': 117 | btnDisplay = lang.sLast; 118 | btnClass = button + (page < pages-1 ? 119 | '' : ' disabled'); 120 | break; 121 | 122 | default: 123 | btnDisplay = button + 1; 124 | btnClass = page === button ? 125 | 'active' : ''; 126 | break; 127 | } 128 | 129 | if ( btnDisplay ) { 130 | node = $('
  • ', { 131 | 'class': classes.sPageButton+' '+btnClass, 132 | 'id': idx === 0 && typeof button === 'string' ? 133 | settings.sTableId +'_'+ button : 134 | null 135 | } ) 136 | .append( $('', { 137 | 'href': '#', 138 | 'aria-controls': settings.sTableId, 139 | 'aria-label': aria[ button ], 140 | 'data-dt-idx': counter, 141 | 'tabindex': settings.iTabIndex 142 | } ) 143 | .html( btnDisplay ) 144 | ) 145 | .appendTo( container ); 146 | 147 | settings.oApi._fnBindAction( 148 | node, {action: button}, clickHandler 149 | ); 150 | 151 | counter++; 152 | } 153 | } 154 | } 155 | }; 156 | 157 | // IE9 throws an 'unknown error' if document.activeElement is used 158 | // inside an iframe or frame. 159 | var activeEl; 160 | 161 | try { 162 | // Because this approach is destroying and recreating the paging 163 | // elements, focus is lost on the select button which is bad for 164 | // accessibility. So we want to restore focus once the draw has 165 | // completed 166 | activeEl = $(host).find(document.activeElement).data('dt-idx'); 167 | } 168 | catch (e) {} 169 | 170 | attach( 171 | $(host).empty().html('