diff --git a/.circleci/config.yml b/.circleci/config.yml index 0e17a8f4..1976976a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -195,6 +195,28 @@ jobs: name: "Update Kubernetes Deployment" command: ./deploy/kubernetes/atst-update-deploy.sh + deploy_test: + docker: + - image: *sourceImage + auth: *sourceAuth + environment: *dockerCmdEnvironment + working_directory: *workingDirectory + steps: + - attach_workspace: + at: . + - run: + name: "Export GIT_SHA" + command: echo "export GIT_SHA=$(git rev-parse --short HEAD)" >> $BASH_ENV + - run: + name: "Generate the Target Image Name" + command: echo "export IMAGE_NAME=\"${ATAT_DOCKER_REGISTRY_URL}/${PROD_IMAGE_NAME}:${GIT_SHA}\"" >> $BASH_ENV + - run: + name: "Update Kubernetes Deployment" + command: ./deploy/kubernetes/atst-update-deploy.sh atat-test + - run: + name: "Reset the Sample Data" + command: ./deploy/kubernetes/atst-reset-sample-data.sh atat-test + workflows: version: 2 run-tests: @@ -215,3 +237,30 @@ workflows: filters: branches: only: master + nightly: + triggers: + - schedule: + cron: "0 0 * * *" + filters: + branches: + only: + - master + jobs: + - app_setup + - test: + requires: + - app_setup + - build_and_push_image: + requires: + - test + filters: + branches: + only: + - master + - deploy_test: + requires: + - build_and_push_image + filters: + branches: + only: + - master diff --git a/.gitignore b/.gitignore index e161f040..c7205185 100644 --- a/.gitignore +++ b/.gitignore @@ -45,8 +45,12 @@ ssl/client-certs/*.srl # uploads /uploads -# coverage output +# python coverage output .coverage +# je coverage output +coverage + + # selenium testing browserstacklocal diff --git a/atst/filters.py b/atst/filters.py index c172a3df..32000105 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -1,6 +1,7 @@ import re import datetime -from flask import current_app as app +from flask import current_app as app, render_template +from jinja2.exceptions import TemplateNotFound def iconSvg(name): @@ -86,6 +87,14 @@ def pageWindow(pagination, size=2): return (max(1, (page - size) - over), min(num_pages, (page + size) - under)) +def renderAuditEvent(event): + template_name = "audit_log/events/{}.html".format(event.resource_type) + try: + return render_template(template_name, event=event) + except TemplateNotFound: + return render_template("audit_log/events/default.html", event=event) + + def register_filters(app): app.jinja_env.filters["iconSvg"] = iconSvg app.jinja_env.filters["dollars"] = dollars @@ -98,3 +107,4 @@ def register_filters(app): app.jinja_env.filters["formattedDate"] = formattedDate app.jinja_env.filters["dateFromString"] = dateFromString app.jinja_env.filters["pageWindow"] = pageWindow + app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index 339cd219..2a1d23bc 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -137,7 +137,7 @@ def get_pagination_opts(request, default_page=1, default_per_page=100): def activity_history(): pagination_opts = get_pagination_opts(request) audit_events = AuditLog.get_all_events(g.current_user, pagination_opts) - return render_template("audit_log.html", audit_events=audit_events) + return render_template("audit_log/audit_log.html", audit_events=audit_events) @bp.route("/about") diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md new file mode 100644 index 00000000..3f3b5eb2 --- /dev/null +++ b/deploy/kubernetes/README.md @@ -0,0 +1,81 @@ +# Adding a New Environment + +## New Config + +Add a new subfolder to this directory. You can copy `uat` or `tests`. You'll need to change any references to the previous environment name (i.e., `uat`) to your new environment name. This includes things like the k8s namespace and the subdomain for the new site. + +## New Load Balancers + +You need two new load balancers. Currently, these are managed in Rackspace. You will need one for the regular site and one for the auth domain. They should have all of the Kubernetes worker nodes attached. When attached to the LB for the main site domain, the nodes should point to port 32761, which is the port where all our NGINX ingress is managed. The auth LB nodes should point to a new port in the 32700 - 32799 range. The `nodePort` specified in your new environment's config should match this port. + +## Initially Apply the New Config + +Apply your new environment config to create the namespace. Pod creation will fail at this point. + +``` +kubectl -n my-new-env apply -f deploy/kubernetes/my-new-env/ +``` + +## Create New Secrets + +You should then copy and duplicate the secrets from another environment. + +### Adjust the INI Config for the New Site + +You can duplicate the override.ini file from one of the existing sites. For instance, to get the `atst-config-ini` secret for the UAT env: + +``` +kubectl -n atat-uat get secret atst-config-ini -o yaml > uat-secrets.yml +``` + +You can copy the base64 secret content from the `uat-secrets.yml` file. Decode it into a new `override.ini` file: + +``` +echo '[Paste in the long base64 string here]' | base64 --decode > override.ini +``` + +Edit and adjust the application config as needed for your new site. Then add it as a secret: + +``` +kubectl -n my-new-env create secret generic atst-config-ini --from-file=override.ini +``` + +### Add a New htpasswd + +Create a new htpasswd to protect the dev login of the new site: + +``` +htpasswd -c htpasswd atat +``` + +You'll be prompted for the new password. Then add it as a secret: + +``` +kubectl -n my-new-env create secret generic atst-nginx-htpasswd --from-file=htpasswd +``` + +### Duplicate the Rest + +You should also the `dhparam-4096` and `nginx-client-ca-bundle` secrets. These can be copied from an existing environment to yours without any changes. The TLS secrets and token will be handled by another service. + +## Disable SSL + +In order for [kube-lego](https://github.com/jetstack/kube-lego) to generate new certs for your site, you have to temporarily disable SSL for your new load balancers. + +For both your LBs, set them to use HTTP/80. Delete the attached nodes for both and re-add them, setting them to use port 32760. This will allow kube-lego to do its job. + +To monitor the process, find the pod ID for the kube-lego worker. To find it, look in the output for: + +``` +kubectl -n kube-system get all +``` + +Then once you know the pod name: + +``` +kubectl -n kube-system --kubeconfig ~/.kube/atat log kube-lego-b96b7bc5c-9fmcv -f --tail=10 +``` + +You will see output about kube-lego attempts to create certs for your new site. + +Once kube-lego is successful, you should restore your load balancers to the config they had initially. Additionally, you should enable HTTPS redirects. diff --git a/deploy/kubernetes/atst-deployer.yml b/deploy/kubernetes/atst-deployer.yml index ed7950d9..77d94a1b 100644 --- a/deploy/kubernetes/atst-deployer.yml +++ b/deploy/kubernetes/atst-deployer.yml @@ -32,6 +32,9 @@ subjects: - kind: ServiceAccount name: atat-deployer namespace: atat +- kind: ServiceAccount + name: atat-deployer + namespace: atat-test roleRef: kind: Role name: atat-deploy-role diff --git a/deploy/kubernetes/atst-reset-sample-data.sh b/deploy/kubernetes/atst-reset-sample-data.sh new file mode 100755 index 00000000..32e0ea06 --- /dev/null +++ b/deploy/kubernetes/atst-reset-sample-data.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# deploy/kubernetes/atst-update-deploy.sh: Resets the sample data on the target +# environment. + +set -o pipefail +set -o errexit +set -o nounset +# set -o xtrace + +# Config +MAX_DEPLOY_WAIT='300' + +if [[ $# -eq 0 ]]; then + NAMESPACE=atat +else + NAMESPACE=$1 +fi + +# Remove the K8S CA file when the script exits +function cleanup { + printf "Cleaning up...\n" + rm -vf "${HOME}/k8s_ca.crt" + printf "Cleaning done." +} +trap cleanup EXIT + +# Decode and save the K8S CA cert +echo "${K8S_CA_CRT}" | base64 -d - > "${HOME}/k8s_ca.crt" + +# Setup the local kubectl client +kubectl config set-context atst-deployer \ + --cluster=atat-cluster \ + --user=atat-deployer \ + --namespace=${NAMESPACE} + +kubectl config set-cluster atat-cluster \ + --embed-certs=true \ + --server="${K8S_ENDPOINT}" \ + --certificate-authority="${HOME}/k8s_ca.crt" + +kubectl config set-credentials atat-deployer --token="$(echo ${K8S_USER_TOKEN} | base64 -d -)" + +kubectl config use-context atst-deployer +kubectl config current-context + +# we only need to run these commands against one existing pod +ATST_POD=$(kubectl -n ${NAMESPACE} get pods -l app=atst -o custom-columns=NAME:.metadata.name --no-headers | sed -n 1p) +# echo "kubectl -n ${NAMESPACE} exec ${ATST_POD} -- pipenv run python script/remove_sample_data.py" +echo "removing sample data on pod ${ATST_POD}" +kubectl -n ${NAMESPACE} exec ${ATST_POD} -- pipenv run python script/remove_sample_data.py +echo "seeding sample data on pod ${ATST_POD}" +kubectl -n ${NAMESPACE} exec ${ATST_POD} -- pipenv run python script/seed_sample.py + diff --git a/deploy/kubernetes/atst-update-deploy.sh b/deploy/kubernetes/atst-update-deploy.sh index ff6d93e7..d9ade43e 100755 --- a/deploy/kubernetes/atst-update-deploy.sh +++ b/deploy/kubernetes/atst-update-deploy.sh @@ -11,6 +11,12 @@ set -o nounset # Config MAX_DEPLOY_WAIT='300' +if [[ $# -eq 0 ]]; then + NAMESPACE=atat +else + NAMESPACE=$1 +fi + if [ "${IMAGE_NAME}x" = "x" ] then IMAGE_NAME="${ATAT_DOCKER_REGISTRY_URL}/${PROD_IMAGE_NAME}:${GIT_SHA}" @@ -31,7 +37,7 @@ echo "${K8S_CA_CRT}" | base64 -d - > "${HOME}/k8s_ca.crt" kubectl config set-context atst-deployer \ --cluster=atat-cluster \ --user=atat-deployer \ - --namespace=atat + --namespace=${NAMESPACE} kubectl config set-cluster atat-cluster \ --embed-certs=true \ @@ -44,15 +50,15 @@ kubectl config use-context atst-deployer kubectl config current-context # Update the ATST deployment -kubectl -n atat set image deployment.apps/atst atst="${IMAGE_NAME}" -kubectl -n atat set image deployment.apps/atst-worker atst-worker="${IMAGE_NAME}" +kubectl -n ${NAMESPACE} set image deployment.apps/atst atst="${IMAGE_NAME}" +kubectl -n ${NAMESPACE} set image deployment.apps/atst-worker atst-worker="${IMAGE_NAME}" # Wait for deployment to finish -if ! timeout -t "${MAX_DEPLOY_WAIT}" -s INT kubectl -n atat rollout status deployment/atst +if ! timeout -t "${MAX_DEPLOY_WAIT}" -s INT kubectl -n ${NAMESPACE} rollout status deployment/atst then # Deploy did not finish before max wait time; abort and rollback the deploy - kubectl -n atat rollout undo deployment/atst - kubectl -n atat rollout undo deployment/atst-worker + kubectl -n ${NAMESPACE} rollout undo deployment/atst + kubectl -n ${NAMESPACE} rollout undo deployment/atst-worker # Exit with a non-zero return code exit 2 fi diff --git a/deploy/kubernetes/test/atat-deploy-role.yml b/deploy/kubernetes/test/atat-deploy-role.yml new file mode 100644 index 00000000..8a7b6f85 --- /dev/null +++ b/deploy/kubernetes/test/atat-deploy-role.yml @@ -0,0 +1,26 @@ +kind: Role +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + namespace: atat-test + name: atat-sample-update +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] +- apiGroups: [""] + resources: ["pods/exec"] + verbs: ["create"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: atst-sample-role-binding + namespace: atat-test +subjects: +- kind: ServiceAccount + name: atat-deployer + namespace: atat +roleRef: + kind: Role + name: atat-sample-update + apiGroup: rbac.authorization.k8s.io diff --git a/deploy/kubernetes/test/atst-configmap.yml b/deploy/kubernetes/test/atst-configmap.yml new file mode 100644 index 00000000..54409d7d --- /dev/null +++ b/deploy/kubernetes/test/atst-configmap.yml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: atst-config + namespace: atat-test +data: + uwsgi-config: |- + [uwsgi] + callable = app + module = app + socket = /var/run/uwsgi/uwsgi.socket + plugins = python3 + virtualenv = /opt/atat/atst/.venv + chmod-socket = 666 diff --git a/deploy/kubernetes/test/atst-envvars-configmap.yml b/deploy/kubernetes/test/atst-envvars-configmap.yml new file mode 100644 index 00000000..94c24d9a --- /dev/null +++ b/deploy/kubernetes/test/atst-envvars-configmap.yml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: atst-envvars + namespace: atat-test +data: + FLASK_ENV: dev + OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst/atst-overrides.ini + UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi-config.ini + RQ_QUEUES: atat-test diff --git a/deploy/kubernetes/test/atst-nginx-configmap.yml b/deploy/kubernetes/test/atst-nginx-configmap.yml new file mode 100644 index 00000000..a425c818 --- /dev/null +++ b/deploy/kubernetes/test/atst-nginx-configmap.yml @@ -0,0 +1,79 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: atst-nginx + namespace: atat-test +data: + nginx-config: |- + server { + server_name test.atat.code.mil; + listen 8442; + listen [::]:8442 ipv6only=on; + if ($http_x_forwarded_proto != 'https') { + return 301 https://$host$request_uri; + } + location /login-redirect { + return 301 https://auth-test.atat.code.mil$request_uri; + } + location /login-dev { + try_files $uri @appbasicauth; + } + location / { + try_files $uri @app; + } + location @app { + include uwsgi_params; + uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket; + } + location @appbasicauth { + include uwsgi_params; + uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket; + auth_basic "Developer Access"; + auth_basic_user_file /etc/nginx/.htpasswd; + } + } + server { + server_name auth-test.atat.code.mil; + listen 8443 ssl; + listen [::]:8443 ssl ipv6only=on; + # SSL server certificate and private key + ssl_certificate /etc/ssl/private/auth.atat.crt; + ssl_certificate_key /etc/ssl/private/auth.atat.key; + # Set SSL protocols, ciphers, and related options + ssl_protocols TLSv1.3 TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; + ssl_prefer_server_ciphers on; + ssl_ecdh_curve secp384r1; + ssl_dhparam /etc/ssl/dhparam.pem; + # SSL session options + ssl_session_timeout 4h; + ssl_session_cache shared:SSL:10m; # 1mb = ~4000 sessions + ssl_session_tickets off; + # OCSP Stapling + ssl_stapling on; + ssl_stapling_verify on; + resolver 8.8.8.8 8.8.4.4; + # Request and validate client certificate + ssl_verify_client on; + ssl_verify_depth 10; + ssl_client_certificate /etc/ssl/client-ca-bundle.pem; + # Guard against HTTPS -> HTTP downgrade + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always"; + location / { + return 301 https://test.atat.code.mil$request_uri; + } + location /login-redirect { + try_files $uri @app; + } + location @app { + include uwsgi_params; + uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket; + uwsgi_param HTTP_X_SSL_CLIENT_VERIFY $ssl_client_verify; + uwsgi_param HTTP_X_SSL_CLIENT_CERT $ssl_client_raw_cert; + uwsgi_param HTTP_X_SSL_CLIENT_S_DN $ssl_client_s_dn; + uwsgi_param HTTP_X_SSL_CLIENT_S_DN_LEGACY $ssl_client_s_dn_legacy; + uwsgi_param HTTP_X_SSL_CLIENT_I_DN $ssl_client_i_dn; + uwsgi_param HTTP_X_SSL_CLIENT_I_DN_LEGACY $ssl_client_i_dn_legacy; + } + } diff --git a/deploy/kubernetes/test/atst-worker-envvars-configmap.yml b/deploy/kubernetes/test/atst-worker-envvars-configmap.yml new file mode 100644 index 00000000..73102195 --- /dev/null +++ b/deploy/kubernetes/test/atst-worker-envvars-configmap.yml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: atst-worker-envvars + namespace: atat-test +data: + REQUIRE_CRLS: "False" diff --git a/deploy/kubernetes/test/test.yml b/deploy/kubernetes/test/test.yml new file mode 100644 index 00000000..362e4f0e --- /dev/null +++ b/deploy/kubernetes/test/test.yml @@ -0,0 +1,223 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: atat-test +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: atst + name: atst + namespace: atat-test +spec: + replicas: 1 + strategy: + type: RollingUpdate + template: + metadata: + labels: + app: atst + spec: + securityContext: + fsGroup: 101 + containers: + - name: atst + image: registry.atat.codes:443/atst-prod:24b2543c + resources: + requests: + memory: "2500Mi" + envFrom: + - configMapRef: + name: atst-envvars + volumeMounts: + - name: atst-config + mountPath: "/opt/atat/atst/atst-overrides.ini" + subPath: atst-overrides.ini + - name: uwsgi-config + mountPath: "/opt/atat/atst/uwsgi-config.ini" + subPath: uwsgi-config.ini + - name: uwsgi-socket-dir + mountPath: "/var/run/uwsgi" + - name: atst-nginx + image: nginx:alpine + ports: + - containerPort: 8442 + name: http + - containerPort: 8443 + name: https + volumeMounts: + - name: nginx-auth-tls + mountPath: "/etc/ssl/private" + - name: nginx-client-ca-bundle + mountPath: "/etc/ssl/client-ca-bundle.pem" + subPath: client-ca-bundle.pem + - name: nginx-config + mountPath: "/etc/nginx/conf.d/atst.conf" + subPath: atst.conf + - name: nginx-dhparam + mountPath: "/etc/ssl/dhparam.pem" + subPath: dhparam.pem + - name: nginx-htpasswd + mountPath: "/etc/nginx/.htpasswd" + subPath: .htpasswd + - name: uwsgi-socket-dir + mountPath: "/var/run/uwsgi" + imagePullSecrets: + - name: regcred + volumes: + - name: atst-config + secret: + secretName: atst-config-ini + items: + - key: override.ini + path: atst-overrides.ini + mode: 0644 + - name: nginx-auth-tls + secret: + secretName: atst-auth-test-ingress-tls + items: + - key: tls.crt + path: auth.atat.crt + mode: 0644 + - key: tls.key + path: auth.atat.key + mode: 0640 + - name: nginx-client-ca-bundle + secret: + secretName: nginx-client-ca-bundle + items: + - key: client-ca-bundle.pem + path: client-ca-bundle.pem + mode: 0666 + - name: nginx-config + configMap: + name: atst-nginx + items: + - key: nginx-config + path: atst.conf + - name: nginx-dhparam + secret: + secretName: dhparam-4096 + items: + - key: dhparam.pem + path: dhparam.pem + mode: 0640 + - name: nginx-htpasswd + secret: + secretName: atst-nginx-htpasswd + items: + - key: htpasswd + path: .htpasswd + mode: 0640 + - name: uwsgi-config + configMap: + name: atst-config + items: + - key: uwsgi-config + path: uwsgi-config.ini + mode: 0644 + - name: uwsgi-socket-dir + emptyDir: + medium: Memory +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: atst + name: atst-worker + namespace: atat-test +spec: + replicas: 1 + strategy: + type: RollingUpdate + template: + metadata: + labels: + app: atst + spec: + securityContext: + fsGroup: 101 + containers: + - name: atst-worker + image: registry.atat.codes:443/atst-prod:24b2543c + args: ["/bin/bash", "-c", "/opt/atat/atst/script/rq_worker"] + resources: + requests: + memory: "500Mi" + envFrom: + - configMapRef: + name: atst-envvars + - configMapRef: + name: atst-worker-envvars + volumeMounts: + - name: atst-config + mountPath: "/opt/atat/atst/atst-overrides.ini" + subPath: atst-overrides.ini + imagePullSecrets: + - name: regcred + volumes: + - name: atst-config + secret: + secretName: atst-config-ini + items: + - key: override.ini + path: atst-overrides.ini + mode: 0644 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: atst + name: atst + namespace: atat-test +spec: + ports: + - name: http + port: 80 + targetPort: 8442 + selector: + app: atst +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: atst + name: atst-auth + namespace: atat-test +spec: + type: NodePort + ports: + - name: https + protocol: TCP + nodePort: 32711 + port: 8443 + selector: + app: atst +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: atst + namespace: atat-test + annotations: + kubernetes.io/tls-acme: "true" + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/proxy-body-size: 10m +spec: + tls: + - secretName: atst-test-ingress-tls + hosts: + - test.atat.code.mil + rules: + - host: test.atat.code.mil + http: + paths: + - path: / + backend: + serviceName: atst + servicePort: 80 diff --git a/js/components/selector.js b/js/components/selector.js index 4e599494..67f6a30f 100644 --- a/js/components/selector.js +++ b/js/components/selector.js @@ -84,7 +84,12 @@ export default { onShow: function () { setTimeout(() => { // timeout is a hack to make focus work in Chrome - this.$refs.choices.find(choice => choice.selected).$refs.input[0].focus() + const selected = this.$refs.choices.find(choice => choice.selected) + if (selected) { + selected.$refs.input[0].focus() + } else { + this.$refs.choices[0].$refs.input[0].focus() + } }, 100) }, diff --git a/templates/audit_log.html b/templates/audit_log.html deleted file mode 100644 index cacd164b..00000000 --- a/templates/audit_log.html +++ /dev/null @@ -1,75 +0,0 @@ -{% extends "base.html" %} -{% from "components/pagination.html" import Pagination %} - -{% block content %} - - -
-
-

Activity History

-
- - - -
- - {{ Pagination(audit_events, 'atst.activity_history') }} -{% endblock %} diff --git a/templates/audit_log/audit_log.html b/templates/audit_log/audit_log.html new file mode 100644 index 00000000..3b505135 --- /dev/null +++ b/templates/audit_log/audit_log.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% from "components/pagination.html" import Pagination %} + +{% block content %} + + +
+
+

Activity History

+
+ + + +
+ + {{ Pagination(audit_events, 'atst.activity_history') }} +{% endblock %} diff --git a/templates/audit_log/events/_base.html b/templates/audit_log/events/_base.html new file mode 100644 index 00000000..03bd5a94 --- /dev/null +++ b/templates/audit_log/events/_base.html @@ -0,0 +1,22 @@ +
+
+ +
+ +
+ {% block header %} +

+ {{ event.user.full_name if event.user else "ATAT System" }} +

+ {{ event.action }} {{ event.resource_type }} {{ event.resource_id }} + {% if event.display_name %} + ({{ event.display_name }}) + {% endif %} + {% endblock %} + +
+ + {% block content %}{% endblock %} +
+
+ diff --git a/templates/audit_log/events/default.html b/templates/audit_log/events/default.html new file mode 100644 index 00000000..2a6ecdf5 --- /dev/null +++ b/templates/audit_log/events/default.html @@ -0,0 +1,26 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} + {% if event.event_details %} +
+ Details: +
+ {% for key, value in event.event_details.items() %} + {% if value is not none %} +
{{ key }}
+
{{ value }}
+ {% endif %} + {% endfor %} +
+ {% endif %} + {% if event.changed_state %} +
+ Changes: +
+ {% for key, value in event.changed_state.items() %} +
{{ key }}
+
{{ value[0] }} to {{ value[1] }}
+ {% endfor %} +
+ {% endif %} +{% endblock %} diff --git a/templates/audit_log/events/environment.html b/templates/audit_log/events/environment.html new file mode 100644 index 00000000..a31a87ef --- /dev/null +++ b/templates/audit_log/events/environment.html @@ -0,0 +1,5 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} + in Workspace {{ event.workspace_id }} ({{ event.workspace.name }}) +{% endblock %} diff --git a/templates/audit_log/events/environment_role.html b/templates/audit_log/events/environment_role.html new file mode 100644 index 00000000..91b7a398 --- /dev/null +++ b/templates/audit_log/events/environment_role.html @@ -0,0 +1,13 @@ +{% extends 'audit_log/events/_base.html' %} + +{% block content %} + for User {{ event.event_details.updated_user_id }} ({{ event.event_details.updated_user_name }}) + {% if event.event_details["environment"] %} +
+ in Environment {{ event.event_details["environment_id"] }} ({{ event.event_details["environment"] }}) +
+ in Project {{ event.event_details["project_id"] }} ({{ event.event_details["project"] }}) +
+ in Workspace {{ event.event_details["workspace_id"] }} ({{ event.event_details["workspace"] }}) + {% endif %} +{% endblock %} diff --git a/templates/audit_log/events/invitation.html b/templates/audit_log/events/invitation.html new file mode 100644 index 00000000..663670bb --- /dev/null +++ b/templates/audit_log/events/invitation.html @@ -0,0 +1,8 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} + {% set accepted = event.changed_state.status and event.changed_state.status.1 == "ACCEPTED" %} + {% if accepted %} + accepted by {{ event.event_details.email }} (DOD {{ event.event_details.dod_id }}) + {% endif %} +{% endblock %} diff --git a/templates/audit_log/events/project.html b/templates/audit_log/events/project.html new file mode 100644 index 00000000..ef822521 --- /dev/null +++ b/templates/audit_log/events/project.html @@ -0,0 +1,4 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} +{% endblock %} diff --git a/templates/audit_log/events/request.html b/templates/audit_log/events/request.html new file mode 100644 index 00000000..ef822521 --- /dev/null +++ b/templates/audit_log/events/request.html @@ -0,0 +1,4 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} +{% endblock %} diff --git a/templates/audit_log/events/request_review.html b/templates/audit_log/events/request_review.html new file mode 100644 index 00000000..ef822521 --- /dev/null +++ b/templates/audit_log/events/request_review.html @@ -0,0 +1,4 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} +{% endblock %} diff --git a/templates/audit_log/events/request_revision.html b/templates/audit_log/events/request_revision.html new file mode 100644 index 00000000..f29caf62 --- /dev/null +++ b/templates/audit_log/events/request_revision.html @@ -0,0 +1,5 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} + on Request {{ event.request_id }} ({{ event.request.displayname }}) +{% endblock %} diff --git a/templates/audit_log/events/request_status_event.html b/templates/audit_log/events/request_status_event.html new file mode 100644 index 00000000..ef822521 --- /dev/null +++ b/templates/audit_log/events/request_status_event.html @@ -0,0 +1,4 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} +{% endblock %} diff --git a/templates/audit_log/events/user.html b/templates/audit_log/events/user.html new file mode 100644 index 00000000..ef822521 --- /dev/null +++ b/templates/audit_log/events/user.html @@ -0,0 +1,4 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} +{% endblock %} diff --git a/templates/audit_log/events/workspace.html b/templates/audit_log/events/workspace.html new file mode 100644 index 00000000..ef822521 --- /dev/null +++ b/templates/audit_log/events/workspace.html @@ -0,0 +1,4 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} +{% endblock %} diff --git a/templates/audit_log/events/workspace_role.html b/templates/audit_log/events/workspace_role.html new file mode 100644 index 00000000..38a22720 --- /dev/null +++ b/templates/audit_log/events/workspace_role.html @@ -0,0 +1,14 @@ +{% extends "audit_log/events/_base.html" %} + +{% block content %} + for User {{ event.event_details.updated_user_id }} ({{ event.event_details.updated_user_name }}) + in Workspace {{ event.workspace_id }} ({{ event.workspace.name }}) + + {% if event.changed_state.status %} + from status "{{ event.changed_state.status[0] }}" to "{{ event.changed_state.status[1] }}" + {% endif %} + + {% if event.changed_state.role %} + from role {{ event.changed_state.role[0] }} to {{ event.changed_state.role[1] }} + {% endif %} +{% endblock %} diff --git a/tests/test_filters.py b/tests/test_filters.py index fde37097..5cfff00e 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,6 +1,7 @@ import pytest -from atst.filters import dollars +from atst.filters import dollars, renderAuditEvent +from atst.models import AuditEvent @pytest.mark.parametrize( @@ -15,3 +16,15 @@ from atst.filters import dollars ) def test_dollar_fomatter(input, expected): assert dollars(input) == expected + + +def test_render_audit_event_with_known_resource_type(): + event = AuditEvent(resource_type="user") + result = renderAuditEvent(event) + assert "