├── .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 | 
48 |
49 | Computer Info:
50 | 
51 |
52 | User Key Request:
53 | 
54 |
55 | Manage Requests:
56 | 
57 |
58 | Approve Request:
59 | 
60 |
61 | Key Retrieval:
62 | 
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 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/server/templates/server/computer_info.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %} {% block script %}
2 |
17 |
18 | {% endblock %} {% block dropdown %}
19 |
20 | New secret
23 |
24 | {% endblock %} {% block nav %}
25 |
26 | Home
29 |
30 | {% endblock %} {% block content %}
31 |
32 |
33 |
{{ computer.computername }}
34 | {{ computer.serial }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Username |
44 | {{ computer.username }} |
45 |
46 |
47 | Computer Name |
48 | {{ computer.computername }} |
49 |
50 |
51 | Serial Number |
52 | {{ computer.serial }} |
53 |
54 |
55 | Last Checked In |
56 | {{ computer.last_checkin}} |
57 |
58 |
59 |
60 |
61 |
62 | {% block button %} {% endblock %}
63 |
64 |
65 |
66 |
67 |
68 |
Secrets
69 |
70 |
71 | Secret Type |
72 | Escrow Date |
73 | |
74 |
75 |
76 | {% for secret in secrets %}
77 |
78 | {{ secret.get_secret_type_display }} |
79 | {{ secret.date_escrowed }} |
80 |
81 | Info / Request
86 | |
87 |
88 |
89 | {% endfor %}
90 |
91 |
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 | New computer
41 | {% endblock %}
42 | {% block nav %}
43 | {% if perms.server.can_approve %}
44 | {% if outstanding.count > 0 %}
45 | Approve requests
46 | {% else %}
47 | Approve requests
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 | Serial Number |
67 | Computer Name |
68 | User Name |
69 | Last Checked In |
70 | |
71 |
72 |
73 |
74 | {% for computer in computers.all %}
75 | {{ computer.serial }} | {{ computer.computername }} | {{ computer.username }} | {{ computer.last_checkin }} | Info |
76 | {% endfor %}
77 |
78 |
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 | Home
21 | {% endblock %}
22 | {% block content %}
23 | Key Requests
24 |
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 |
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 |
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 |
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 | Home
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 | Computer
21 | {% endblock %}
22 | {% block content %}
23 |
24 |
25 |
{{ computer.computername }}
26 | {{ computer.serial }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Username | {{ computer.username }} |
35 | Computer Name | {{ computer.computername }} |
36 | Serial Number | {{ computer.serial }} |
37 | Secret Type | {{ secret.get_secret_type_display }} |
38 | Escrow Date | {{ secret.date_escrowed }} |
39 |
40 |
41 |
42 |
43 | {% block button %}
44 | {% endblock %}
45 |
46 |
47 |
48 | {% if perms.server.can_approve %}
49 |
50 |
51 |
Requests
52 |
53 |
54 | Requesting User |
55 | Reason for Request |
56 | Date Requested |
57 | Approved By |
58 | Approval Notes |
59 | Date Approved |
60 |
61 |
62 | {% for the_request in requests %}
63 |
64 |
65 | {{ the_request.requesting_user }} |
66 | {{ the_request.reason_for_request }} |
67 | {{ the_request.date_requested }} |
68 | {{ the_request.auth_user }} |
69 | {{ the_request.reason_for_approval }} |
70 | {{ the_request.date_approved }} |
71 |
72 |
73 | {% endfor %}
74 |
75 |
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('').children('ul'),
172 | buttons
173 | );
174 |
175 | if ( activeEl !== undefined ) {
176 | $(host).find( '[data-dt-idx='+activeEl+']' ).focus();
177 | }
178 | };
179 |
180 |
181 | return DataTable;
182 | }));
183 |
--------------------------------------------------------------------------------
/site_static/dataTables/js/dataTables.bootstrap.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | DataTables Bootstrap 3 integration
3 | ©2011-2015 SpryMedia Ltd - datatables.net/license
4 | */
5 | (function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d,m){var f=b.fn.dataTable;b.extend(!0,f.defaults,{dom:"<'row'<'col-sm-6'l><'col-sm-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-5'i><'col-sm-7'p>>",renderer:"bootstrap"});b.extend(f.ext.classes,
6 | {sWrapper:"dataTables_wrapper form-inline dt-bootstrap",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm",sProcessing:"dataTables_processing panel panel-default"});f.ext.renderer.pageButton.bootstrap=function(a,h,r,s,j,n){var o=new f.Api(a),t=a.oClasses,k=a.oLanguage.oPaginate,u=a.oLanguage.oAria.paginate||{},e,g,p=0,q=function(d,f){var l,h,i,c,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")};
7 | l=0;for(h=f.length;l",{"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("",{href:"#",
8 | "aria-controls":a.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:a.iTabIndex}).html(e)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(d.activeElement).data("dt-idx")}catch(v){}q(b(h).empty().html('').children("ul"),s);i!==m&&b(h).find("[data-dt-idx="+i+"]").focus()};return f});
9 |
--------------------------------------------------------------------------------
/site_static/dataTables/js/dataTables.bootstrap4.js:
--------------------------------------------------------------------------------
1 | /*! DataTables Bootstrap 4 integration
2 | * ©2011-2017 SpryMedia Ltd - datatables.net/license
3 | */
4 |
5 | /**
6 | * DataTables integration for Bootstrap 4. This requires Bootstrap 4 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-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
50 | "<'row'<'col-sm-12'tr>>" +
51 | "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
52 | renderer: 'bootstrap'
53 | } );
54 |
55 |
56 | /* Default class modification */
57 | $.extend( DataTable.ext.classes, {
58 | sWrapper: "dataTables_wrapper dt-bootstrap4",
59 | sFilterInput: "form-control form-control-sm",
60 | sLengthSelect: "custom-select custom-select-sm form-control form-control-sm",
61 | sProcessing: "dataTables_processing card",
62 | sPageButton: "paginate_button page-item"
63 | } );
64 |
65 |
66 | /* Bootstrap paging button renderer */
67 | DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) {
68 | var api = new DataTable.Api( settings );
69 | var classes = settings.oClasses;
70 | var lang = settings.oLanguage.oPaginate;
71 | var aria = settings.oLanguage.oAria.paginate || {};
72 | var btnDisplay, btnClass, counter=0;
73 |
74 | var attach = function( container, buttons ) {
75 | var i, ien, node, button;
76 | var clickHandler = function ( e ) {
77 | e.preventDefault();
78 | if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) {
79 | api.page( e.data.action ).draw( 'page' );
80 | }
81 | };
82 |
83 | for ( i=0, ien=buttons.length ; i 0 ?
102 | '' : ' disabled');
103 | break;
104 |
105 | case 'previous':
106 | btnDisplay = lang.sPrevious;
107 | btnClass = button + (page > 0 ?
108 | '' : ' disabled');
109 | break;
110 |
111 | case 'next':
112 | btnDisplay = lang.sNext;
113 | btnClass = button + (page < pages-1 ?
114 | '' : ' disabled');
115 | break;
116 |
117 | case 'last':
118 | btnDisplay = lang.sLast;
119 | btnClass = button + (page < pages-1 ?
120 | '' : ' disabled');
121 | break;
122 |
123 | default:
124 | btnDisplay = button + 1;
125 | btnClass = page === button ?
126 | 'active' : '';
127 | break;
128 | }
129 |
130 | if ( btnDisplay ) {
131 | node = $('', {
132 | 'class': classes.sPageButton+' '+btnClass,
133 | 'id': idx === 0 && typeof button === 'string' ?
134 | settings.sTableId +'_'+ button :
135 | null
136 | } )
137 | .append( $('', {
138 | 'href': '#',
139 | 'aria-controls': settings.sTableId,
140 | 'aria-label': aria[ button ],
141 | 'data-dt-idx': counter,
142 | 'tabindex': settings.iTabIndex,
143 | 'class': 'page-link'
144 | } )
145 | .html( btnDisplay )
146 | )
147 | .appendTo( container );
148 |
149 | settings.oApi._fnBindAction(
150 | node, {action: button}, clickHandler
151 | );
152 |
153 | counter++;
154 | }
155 | }
156 | }
157 | };
158 |
159 | // IE9 throws an 'unknown error' if document.activeElement is used
160 | // inside an iframe or frame.
161 | var activeEl;
162 |
163 | try {
164 | // Because this approach is destroying and recreating the paging
165 | // elements, focus is lost on the select button which is bad for
166 | // accessibility. So we want to restore focus once the draw has
167 | // completed
168 | activeEl = $(host).find(document.activeElement).data('dt-idx');
169 | }
170 | catch (e) {}
171 |
172 | attach(
173 | $(host).empty().html('').children('ul'),
174 | buttons
175 | );
176 |
177 | if ( activeEl !== undefined ) {
178 | $(host).find( '[data-dt-idx='+activeEl+']' ).focus();
179 | }
180 | };
181 |
182 |
183 | return DataTable;
184 | }));
185 |
--------------------------------------------------------------------------------
/site_static/dataTables/js/dataTables.bootstrap4.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | DataTables Bootstrap 4 integration
3 | ©2011-2017 SpryMedia Ltd - datatables.net/license
4 | */
5 | (function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d,m){var f=b.fn.dataTable;b.extend(!0,f.defaults,{dom:"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
6 | renderer:"bootstrap"});b.extend(f.ext.classes,{sWrapper:"dataTables_wrapper dt-bootstrap4",sFilterInput:"form-control form-control-sm",sLengthSelect:"custom-select custom-select-sm form-control form-control-sm",sProcessing:"dataTables_processing card",sPageButton:"paginate_button page-item"});f.ext.renderer.pageButton.bootstrap=function(a,h,r,s,j,n){var o=new f.Api(a),t=a.oClasses,k=a.oLanguage.oPaginate,u=a.oLanguage.oAria.paginate||{},e,g,p=0,q=function(d,f){var l,h,i,c,m=function(a){a.preventDefault();
7 | !b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")};l=0;for(h=f.length;l",
8 | {"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("",{href:"#","aria-controls":a.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:a.iTabIndex,"class":"page-link"}).html(e)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(d.activeElement).data("dt-idx")}catch(v){}q(b(h).empty().html('').children("ul"),s);i!==m&&b(h).find("[data-dt-idx="+i+"]").focus()};return f});
9 |
--------------------------------------------------------------------------------
/site_static/dataTables/js/dataTables.foundation.js:
--------------------------------------------------------------------------------
1 | /*! DataTables Foundation integration
2 | * ©2011-2015 SpryMedia Ltd - datatables.net/license
3 | */
4 |
5 | /**
6 | * DataTables integration for Foundation. This requires Foundation 5 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 Foundation. See http://datatables.net/manual/styling/foundation
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.net')(root, $).$;
29 | }
30 |
31 | return factory( $, root, root.document );
32 | };
33 | }
34 | else {
35 | // Browser
36 | factory( jQuery, window, document );
37 | }
38 | }(function( $, window, document, undefined ) {
39 | 'use strict';
40 | var DataTable = $.fn.dataTable;
41 |
42 | // Detect Foundation 5 / 6 as they have different element and class requirements
43 | var meta = $('').appendTo('head');
44 | DataTable.ext.foundationVersion = meta.css('font-family').match(/small|medium|large/) ? 6 : 5;
45 | meta.remove();
46 |
47 |
48 | $.extend( DataTable.ext.classes, {
49 | sWrapper: "dataTables_wrapper dt-foundation",
50 | sProcessing: "dataTables_processing panel callout"
51 | } );
52 |
53 |
54 | /* Set the defaults for DataTables initialisation */
55 | $.extend( true, DataTable.defaults, {
56 | dom:
57 | "<'row grid-x'<'small-6 columns cell'l><'small-6 columns cell'f>r>"+
58 | "t"+
59 | "<'row grid-x'<'small-6 columns cell'i><'small-6 columns cell'p>>",
60 | renderer: 'foundation'
61 | } );
62 |
63 |
64 | /* Page button renderer */
65 | DataTable.ext.renderer.pageButton.foundation = function ( settings, host, idx, buttons, page, pages ) {
66 | var api = new DataTable.Api( settings );
67 | var classes = settings.oClasses;
68 | var lang = settings.oLanguage.oPaginate;
69 | var aria = settings.oLanguage.oAria.paginate || {};
70 | var btnDisplay, btnClass;
71 | var tag;
72 | var v5 = DataTable.ext.foundationVersion === 5;
73 |
74 | var attach = function( container, buttons ) {
75 | var i, ien, node, button;
76 | var clickHandler = function ( e ) {
77 | e.preventDefault();
78 | if ( !$(e.currentTarget).hasClass('unavailable') && api.page() != e.data.action ) {
79 | api.page( e.data.action ).draw( 'page' );
80 | }
81 | };
82 |
83 | for ( i=0, ien=buttons.length ; i 0 ?
104 | '' : ' unavailable disabled');
105 | tag = page > 0 ? 'a' : null;
106 | break;
107 |
108 | case 'previous':
109 | btnDisplay = lang.sPrevious;
110 | btnClass = button + (page > 0 ?
111 | '' : ' unavailable disabled');
112 | tag = page > 0 ? 'a' : null;
113 | break;
114 |
115 | case 'next':
116 | btnDisplay = lang.sNext;
117 | btnClass = button + (page < pages-1 ?
118 | '' : ' unavailable disabled');
119 | tag = page < pages-1 ? 'a' : null;
120 | break;
121 |
122 | case 'last':
123 | btnDisplay = lang.sLast;
124 | btnClass = button + (page < pages-1 ?
125 | '' : ' unavailable disabled');
126 | tag = page < pages-1 ? 'a' : null;
127 | break;
128 |
129 | default:
130 | btnDisplay = button + 1;
131 | btnClass = page === button ?
132 | 'current' : '';
133 | tag = page === button ?
134 | null : 'a';
135 | break;
136 | }
137 |
138 | if ( v5 ) {
139 | tag = 'a';
140 | }
141 |
142 | if ( btnDisplay ) {
143 | node = $('', {
144 | 'class': classes.sPageButton+' '+btnClass,
145 | 'aria-controls': settings.sTableId,
146 | 'aria-label': aria[ button ],
147 | 'tabindex': settings.iTabIndex,
148 | 'id': idx === 0 && typeof button === 'string' ?
149 | settings.sTableId +'_'+ button :
150 | null
151 | } )
152 | .append( tag ?
153 | $('<'+tag+'/>', {'href': '#'} ).html( btnDisplay ) :
154 | btnDisplay
155 | )
156 | .appendTo( container );
157 |
158 | settings.oApi._fnBindAction(
159 | node, {action: button}, clickHandler
160 | );
161 | }
162 | }
163 | }
164 | };
165 |
166 | attach(
167 | $(host).empty().html('').children('ul'),
168 | buttons
169 | );
170 | };
171 |
172 |
173 | return DataTable;
174 | }));
175 |
--------------------------------------------------------------------------------
/site_static/dataTables/js/dataTables.foundation.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | DataTables Foundation integration
3 | ©2011-2015 SpryMedia Ltd - datatables.net/license
4 | */
5 | (function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return d(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);if(!b||!b.fn.dataTable)b=require("datatables.net")(a,b).$;return d(b,a,a.document)}:d(jQuery,window,document)})(function(d){var a=d.fn.dataTable,b=d('').appendTo("head");a.ext.foundationVersion=b.css("font-family").match(/small|medium|large/)?6:5;b.remove();d.extend(a.ext.classes,
6 | {sWrapper:"dataTables_wrapper dt-foundation",sProcessing:"dataTables_processing panel callout"});d.extend(!0,a.defaults,{dom:"<'row grid-x'<'small-6 columns cell'l><'small-6 columns cell'f>r>t<'row grid-x'<'small-6 columns cell'i><'small-6 columns cell'p>>",renderer:"foundation"});a.ext.renderer.pageButton.foundation=function(b,l,r,s,e,i){var m=new a.Api(b),t=b.oClasses,j=b.oLanguage.oPaginate,u=b.oLanguage.oAria.paginate||{},f,h,g,v=5===a.ext.foundationVersion,q=function(a,n){var k,o,p,c,l=function(a){a.preventDefault();
7 | !d(a.currentTarget).hasClass("unavailable")&&m.page()!=a.data.action&&m.page(a.data.action).draw("page")};k=0;for(o=n.length;k",{"class":t.sPageButton+" "+h,"aria-controls":b.sTableId,"aria-label":u[c],tabindex:b.iTabIndex,id:0===r&&"string"===typeof c?b.sTableId+"_"+c:null}).append(g?d("<"+g+"/>",{href:"#"}).html(f):f).appendTo(a),b.oApi._fnBindAction(p,{action:c},l))}};q(d(l).empty().html('').children("ul"),s)};return a});
9 |
--------------------------------------------------------------------------------
/site_static/dataTables/js/dataTables.jqueryui.js:
--------------------------------------------------------------------------------
1 | /*! DataTables jQuery UI integration
2 | * ©2011-2014 SpryMedia Ltd - datatables.net/license
3 | */
4 |
5 | /**
6 | * DataTables integration for jQuery UI. This requires jQuery UI 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 jQuery UI. See http://datatables.net/manual/styling/jqueryui
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.net')(root, $).$;
29 | }
30 |
31 | return factory( $, root, root.document );
32 | };
33 | }
34 | else {
35 | // Browser
36 | factory( jQuery, window, document );
37 | }
38 | }(function( $, window, document, undefined ) {
39 | 'use strict';
40 | var DataTable = $.fn.dataTable;
41 |
42 |
43 | var sort_prefix = 'css_right ui-icon ui-icon-';
44 | var toolbar_prefix = 'fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix ui-corner-';
45 |
46 | /* Set the defaults for DataTables initialisation */
47 | $.extend( true, DataTable.defaults, {
48 | dom:
49 | '<"'+toolbar_prefix+'tl ui-corner-tr"lfr>'+
50 | 't'+
51 | '<"'+toolbar_prefix+'bl ui-corner-br"ip>',
52 | renderer: 'jqueryui'
53 | } );
54 |
55 |
56 | $.extend( DataTable.ext.classes, {
57 | "sWrapper": "dataTables_wrapper dt-jqueryui",
58 |
59 | /* Full numbers paging buttons */
60 | "sPageButton": "fg-button ui-button ui-state-default",
61 | "sPageButtonActive": "ui-state-disabled",
62 | "sPageButtonDisabled": "ui-state-disabled",
63 |
64 | /* Features */
65 | "sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+
66 | "ui-buttonset-multi paging_", /* Note that the type is postfixed */
67 |
68 | /* Sorting */
69 | "sSortAsc": "ui-state-default sorting_asc",
70 | "sSortDesc": "ui-state-default sorting_desc",
71 | "sSortable": "ui-state-default sorting",
72 | "sSortableAsc": "ui-state-default sorting_asc_disabled",
73 | "sSortableDesc": "ui-state-default sorting_desc_disabled",
74 | "sSortableNone": "ui-state-default sorting_disabled",
75 | "sSortIcon": "DataTables_sort_icon",
76 |
77 | /* Scrolling */
78 | "sScrollHead": "dataTables_scrollHead "+"ui-state-default",
79 | "sScrollFoot": "dataTables_scrollFoot "+"ui-state-default",
80 |
81 | /* Misc */
82 | "sHeaderTH": "ui-state-default",
83 | "sFooterTH": "ui-state-default"
84 | } );
85 |
86 |
87 | DataTable.ext.renderer.header.jqueryui = function ( settings, cell, column, classes ) {
88 | // Calculate what the unsorted class should be
89 | var noSortAppliedClass = sort_prefix+'caret-2-n-s';
90 | var asc = $.inArray('asc', column.asSorting) !== -1;
91 | var desc = $.inArray('desc', column.asSorting) !== -1;
92 |
93 | if ( !column.bSortable || (!asc && !desc) ) {
94 | noSortAppliedClass = '';
95 | }
96 | else if ( asc && !desc ) {
97 | noSortAppliedClass = sort_prefix+'caret-1-n';
98 | }
99 | else if ( !asc && desc ) {
100 | noSortAppliedClass = sort_prefix+'caret-1-s';
101 | }
102 |
103 | // Setup the DOM structure
104 | $('')
105 | .addClass( 'DataTables_sort_wrapper' )
106 | .append( cell.contents() )
107 | .append( $('')
108 | .addClass( classes.sSortIcon+' '+noSortAppliedClass )
109 | )
110 | .appendTo( cell );
111 |
112 | // Attach a sort listener to update on sort
113 | $(settings.nTable).on( 'order.dt', function ( e, ctx, sorting, columns ) {
114 | if ( settings !== ctx ) {
115 | return;
116 | }
117 |
118 | var colIdx = column.idx;
119 |
120 | cell
121 | .removeClass( classes.sSortAsc +" "+classes.sSortDesc )
122 | .addClass( columns[ colIdx ] == 'asc' ?
123 | classes.sSortAsc : columns[ colIdx ] == 'desc' ?
124 | classes.sSortDesc :
125 | column.sSortingClass
126 | );
127 |
128 | cell
129 | .find( 'span.'+classes.sSortIcon )
130 | .removeClass(
131 | sort_prefix+'triangle-1-n' +" "+
132 | sort_prefix+'triangle-1-s' +" "+
133 | sort_prefix+'caret-2-n-s' +" "+
134 | sort_prefix+'caret-1-n' +" "+
135 | sort_prefix+'caret-1-s'
136 | )
137 | .addClass( columns[ colIdx ] == 'asc' ?
138 | sort_prefix+'triangle-1-n' : columns[ colIdx ] == 'desc' ?
139 | sort_prefix+'triangle-1-s' :
140 | noSortAppliedClass
141 | );
142 | } );
143 | };
144 |
145 |
146 | /*
147 | * TableTools jQuery UI compatibility
148 | * Required TableTools 2.1+
149 | */
150 | if ( DataTable.TableTools ) {
151 | $.extend( true, DataTable.TableTools.classes, {
152 | "container": "DTTT_container ui-buttonset ui-buttonset-multi",
153 | "buttons": {
154 | "normal": "DTTT_button ui-button ui-state-default"
155 | },
156 | "collection": {
157 | "container": "DTTT_collection ui-buttonset ui-buttonset-multi"
158 | }
159 | } );
160 | }
161 |
162 |
163 | return DataTable;
164 | }));
165 |
--------------------------------------------------------------------------------
/site_static/dataTables/js/dataTables.jqueryui.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | DataTables jQuery UI integration
3 | ©2011-2014 SpryMedia Ltd - datatables.net/license
4 | */
5 | (function(a){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(b){return a(b,window,document)}):"object"===typeof exports?module.exports=function(b,d){b||(b=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(b,d).$;return a(d,b,b.document)}:a(jQuery,window,document)})(function(a){var b=a.fn.dataTable;a.extend(!0,b.defaults,{dom:'<"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix ui-corner-tl ui-corner-tr"lfr>t<"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix ui-corner-bl ui-corner-br"ip>',
6 | renderer:"jqueryui"});a.extend(b.ext.classes,{sWrapper:"dataTables_wrapper dt-jqueryui",sPageButton:"fg-button ui-button ui-state-default",sPageButtonActive:"ui-state-disabled",sPageButtonDisabled:"ui-state-disabled",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",sSortAsc:"ui-state-default sorting_asc",sSortDesc:"ui-state-default sorting_desc",sSortable:"ui-state-default sorting",sSortableAsc:"ui-state-default sorting_asc_disabled",sSortableDesc:"ui-state-default sorting_desc_disabled",
7 | sSortableNone:"ui-state-default sorting_disabled",sSortIcon:"DataTables_sort_icon",sScrollHead:"dataTables_scrollHead ui-state-default",sScrollFoot:"dataTables_scrollFoot ui-state-default",sHeaderTH:"ui-state-default",sFooterTH:"ui-state-default"});b.ext.renderer.header.jqueryui=function(b,h,e,c){var f="css_right ui-icon ui-icon-caret-2-n-s",g=-1!==a.inArray("asc",e.asSorting),i=-1!==a.inArray("desc",e.asSorting);!e.bSortable||!g&&!i?f="":g&&!i?f="css_right ui-icon ui-icon-caret-1-n":!g&&i&&(f="css_right ui-icon ui-icon-caret-1-s");
8 | a("").addClass("DataTables_sort_wrapper").append(h.contents()).append(a("").addClass(c.sSortIcon+" "+f)).appendTo(h);a(b.nTable).on("order.dt",function(a,g,i,j){b===g&&(a=e.idx,h.removeClass(c.sSortAsc+" "+c.sSortDesc).addClass("asc"==j[a]?c.sSortAsc:"desc"==j[a]?c.sSortDesc:e.sSortingClass),h.find("span."+c.sSortIcon).removeClass("css_right ui-icon ui-icon-triangle-1-n css_right ui-icon ui-icon-triangle-1-s css_right ui-icon ui-icon-caret-2-n-s css_right ui-icon ui-icon-caret-1-n css_right ui-icon ui-icon-caret-1-s").addClass("asc"==
9 | j[a]?"css_right ui-icon ui-icon-triangle-1-n":"desc"==j[a]?"css_right ui-icon ui-icon-triangle-1-s":f))})};b.TableTools&&a.extend(!0,b.TableTools.classes,{container:"DTTT_container ui-buttonset ui-buttonset-multi",buttons:{normal:"DTTT_button ui-button ui-state-default"},collection:{container:"DTTT_collection ui-buttonset ui-buttonset-multi"}});return b});
10 |
--------------------------------------------------------------------------------
/site_static/dataTables/js/dataTables.semanticui.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 | "<'ui stackable grid'"+
50 | "<'row'"+
51 | "<'eight wide column'l>"+
52 | "<'right aligned eight wide column'f>"+
53 | ">"+
54 | "<'row dt-table'"+
55 | "<'sixteen wide column'tr>"+
56 | ">"+
57 | "<'row'"+
58 | "<'seven wide column'i>"+
59 | "<'right aligned nine wide column'p>"+
60 | ">"+
61 | ">",
62 | renderer: 'semanticUI'
63 | } );
64 |
65 |
66 | /* Default class modification */
67 | $.extend( DataTable.ext.classes, {
68 | sWrapper: "dataTables_wrapper dt-semanticUI",
69 | sFilter: "dataTables_filter ui input",
70 | sProcessing: "dataTables_processing ui segment",
71 | sPageButton: "paginate_button item"
72 | } );
73 |
74 |
75 | /* Bootstrap paging button renderer */
76 | DataTable.ext.renderer.pageButton.semanticUI = function ( settings, host, idx, buttons, page, pages ) {
77 | var api = new DataTable.Api( settings );
78 | var classes = settings.oClasses;
79 | var lang = settings.oLanguage.oPaginate;
80 | var aria = settings.oLanguage.oAria.paginate || {};
81 | var btnDisplay, btnClass, counter=0;
82 |
83 | var attach = function( container, buttons ) {
84 | var i, ien, node, button;
85 | var clickHandler = function ( e ) {
86 | e.preventDefault();
87 | if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) {
88 | api.page( e.data.action ).draw( 'page' );
89 | }
90 | };
91 |
92 | for ( i=0, ien=buttons.length ; i 0 ?
111 | '' : ' disabled');
112 | break;
113 |
114 | case 'previous':
115 | btnDisplay = lang.sPrevious;
116 | btnClass = button + (page > 0 ?
117 | '' : ' disabled');
118 | break;
119 |
120 | case 'next':
121 | btnDisplay = lang.sNext;
122 | btnClass = button + (page < pages-1 ?
123 | '' : ' disabled');
124 | break;
125 |
126 | case 'last':
127 | btnDisplay = lang.sLast;
128 | btnClass = button + (page < pages-1 ?
129 | '' : ' disabled');
130 | break;
131 |
132 | default:
133 | btnDisplay = button + 1;
134 | btnClass = page === button ?
135 | 'active' : '';
136 | break;
137 | }
138 |
139 | var tag = btnClass.indexOf( 'disabled' ) === -1 ?
140 | 'a' :
141 | 'div';
142 |
143 | if ( btnDisplay ) {
144 | node = $('<'+tag+'>', {
145 | 'class': classes.sPageButton+' '+btnClass,
146 | 'id': idx === 0 && typeof button === 'string' ?
147 | settings.sTableId +'_'+ button :
148 | null,
149 | 'href': '#',
150 | 'aria-controls': settings.sTableId,
151 | 'aria-label': aria[ button ],
152 | 'data-dt-idx': counter,
153 | 'tabindex': settings.iTabIndex
154 | } )
155 | .html( btnDisplay )
156 | .appendTo( container );
157 |
158 | settings.oApi._fnBindAction(
159 | node, {action: button}, clickHandler
160 | );
161 |
162 | counter++;
163 | }
164 | }
165 | }
166 | };
167 |
168 | // IE9 throws an 'unknown error' if document.activeElement is used
169 | // inside an iframe or frame.
170 | var activeEl;
171 |
172 | try {
173 | // Because this approach is destroying and recreating the paging
174 | // elements, focus is lost on the select button which is bad for
175 | // accessibility. So we want to restore focus once the draw has
176 | // completed
177 | activeEl = $(host).find(document.activeElement).data('dt-idx');
178 | }
179 | catch (e) {}
180 |
181 | attach(
182 | $(host).empty().html('').children(),
183 | buttons
184 | );
185 |
186 | if ( activeEl !== undefined ) {
187 | $(host).find( '[data-dt-idx='+activeEl+']' ).focus();
188 | }
189 | };
190 |
191 |
192 | // Javascript enhancements on table initialisation
193 | $(document).on( 'init.dt', function (e, ctx) {
194 | if ( e.namespace !== 'dt' ) {
195 | return;
196 | }
197 |
198 | var api = new $.fn.dataTable.Api( ctx );
199 |
200 | // Length menu drop down
201 | if ( $.fn.dropdown ) {
202 | $( 'div.dataTables_length select', api.table().container() ).dropdown();
203 | }
204 |
205 | // Filtering input
206 | $( 'div.dataTables_filter.ui.input', api.table().container() ).removeClass('input').addClass('form');
207 | $( 'div.dataTables_filter input', api.table().container() ).wrap( '' );
208 | } );
209 |
210 |
211 | return DataTable;
212 | }));
213 |
--------------------------------------------------------------------------------
/site_static/dataTables/js/dataTables.semanticui.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | DataTables Bootstrap 3 integration
3 | ©2011-2015 SpryMedia Ltd - datatables.net/license
4 | */
5 | (function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,e){a||(a=window);if(!e||!e.fn.dataTable)e=require("datatables.net")(a,e).$;return b(e,a,a.document)}:b(jQuery,window,document)})(function(b,a,e,m){var c=b.fn.dataTable;b.extend(!0,c.defaults,{dom:"<'ui stackable grid'<'row'<'eight wide column'l><'right aligned eight wide column'f>><'row dt-table'<'sixteen wide column'tr>><'row'<'seven wide column'i><'right aligned nine wide column'p>>>",
6 | renderer:"semanticUI"});b.extend(c.ext.classes,{sWrapper:"dataTables_wrapper dt-semanticUI",sFilter:"dataTables_filter ui input",sProcessing:"dataTables_processing ui segment",sPageButton:"paginate_button item"});c.ext.renderer.pageButton.semanticUI=function(h,a,r,s,j,n){var o=new c.Api(h),t=h.oClasses,k=h.oLanguage.oPaginate,u=h.oLanguage.oAria.paginate||{},f,g,p=0,q=function(a,e){var c,i,l,d,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")};
7 | c=0;for(i=e.length;c",{"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof d?
8 | h.sTableId+"_"+d:null,href:"#","aria-controls":h.sTableId,"aria-label":u[d],"data-dt-idx":p,tabindex:h.iTabIndex}).html(f).appendTo(a),h.oApi._fnBindAction(l,{action:d},m),p++)}},i;try{i=b(a).find(e.activeElement).data("dt-idx")}catch(v){}q(b(a).empty().html('').children(),s);i!==m&&b(a).find("[data-dt-idx="+i+"]").focus()};b(e).on("init.dt",function(a,e){if("dt"===a.namespace){var c=new b.fn.dataTable.Api(e);b.fn.dropdown&&b("div.dataTables_length select",
9 | c.table().container()).dropdown();b("div.dataTables_filter.ui.input",c.table().container()).removeClass("input").addClass("form");b("div.dataTables_filter input",c.table().container()).wrap('')}});return c});
10 |
--------------------------------------------------------------------------------
/site_static/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/site_static/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/site_static/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/site_static/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/site_static/img/sal-logo-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
--------------------------------------------------------------------------------
/site_static/img/select2-spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/img/select2-spinner.gif
--------------------------------------------------------------------------------
/site_static/img/select2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/img/select2.png
--------------------------------------------------------------------------------
/site_static/img/select2x2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grahamgilbert/Crypt-Server/1c6879feb7fb429b06219a318fcb80731afd4fc3/site_static/img/select2x2.png
--------------------------------------------------------------------------------
/site_static/js/main.js:
--------------------------------------------------------------------------------
1 | (function () { 'use strict';
2 |
3 | $( document ).ready( function() {
4 | $( '#id_munki_item' ).select2({
5 | placeholder: "select software",
6 | });
7 | });
8 |
9 | }()); // end 'use strict'
--------------------------------------------------------------------------------
/site_static/style.css:
--------------------------------------------------------------------------------
1 |
2 | h1, h2, h3, h4, h5, h6, h3 a{
3 | font-family: 'Lato', "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
4 | font-weight:normal;
5 | color: black;
6 | }
7 |
8 | code {
9 | background-color: #444;
10 | padding: 5px;
11 | }
12 | code span {
13 | padding-left: 2px;
14 | padding-right: 2px;
15 | font-size: 1.25em;
16 | }
17 | code span.letter {
18 | color: #fff;
19 | }
20 | code span.number {
21 | color: #a1bcea;
22 | }
23 | code span.other {
24 | color: #f2a5bd;
25 | }
26 |
--------------------------------------------------------------------------------
/smtp.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | sudo python -m smtpd -n -c DebuggingServer localhost:25
3 |
--------------------------------------------------------------------------------
/static/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
5 |
--------------------------------------------------------------------------------
/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Page not found{% endblock %}
4 |
5 | {% block content %}
6 | Page not found
7 |
8 | Sorry, but the requested page could not be found.
9 | {% endblock %}
--------------------------------------------------------------------------------
/templates/500.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | Page unavailable
6 |
7 |
8 | Page unavailable
9 |
10 | Sorry, but the requested page is unavailable due to a
11 | server hiccup.
12 |
13 | Our engineers have been notified, so check back later.
14 |
15 |
--------------------------------------------------------------------------------
/templates/admin/base_site.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base.html" %}
2 | {% load i18n %}
3 |
4 | {% block title %}Crypt{% endblock %}
5 | {% block extrastyle %}
6 |
17 | {% endblock %}
18 |
19 | {% block branding %}
20 | Crypt
21 | {% endblock %}
22 |
23 | {% block nav-global %}{% endblock %}
24 |
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load bootstrap4 %}
3 |
4 |
5 |
6 |
7 | {% block title %}Crypt{% endblock %}
8 |
9 |
10 |
11 |
12 |
13 |
14 | {# Load CSS and JavaScript #}
15 |
18 |
21 | {% bootstrap_javascript jquery='full' %}
22 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
81 |
82 | {% block content %}
83 |
84 | {% endblock %}
85 |
86 |
Crypt Server version {{ CRYPT_VERSION }}
87 |
88 |
89 |
90 | {% block script %}
91 | {% endblock%}
92 |
93 |
94 |
--------------------------------------------------------------------------------
/templates/registration/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 | {% if user.is_authenticated %}
5 | Welcome, {{ user.username }}.
6 |
7 | {% else %}
8 | {% if form.errors %}
9 | Your username and password didn't match. Please try again.
10 | {% else %}
11 | Please log in.
12 | {% endif %}
13 |
14 |
30 | {% endif %}
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/templates/registration/password_change_done.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load bootstap_toolkit }
3 | {% block content %}
4 |
5 | Password changed!
6 | Back to main page
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/templates/registration/password_change_form.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load bootstap_toolkit }
3 | {% block content %}
4 |
5 | Change password
6 |
7 |
8 |
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------