Compare commits
40 commits
dlt_regist
...
release
Author | SHA1 | Date | |
---|---|---|---|
pedro | 052ee0f2b5 | ||
Cayo Puigdefabregas | 6a5d83a450 | ||
Cayo Puigdefabregas | 672b3f3a8e | ||
8ee11d8820 | |||
pedro | 4c28d5d7bc | ||
Cayo Puigdefabregas | f39061206f | ||
e25b68035d | |||
Cayo Puigdefabregas | 3b7fbaf99c | ||
Cayo Puigdefabregas | c45f8ff530 | ||
Cayo Puigdefabregas | 7155a309b5 | ||
Cayo Puigdefabregas | 48cb351263 | ||
c482a6e1ba | |||
pedro | b109fb730a | ||
f09efcdf10 | |||
32be93d294 | |||
3d77e9983e | |||
c8729973b7 | |||
Cayo Puigdefabregas | 0f258c9076 | ||
Cayo Puigdefabregas | 97e2bb36c4 | ||
Cayo Puigdefabregas | 84cfe72362 | ||
Cayo Puigdefabregas | 730d689430 | ||
Cayo Puigdefabregas | b6162e2491 | ||
Cayo Puigdefabregas | 8b3f4704e7 | ||
Cayo Puigdefabregas | 1ae201bc94 | ||
pedro | 273440818f | ||
cea449c878 | |||
a50b28518d | |||
411e8806bb | |||
6f9a06faa1 | |||
6e4f3d7be3 | |||
d9a0de1c68 | |||
cayop | 60890ae8f1 | ||
Cayo Puigdefabregas | 2c3225f08b | ||
Cayo Puigdefabregas | 4c07920a6a | ||
Cayo Puigdefabregas | 3a7eb0f97e | ||
Cayo Puigdefabregas | baa4d87a11 | ||
Cayo Puigdefabregas | 9ebd3b18a4 | ||
Cayo Puigdefabregas | baf5c1e924 | ||
Cayo Puigdefabregas | 717b4ab6d5 | ||
Cayo Puigdefabregas | 8cfa83f921 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.git
|
||||||
|
db.sqlite
|
45
.env.example
Normal file
45
.env.example
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
####
|
||||||
|
# DEV OPTIONS
|
||||||
|
####
|
||||||
|
|
||||||
|
DEV_DOCKER_ALWAYS_BUILD=false
|
||||||
|
|
||||||
|
# IDHUB
|
||||||
|
####
|
||||||
|
|
||||||
|
IDHUB_DOMAIN=localhost
|
||||||
|
IDHUB_PORT=9001
|
||||||
|
IDHUB_DEMO=true
|
||||||
|
IDHUB_ALLOWED_HOSTS=${IDHUB_DOMAIN},${IDHUB_DOMAIN}:${IDHUB_PORT},127.0.0.1,127.0.0.1:${IDHUB_PORT}
|
||||||
|
IDHUB_TIME_ZONE='Europe/Madrid'
|
||||||
|
#IDHUB_SECRET_KEY='uncomment-it-and-fill-this'
|
||||||
|
# enable dev flags when DEVELOPMENT deployment
|
||||||
|
# adapt to your domain in a production/reverse proxy env
|
||||||
|
IDHUB_CSRF_TRUSTED_ORIGINS='https://idhub.example.org'
|
||||||
|
|
||||||
|
# fill this section with your email credentials
|
||||||
|
IDHUB_DEFAULT_FROM_EMAIL="user@example.org"
|
||||||
|
IDHUB_EMAIL_HOST="smtp.example.org"
|
||||||
|
IDHUB_EMAIL_HOST_USER="smtp_user"
|
||||||
|
IDHUB_EMAIL_HOST_PASSWORD="smtp_passwd"
|
||||||
|
IDHUB_EMAIL_PORT=25
|
||||||
|
IDHUB_EMAIL_USE_TLS=True
|
||||||
|
IDHUB_EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
|
||||||
|
|
||||||
|
# replace with production data
|
||||||
|
# this is used when IDHUB_DEPLOYMENT is not equal to DEVELOPMENT
|
||||||
|
IDHUB_ADMIN_USER='admin'
|
||||||
|
IDHUB_ADMIN_PASSWD='admin'
|
||||||
|
IDHUB_ADMIN_EMAIL='admin@example.org'
|
||||||
|
|
||||||
|
IDHUB_SUPPORTED_CREDENTIALS="['CourseCredential', 'EOperatorClaim', 'FederationMembership', 'FinancialVulnerabilityCredential', 'MembershipCard', 'Snapshot']"
|
||||||
|
|
||||||
|
# this option needs to be set to 'n' to be able to make work idhub in docker
|
||||||
|
# by default it is set to 'y' to facilitate idhub dev when outside docker
|
||||||
|
IDHUB_SYNC_ORG_DEV='n'
|
||||||
|
|
||||||
|
# TODO that is only for testing/demo purposes
|
||||||
|
IDHUB_ENABLE_EMAIL=false
|
||||||
|
IDHUB_ENABLE_2FACTOR_AUTH=false
|
||||||
|
IDHUB_ENABLE_DOMAIN_CHECKER=false
|
||||||
|
IDHUB_PREDEFINED_TOKEN='27f944ce-3d58-4f48-b068-e4aa95f97c95'
|
|
@ -41,7 +41,7 @@ The application's backend is responsible for issuing credentials upun user reque
|
||||||
```
|
```
|
||||||
5. Optionally you can install a minumum data set:
|
5. Optionally you can install a minumum data set:
|
||||||
```
|
```
|
||||||
python manage.py initial_datas
|
python manage.py demo_data
|
||||||
```
|
```
|
||||||
6. Start the development server:
|
6. Start the development server:
|
||||||
```
|
```
|
||||||
|
@ -79,7 +79,7 @@ MEDIA_ROOT=/tmp/media/
|
||||||
# Currently unused but will be used in the future
|
# Currently unused but will be used in the future
|
||||||
# DATABASE_URL=postgres://link:to@database:port/idhub
|
# DATABASE_URL=postgres://link:to@database:port/idhub
|
||||||
|
|
||||||
# Defines the admin user after running the initial_datas command
|
# Defines the admin user after running the demo_data command
|
||||||
# Defaults to "admin@example.org" if no INITIAL_ADMIN_EMAIL is provided
|
# Defaults to "admin@example.org" if no INITIAL_ADMIN_EMAIL is provided
|
||||||
# INITIAL_ADMIN_EMAIL="idhub_admin@pangea.org"
|
# INITIAL_ADMIN_EMAIL="idhub_admin@pangea.org"
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
services:
|
||||||
|
|
||||||
|
idhub:
|
||||||
|
init: true
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/idhub.Dockerfile
|
||||||
|
environment:
|
||||||
|
- DOMAIN=${IDHUB_DOMAIN:-localhost}
|
||||||
|
- ALLOWED_HOSTS=${IDHUB_ALLOWED_HOSTS:-$IDHUB_DOMAIN}
|
||||||
|
- DEBUG=true
|
||||||
|
- DEMO=${IDHUB_DEMO:-}
|
||||||
|
- INITIAL_ADMIN_EMAIL=${IDHUB_ADMIN_EMAIL}
|
||||||
|
- INITIAL_ADMIN_PASSWORD=${IDHUB_ADMIN_PASSWD}
|
||||||
|
- CREATE_TEST_USERS=true
|
||||||
|
- ENABLE_EMAIL=${IDHUB_ENABLE_EMAIL:-true}
|
||||||
|
- ENABLE_2FACTOR_AUTH=${IDHUB_ENABLE_2FACTOR_AUTH:-true}
|
||||||
|
- ENABLE_DOMAIN_CHECKER=${IDHUB_ENABLE_DOMAIN_CHECKER:-true}
|
||||||
|
- PREDEFINED_TOKEN=${IDHUB_PREDEFINED_TOKEN:-}
|
||||||
|
- SECRET_KEY=${IDHUB_SECRET_KEY:-publicsecretisnotsecureVtmKBfxpVV47PpBCF2Nzz2H6qnbd}
|
||||||
|
- STATIC_ROOT=${IDHUB_STATIC_ROOT:-/static/}
|
||||||
|
- MEDIA_ROOT=${IDHUB_MEDIA_ROOT:-/media/}
|
||||||
|
- PORT=${IDHUB_PORT:-9001}
|
||||||
|
- DEFAULT_FROM_EMAIL=${IDHUB_DEFAULT_FROM_EMAIL}
|
||||||
|
- EMAIL_HOST=${IDHUB_EMAIL_HOST}
|
||||||
|
- EMAIL_HOST_USER=${IDHUB_EMAIL_HOST_USER}
|
||||||
|
- EMAIL_HOST_PASSWORD=${IDHUB_EMAIL_HOST_PASSWORD}
|
||||||
|
- EMAIL_PORT=${IDHUB_EMAIL_PORT}
|
||||||
|
- EMAIL_USE_TLS=${IDHUB_EMAIL_USE_TLS}
|
||||||
|
- EMAIL_BACKEND=${IDHUB_EMAIL_BACKEND}
|
||||||
|
- SUPPORTED_CREDENTIALS=${IDHUB_SUPPORTED_CREDENTIALS:-}
|
||||||
|
- SYNC_ORG_DEV=${IDHUB_SYNC_ORG_DEV}
|
||||||
|
ports:
|
||||||
|
- ${IDHUB_PORT:-9001}:${IDHUB_PORT:-9001}
|
||||||
|
# TODO manage volumes dev vs prod
|
||||||
|
volumes:
|
||||||
|
- .:/opt/idhub
|
38
docker-reset.sh
Executable file
38
docker-reset.sh
Executable file
|
@ -0,0 +1,38 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
# DEBUG
|
||||||
|
set -x
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$(dirname "${0}")"
|
||||||
|
|
||||||
|
rm -fv ./db.sqlite3
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cp -v .env.example .env
|
||||||
|
echo "WARNING: .env was not there, .env.example was copied, this only happens once"
|
||||||
|
fi
|
||||||
|
. ./.env
|
||||||
|
|
||||||
|
docker compose down -v
|
||||||
|
if [ "${DEV_DOCKER_ALWAYS_BUILD:-}" = 'true' ]; then
|
||||||
|
docker compose build
|
||||||
|
fi
|
||||||
|
docker compose up ${detach_arg:-}
|
||||||
|
|
||||||
|
# TODO docker registry
|
||||||
|
#project=dkr-dsg.ac.upc.edu/trustchain-oc1-orchestral
|
||||||
|
#idhub_image=${project}/idhub:${idhub_tag}
|
||||||
|
#idhub_branch=$(git -C IdHub branch --show-current)
|
||||||
|
# docker build -f docker/idhub.Dockerfile -t ${idhub_image} -t ${project}/idhub:${idhub_branch}__latest .
|
||||||
|
#docker tag hello-world:latest farga.pangea.org/pedro/test/hello-world
|
||||||
|
#docker push farga.pangea.org/pedro/test/hello-world:latest
|
||||||
|
}
|
||||||
|
|
||||||
|
main "${@}"
|
||||||
|
|
||||||
|
# written in emacs
|
||||||
|
# -*- mode: shell-scrip; -*-t
|
37
docker/idhub.Dockerfile
Normal file
37
docker/idhub.Dockerfile
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
FROM python:3.11.7-slim-bookworm
|
||||||
|
|
||||||
|
# last line is dependencies for weasyprint (for generating pdfs in lafede pilot) https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#debian-11
|
||||||
|
RUN apt update && \
|
||||||
|
apt-get install -y \
|
||||||
|
git \
|
||||||
|
sqlite3 \
|
||||||
|
jq \
|
||||||
|
libpango-1.0-0 libpangoft2-1.0-0 \
|
||||||
|
&& pip install cffi brotli \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /opt/idhub
|
||||||
|
|
||||||
|
# reduce size (python specifics) -> src https://stackoverflow.com/questions/74616667/removing-pip-cache-after-installing-dependencies-in-docker-image
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
# here document in dockerfile src https://stackoverflow.com/questions/40359282/launch-a-cat-command-unix-into-dockerfile
|
||||||
|
RUN cat > /etc/pip.conf <<END
|
||||||
|
[install]
|
||||||
|
compile = no
|
||||||
|
|
||||||
|
[global]
|
||||||
|
no-cache-dir = True
|
||||||
|
END
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
|
# not needed anymore?
|
||||||
|
#COPY ssikit_trustchain/didkit-0.3.2-cp311-cp311-manylinux_2_34_x86_64.whl /opt/idhub
|
||||||
|
COPY ./requirements.txt /opt/idhub
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY docker/idhub.entrypoint.sh /
|
||||||
|
|
||||||
|
COPY . /opt/idhub/
|
||||||
|
|
||||||
|
ENTRYPOINT sh /idhub.entrypoint.sh
|
147
docker/idhub.entrypoint.sh
Executable file
147
docker/idhub.entrypoint.sh
Executable file
|
@ -0,0 +1,147 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
set -x
|
||||||
|
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<END
|
||||||
|
ERROR: you need to map your idhub git repo volume to docker, suggested volume mapping is:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ./IdHub:/opt/idhub
|
||||||
|
END
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
inject_env_vars() {
|
||||||
|
# related https://www.kenmuse.com/blog/avoiding-dubious-ownership-in-dev-containers/
|
||||||
|
git config --global --add safe.directory "${idhub_dir}"
|
||||||
|
export COMMIT="commit: $(git log --pretty=format:'%h' -n 1)"
|
||||||
|
|
||||||
|
cat > status_data <<END
|
||||||
|
DOMAIN=${DOMAIN}
|
||||||
|
END
|
||||||
|
}
|
||||||
|
|
||||||
|
deployment_strategy() {
|
||||||
|
# detect if existing deployment (TODO only works with sqlite)
|
||||||
|
if [ -f "${idhub_dir}/db.sqlite3" ]; then
|
||||||
|
echo "INFO: detected EXISTING deployment"
|
||||||
|
./manage.py migrate
|
||||||
|
|
||||||
|
# warn admin that it should re-enter password to keep the service working
|
||||||
|
./manage.py send_mail_admins
|
||||||
|
else
|
||||||
|
# this file helps all docker containers to guess number of hosts involved
|
||||||
|
# right now is only needed by new deployment for oidc
|
||||||
|
if [ -d "/sharedsecret" ]; then
|
||||||
|
touch /sharedsecret/${DOMAIN}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# move the migrate thing in docker entrypoint
|
||||||
|
# inspired by https://medium.com/analytics-vidhya/django-with-docker-and-docker-compose-python-part-2-8415976470cc
|
||||||
|
echo "INFO detected NEW deployment"
|
||||||
|
./manage.py migrate
|
||||||
|
|
||||||
|
if [ "${DEMO:-}" = 'true' ]; then
|
||||||
|
printf "This is DEVELOPMENT/PILOTS_EARLY DEPLOYMENT: including demo hardcoded data\n" >&2
|
||||||
|
PREDEFINED_TOKEN="${PREDEFINED_TOKEN:-}"
|
||||||
|
./manage.py demo_data "${PREDEFINED_TOKEN}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${OIDC_ORGS:-}" ]; then
|
||||||
|
config_oidc4vp
|
||||||
|
else
|
||||||
|
echo "Note: skipping oidc4vp config"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_set() {
|
||||||
|
key="${1}"
|
||||||
|
value="${2}"
|
||||||
|
domain="${3}"
|
||||||
|
sqlite3 db.sqlite3 "update oidc4vp_organization set ${key}='${value}' where domain='${domain}';"
|
||||||
|
}
|
||||||
|
|
||||||
|
_get() {
|
||||||
|
sqlite3 -json db.sqlite3 "select * from oidc4vp_organization;"
|
||||||
|
}
|
||||||
|
|
||||||
|
_lines () {
|
||||||
|
local myfile="${1}"
|
||||||
|
cat "${myfile}" | wc -l
|
||||||
|
}
|
||||||
|
|
||||||
|
config_oidc4vp() {
|
||||||
|
# populate your config
|
||||||
|
data="$(_get)"
|
||||||
|
echo "${data}" | jq --arg domain "${DOMAIN}" '{ ($domain): .}' > /sharedsecret/${DOMAIN}
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
echo wait the other idhubs to write, this is the only oportunity to sync with other idhubs in the docker compose
|
||||||
|
## break when no empty files left
|
||||||
|
if ! wc -l /sharedsecret/* | awk '{print $1;}' | grep -qE '^0$'; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
# get other configs
|
||||||
|
for host in /sharedsecret/*; do
|
||||||
|
# we are flexible on querying for DOMAIN: the first one based on regex
|
||||||
|
target_domain="$(cat "${host}" | jq -r 'keys[0]')"
|
||||||
|
if [ "${target_domain}" != "${DOMAIN}" ]; then
|
||||||
|
filtered_data="$(cat "${host}" | jq --arg domain "${DOMAIN}" 'first(.[][] | select(.domain | test ($domain)))')"
|
||||||
|
client_id="$(echo "${filtered_data}" | jq -r '.client_id')"
|
||||||
|
client_secret="$(echo "${filtered_data}" | jq -r '.client_secret')"
|
||||||
|
|
||||||
|
_set my_client_id ${client_id} ${target_domain}
|
||||||
|
_set my_client_secret ${client_secret} ${target_domain}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
runserver() {
|
||||||
|
PORT="${PORT:-8000}"
|
||||||
|
if [ ! "${DEBUG:-}" = "true" ]; then
|
||||||
|
./manage.py collectstatic
|
||||||
|
if [ "${EXPERIMENTAL:-}" = "true" ]; then
|
||||||
|
# reloading on source code changing is a debugging future, maybe better then use debug
|
||||||
|
# src https://stackoverflow.com/questions/12773763/gunicorn-autoreload-on-source-change/24893069#24893069
|
||||||
|
# gunicorn with 1 worker, with more than 1 worker this is not expected to work
|
||||||
|
gunicorn --access-logfile - --error-logfile - -b :${PORT} trustchain_idhub.wsgi:application
|
||||||
|
else
|
||||||
|
./manage.py runserver 0.0.0.0:${PORT}
|
||||||
|
fi
|
||||||
|
elif [ "${DEMO:-}" = 'true' ]; then
|
||||||
|
VAULT_PASSWORD="DEMO"
|
||||||
|
# open_service: automatically unlocks the vault,
|
||||||
|
# useful for debugging/dev purposes ./manage.py
|
||||||
|
./manage.py open_service "${VAULT_PASSWORD}" 0.0.0.0:${PORT}
|
||||||
|
else
|
||||||
|
./manage.py runserver 0.0.0.0:${PORT}
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_app_is_there() {
|
||||||
|
if [ ! -f "./manage.py" ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
idhub_dir='/opt/idhub'
|
||||||
|
cd "${idhub_dir}"
|
||||||
|
|
||||||
|
check_app_is_there
|
||||||
|
|
||||||
|
deployment_strategy
|
||||||
|
|
||||||
|
inject_env_vars
|
||||||
|
|
||||||
|
runserver
|
||||||
|
}
|
||||||
|
|
||||||
|
main "${@}"
|
|
@ -2,7 +2,7 @@ We use the following files as examples to demonstrate different functionalities
|
||||||
|
|
||||||
- **organizations.csv** is used for install organizations with the command:
|
- **organizations.csv** is used for install organizations with the command:
|
||||||
```
|
```
|
||||||
python manage.py initial_datas
|
python manage.py demo_data
|
||||||
```
|
```
|
||||||
- **federation-membership.xls** is a example of datas for offer credentials for NGOs that are members of a NGO federation.
|
- **federation-membership.xls** is a example of datas for offer credentials for NGOs that are members of a NGO federation.
|
||||||
- **membership-card.xls** is a example of datas for credential of membership
|
- **membership-card.xls** is a example of datas for credential of membership
|
||||||
|
|
1
examples/keys_did.json
Normal file
1
examples/keys_did.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"label": "DEMO", "key_material": "{\"kty\": \"OKP\", \"crv\": \"Ed25519\", \"x\": \"IRqDfIumhbKKHhqMjOngikQmGoT1cZ6LPP-JjXa8CsY\", \"d\": \"AZXUEnJYFbGcn3Ebzy3vQWYFzx6rdnoHKilaMYUWuHA\", \"kid\": \"Generated\"}"}
|
|
@ -36,16 +36,16 @@ class TermsConditionsForm2(forms.Form):
|
||||||
if data.get("accept"):
|
if data.get("accept"):
|
||||||
self.user.accept_gdpr = True
|
self.user.accept_gdpr = True
|
||||||
else:
|
else:
|
||||||
self.user.accept_gdpr = False
|
self.user.accept_gdpr = False
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
self.user.save()
|
self.user.save()
|
||||||
return self.user
|
return self.user
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class EncryptionKeyForm(forms.Form):
|
class EncryptionKeyForm(forms.Form):
|
||||||
|
@ -80,8 +80,8 @@ class EncryptionKeyForm(forms.Form):
|
||||||
did = DID.objects.create(label='Default', type=DID.Types.WEB)
|
did = DID.objects.create(label='Default', type=DID.Types.WEB)
|
||||||
did.set_did()
|
did.set_did()
|
||||||
did.save()
|
did.save()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class TermsConditionsForm(forms.Form):
|
class TermsConditionsForm(forms.Form):
|
||||||
|
@ -131,16 +131,16 @@ class TermsConditionsForm(forms.Form):
|
||||||
if privacy and legal and cookies:
|
if privacy and legal and cookies:
|
||||||
self.user.accept_gdpr = True
|
self.user.accept_gdpr = True
|
||||||
else:
|
else:
|
||||||
self.user.accept_gdpr = False
|
self.user.accept_gdpr = False
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
self.user.save()
|
self.user.save()
|
||||||
return self.user
|
return self.user
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class ImportForm(forms.Form):
|
class ImportForm(forms.Form):
|
||||||
|
@ -197,7 +197,7 @@ class ImportForm(forms.Form):
|
||||||
eidas1=True,
|
eidas1=True,
|
||||||
did=eidas1
|
did=eidas1
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def clean_schema(self):
|
def clean_schema(self):
|
||||||
|
@ -288,15 +288,15 @@ class ImportForm(forms.Form):
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
table = []
|
table = []
|
||||||
for k, v in self.rows.items():
|
for k, v in self.rows.items():
|
||||||
table.append(self.create_credential(k, v))
|
table.append(self.create_credential(k, v))
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
for cred in table:
|
for cred in table:
|
||||||
cred.save()
|
cred.save()
|
||||||
File_datas.objects.create(file_name=self.file_name)
|
File_datas.objects.create(file_name=self.file_name)
|
||||||
return table
|
return table
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def validate_jsonld(self, line, row):
|
def validate_jsonld(self, line, row):
|
||||||
try:
|
try:
|
||||||
|
@ -355,7 +355,7 @@ class ImportForm(forms.Form):
|
||||||
class SchemaForm(forms.Form):
|
class SchemaForm(forms.Form):
|
||||||
file_template = forms.FileField(label=_("File template"))
|
file_template = forms.FileField(label=_("File template"))
|
||||||
|
|
||||||
|
|
||||||
class MembershipForm(forms.ModelForm):
|
class MembershipForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -376,7 +376,7 @@ class MembershipForm(forms.ModelForm):
|
||||||
if members.filter(start_date__lte=start_date, end_date=None).exists():
|
if members.filter(start_date__lte=start_date, end_date=None).exists():
|
||||||
msg = _("This membership already exists!")
|
msg = _("This membership already exists!")
|
||||||
raise forms.ValidationError(msg)
|
raise forms.ValidationError(msg)
|
||||||
|
|
||||||
if (start_date and end_date):
|
if (start_date and end_date):
|
||||||
if start_date > end_date:
|
if start_date > end_date:
|
||||||
msg = _("The end date is less than the start date")
|
msg = _("The end date is less than the start date")
|
||||||
|
@ -399,8 +399,8 @@ class MembershipForm(forms.ModelForm):
|
||||||
if members.exists():
|
if members.exists():
|
||||||
msg = _("This membership already exists!")
|
msg = _("This membership already exists!")
|
||||||
raise forms.ValidationError(msg)
|
raise forms.ValidationError(msg)
|
||||||
|
|
||||||
|
|
||||||
return end_date
|
return end_date
|
||||||
|
|
||||||
|
|
||||||
|
@ -417,7 +417,7 @@ class UserRolForm(forms.ModelForm):
|
||||||
choices = self.fields['service'].choices
|
choices = self.fields['service'].choices
|
||||||
choices.queryset = choices.queryset.exclude(users__user=user)
|
choices.queryset = choices.queryset.exclude(users__user=user)
|
||||||
self.fields['service'].choices = choices
|
self.fields['service'].choices = choices
|
||||||
|
|
||||||
def clean_service(self):
|
def clean_service(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
service = UserRol.objects.filter(
|
service = UserRol.objects.filter(
|
||||||
|
|
|
@ -7,22 +7,45 @@ from utils import credtools
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from decouple import config
|
from django.core.cache import cache
|
||||||
from idhub.models import Schemas
|
from django.urls import reverse
|
||||||
|
from pyvckit.did import (
|
||||||
|
generate_did,
|
||||||
|
gen_did_document,
|
||||||
|
)
|
||||||
|
|
||||||
|
from idhub.models import Schemas, DID
|
||||||
from oidc4vp.models import Organization
|
from oidc4vp.models import Organization
|
||||||
|
from webhook.models import Token
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Insert minimum datas for the project"
|
help = "Insert minimum data for the project"
|
||||||
DOMAIN = settings.DOMAIN
|
DOMAIN = settings.DOMAIN
|
||||||
OIDC_ORGS = settings.OIDC_ORGS
|
OIDC_ORGS = settings.OIDC_ORGS
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('predefined_token', nargs='?', default='', type=str, help='predefined token')
|
||||||
|
parser.add_argument('predefined_did', nargs='?', default='', type=str, help='predefined did')
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
ADMIN_EMAIL = settings.INITIAL_ADMIN_EMAIL
|
ADMIN_EMAIL = settings.INITIAL_ADMIN_EMAIL
|
||||||
ADMIN_PASSWORD = settings.INITIAL_ADMIN_PASSWORD
|
ADMIN_PASSWORD = settings.INITIAL_ADMIN_PASSWORD
|
||||||
|
self.predefined_token = kwargs['predefined_token']
|
||||||
|
self.predefined_did = kwargs['predefined_did']
|
||||||
|
# on demo situation, encrypted vault is hardcoded with password DEMO
|
||||||
|
cache.set("KEY_DIDS", "DEMO", None)
|
||||||
|
|
||||||
|
self.org = Organization.objects.create(
|
||||||
|
name=self.DOMAIN,
|
||||||
|
domain=self.DOMAIN,
|
||||||
|
main=True
|
||||||
|
)
|
||||||
|
self.org.set_encrypted_sensitive_data()
|
||||||
|
self.org.save()
|
||||||
|
|
||||||
self.create_admin_users(ADMIN_EMAIL, ADMIN_PASSWORD)
|
self.create_admin_users(ADMIN_EMAIL, ADMIN_PASSWORD)
|
||||||
if settings.CREATE_TEST_USERS:
|
if settings.CREATE_TEST_USERS:
|
||||||
|
@ -30,12 +53,6 @@ class Command(BaseCommand):
|
||||||
user = 'user{}@example.org'.format(u)
|
user = 'user{}@example.org'.format(u)
|
||||||
self.create_users(user, '1234')
|
self.create_users(user, '1234')
|
||||||
|
|
||||||
self.org = Organization.objects.create(
|
|
||||||
name=self.DOMAIN,
|
|
||||||
domain=self.DOMAIN,
|
|
||||||
main=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.OIDC_ORGS:
|
if self.OIDC_ORGS:
|
||||||
self.create_organizations()
|
self.create_organizations()
|
||||||
|
|
||||||
|
@ -45,6 +62,61 @@ class Command(BaseCommand):
|
||||||
su = User.objects.create_superuser(email=email, password=password)
|
su = User.objects.create_superuser(email=email, password=password)
|
||||||
su.save()
|
su.save()
|
||||||
|
|
||||||
|
if self.predefined_token:
|
||||||
|
tk = Token.objects.filter(token=self.predefined_token).first()
|
||||||
|
if not tk:
|
||||||
|
Token.objects.create(token=self.predefined_token)
|
||||||
|
|
||||||
|
self.create_default_did()
|
||||||
|
|
||||||
|
def create_default_did(self):
|
||||||
|
|
||||||
|
fdid = self.open_example_did()
|
||||||
|
if not fdid:
|
||||||
|
return
|
||||||
|
|
||||||
|
did = DID(type=DID.Types.WEB)
|
||||||
|
new_key_material = fdid.get("key_material", "")
|
||||||
|
label = fdid.get("label", "")
|
||||||
|
if not new_key_material:
|
||||||
|
return
|
||||||
|
|
||||||
|
did.set_key_material(new_key_material)
|
||||||
|
|
||||||
|
if label:
|
||||||
|
did.label = label
|
||||||
|
|
||||||
|
if did.type == did.Types.KEY:
|
||||||
|
did.did = generate_did(new_key_material)
|
||||||
|
elif did.type == did.Types.WEB:
|
||||||
|
url = "https://{}".format(settings.DOMAIN)
|
||||||
|
path = reverse("idhub:serve_did", args=["a"])
|
||||||
|
|
||||||
|
if path:
|
||||||
|
path = path.split("/a/did.json")[0]
|
||||||
|
url = "https://{}/{}".format(settings.DOMAIN, path)
|
||||||
|
|
||||||
|
did.did = generate_did(new_key_material, url)
|
||||||
|
key = json.loads(new_key_material)
|
||||||
|
url, did.didweb_document = gen_did_document(did.did, key)
|
||||||
|
|
||||||
|
did.save()
|
||||||
|
|
||||||
|
def open_example_did(self):
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
|
||||||
|
didweb_path = os.path.join(BASE_DIR, "examples", "keys_did.json")
|
||||||
|
|
||||||
|
if self.predefined_did:
|
||||||
|
didweb_path = self.predefined_did
|
||||||
|
|
||||||
|
data = ''
|
||||||
|
with open(didweb_path) as _file:
|
||||||
|
try:
|
||||||
|
data = json.loads(_file.read())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def create_users(self, email, password):
|
def create_users(self, email, password):
|
||||||
u = User.objects.create(email=email, password=password)
|
u = User.objects.create(email=email, password=password)
|
||||||
|
@ -105,6 +177,7 @@ class Command(BaseCommand):
|
||||||
assert dname
|
assert dname
|
||||||
assert title
|
assert title
|
||||||
except Exception:
|
except Exception:
|
||||||
|
ldata = {}
|
||||||
title = ''
|
title = ''
|
||||||
_name = ''
|
_name = ''
|
||||||
|
|
48
idhub/management/commands/open_service.py
Normal file
48
idhub/management/commands/open_service.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from nacl.exceptions import CryptoError
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
from idhub.models import DID
|
||||||
|
from idhub_auth.models import User
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Command for open de service"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('key', nargs='?', default='', type=str, help='key')
|
||||||
|
parser.add_argument('ip_port', nargs='?', default='', type=str, help='ip_port')
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
self._key = kwargs["key"]
|
||||||
|
self.ip_port = kwargs["ip_port"]
|
||||||
|
cache.set("KEY_DIDS", self._key, None)
|
||||||
|
|
||||||
|
admin = User.objects.filter(is_admin=True).first()
|
||||||
|
admin.accept_gdpr = True
|
||||||
|
admin.save()
|
||||||
|
|
||||||
|
if not DID.objects.exists():
|
||||||
|
cache.set("KEY_DIDS", self._key, None)
|
||||||
|
call_command('runserver', self.ip_port)
|
||||||
|
return
|
||||||
|
|
||||||
|
did = DID.objects.first()
|
||||||
|
cache.set("KEY_DIDS", self._key, None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
did.get_key_material()
|
||||||
|
except CryptoError:
|
||||||
|
cache.set("KEY_DIDS", None)
|
||||||
|
txt = "Key no valid!"
|
||||||
|
logger.error(txt)
|
||||||
|
return
|
||||||
|
|
||||||
|
cache.set("KEY_DIDS", self._key, None)
|
||||||
|
call_command('runserver', self.ip_port)
|
|
@ -33,10 +33,11 @@ class UserView(LoginRequiredMixin):
|
||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
err_txt = "User domain is {} which does not match server domain {}".format(
|
if settings.ENABLE_DOMAIN_CHECKER:
|
||||||
request.get_host(), settings.DOMAIN
|
err_txt = "User domain is {} which does not match server domain {}".format(
|
||||||
)
|
request.get_host(), settings.DOMAIN
|
||||||
assert request.get_host() == settings.DOMAIN, err_txt
|
)
|
||||||
|
assert request.get_host() == settings.DOMAIN, err_txt
|
||||||
self.admin_validated = cache.get("KEY_DIDS")
|
self.admin_validated = cache.get("KEY_DIDS")
|
||||||
response = super().get(request, *args, **kwargs)
|
response = super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -55,10 +56,11 @@ class UserView(LoginRequiredMixin):
|
||||||
return url or response
|
return url or response
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
err_txt = "User domain is {} which does not match server domain {}".format(
|
if settings.ENABLE_DOMAIN_CHECKER:
|
||||||
request.get_host(), settings.DOMAIN
|
err_txt = "User domain is {} which does not match server domain {}".format(
|
||||||
)
|
request.get_host(), settings.DOMAIN
|
||||||
assert request.get_host() == settings.DOMAIN, err_txt
|
)
|
||||||
|
assert request.get_host() == settings.DOMAIN, err_txt
|
||||||
self.admin_validated = cache.get("KEY_DIDS")
|
self.admin_validated = cache.get("KEY_DIDS")
|
||||||
response = super().post(request, *args, **kwargs)
|
response = super().post(request, *args, **kwargs)
|
||||||
url = self.check_gdpr()
|
url = self.check_gdpr()
|
||||||
|
|
|
@ -680,10 +680,18 @@ class VerificableCredential(models.Model):
|
||||||
credential_subject = ujson.loads(data).get("credentialSubject", {})
|
credential_subject = ujson.loads(data).get("credentialSubject", {})
|
||||||
return credential_subject.items()
|
return credential_subject.items()
|
||||||
|
|
||||||
def issue(self, did, domain):
|
def issue(self, did, domain, save=True):
|
||||||
if self.status == self.Status.ISSUED:
|
if self.status == self.Status.ISSUED:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
supported = False
|
||||||
|
for name in self.schema.get_schema.get("name"):
|
||||||
|
if name.get("value") in settings.SUPPORTED_CREDENTIALS:
|
||||||
|
supported = True
|
||||||
|
|
||||||
|
if not supported:
|
||||||
|
return
|
||||||
|
|
||||||
self.subject_did = did
|
self.subject_did = did
|
||||||
self.issued_on = datetime.datetime.now().astimezone(pytz.utc)
|
self.issued_on = datetime.datetime.now().astimezone(pytz.utc)
|
||||||
|
|
||||||
|
@ -700,6 +708,9 @@ class VerificableCredential(models.Model):
|
||||||
if not valid:
|
if not valid:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not save:
|
||||||
|
return vc_str
|
||||||
|
|
||||||
self.data = self.user.encrypt_data(vc_str)
|
self.data = self.user.encrypt_data(vc_str)
|
||||||
|
|
||||||
self.status = self.Status.ISSUED
|
self.status = self.Status.ISSUED
|
||||||
|
|
61
idhub/templates/credentials/device-snapshot-v1.json
Normal file
61
idhub/templates/credentials/device-snapshot-v1.json
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/2018/credentials/v1"
|
||||||
|
],
|
||||||
|
"type": ["VerifiableCredential", "DeviceSnapshot"],
|
||||||
|
"issuer": "{{ issuer_did }}",
|
||||||
|
"issuanceDate": "{{ issuance_date }}",
|
||||||
|
"credentialSubject": {
|
||||||
|
"operatorId": "{{ operator_id }}",
|
||||||
|
"uuid": "{{ uuid }}",
|
||||||
|
"type": "hardwareList",
|
||||||
|
"software": "workbench-script",
|
||||||
|
"deviceId": [
|
||||||
|
{
|
||||||
|
"name": "Manufacturer",
|
||||||
|
"value": "{{ manufacturer }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Model",
|
||||||
|
"value": "{{ model }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Serial",
|
||||||
|
"value": "{{ serial_number }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SKU",
|
||||||
|
"value": "{{ sku }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EthernetMacAddress",
|
||||||
|
"value": "{{ mac }}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timestamp": "{{ issuance_date }}"
|
||||||
|
},
|
||||||
|
"evidence": [
|
||||||
|
{
|
||||||
|
"type": "HardwareList",
|
||||||
|
"operation": "dmidecode",
|
||||||
|
"output": "{{ dmidecode }}",
|
||||||
|
"timestamp": "{{ issuance_date }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "HardwareList",
|
||||||
|
"operation": "smartctl",
|
||||||
|
"output": {{ smartctl|default:'""'|safe }},
|
||||||
|
"timestamp": "{{ issuance_date }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "HardwareList",
|
||||||
|
"operation": "inxi",
|
||||||
|
"output": {{ inxi|default:'""'|safe }},
|
||||||
|
"timestamp": "{{ issuance_date }}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credentialSchema": {
|
||||||
|
"id": "https://idhub.pangea.org/vc_schemas/device-snapshot-v1.json",
|
||||||
|
"type": "FullJsonSchemaValidator2021"
|
||||||
|
}
|
||||||
|
}
|
|
@ -179,8 +179,8 @@ class TermsAndConditionsView(UserView, FormView):
|
||||||
|
|
||||||
class WaitingView(UserView, TemplateView):
|
class WaitingView(UserView, TemplateView):
|
||||||
template_name = "idhub/user/waiting.html"
|
template_name = "idhub/user/waiting.html"
|
||||||
title = _("Comunication with admin")
|
title = _("Comunication with admin required")
|
||||||
subtitle = _('Service temporary close')
|
subtitle = _('Service temporarily closed')
|
||||||
section = ""
|
section = ""
|
||||||
icon = 'bi bi-file-earmark-medical'
|
icon = 'bi bi-file-earmark-medical'
|
||||||
success_url = reverse_lazy('idhub:user_dashboard')
|
success_url = reverse_lazy('idhub:user_dashboard')
|
||||||
|
|
|
@ -125,7 +125,7 @@ class User(AbstractBaseUser):
|
||||||
sb = secret.SecretBox(sb_key)
|
sb = secret.SecretBox(sb_key)
|
||||||
if not isinstance(data, bytes):
|
if not isinstance(data, bytes):
|
||||||
data = data.encode('utf-8')
|
data = data.encode('utf-8')
|
||||||
|
|
||||||
return base64.b64encode(sb.encrypt(data)).decode('utf-8')
|
return base64.b64encode(sb.encrypt(data)).decode('utf-8')
|
||||||
|
|
||||||
def get_salt(self):
|
def get_salt(self):
|
||||||
|
@ -166,6 +166,6 @@ class User(AbstractBaseUser):
|
||||||
sb = secret.SecretBox(sb_key)
|
sb = secret.SecretBox(sb_key)
|
||||||
if not isinstance(data, bytes):
|
if not isinstance(data, bytes):
|
||||||
data = data.encode('utf-8')
|
data = data.encode('utf-8')
|
||||||
|
|
||||||
encrypted_data = base64.b64encode(sb.encrypt(data)).decode('utf-8')
|
encrypted_data = base64.b64encode(sb.encrypt(data)).decode('utf-8')
|
||||||
self.encrypted_sensitive_data = encrypted_data
|
self.encrypted_sensitive_data = encrypted_data
|
||||||
|
|
|
@ -2796,11 +2796,11 @@ msgid "Data Protection"
|
||||||
msgstr "Protecció de dades"
|
msgstr "Protecció de dades"
|
||||||
|
|
||||||
#: idhub/user/views.py:183
|
#: idhub/user/views.py:183
|
||||||
msgid "Comunication with admin"
|
msgid "Comunication with admin required"
|
||||||
msgstr "Comunicació amb l'admin"
|
msgstr "Es requereix comunicació amb l'admin"
|
||||||
|
|
||||||
#: idhub/user/views.py:184
|
#: idhub/user/views.py:184
|
||||||
msgid "Service temporary close"
|
msgid "Service temporarily closed"
|
||||||
msgstr "Tancament temporal del servei"
|
msgstr "Tancament temporal del servei"
|
||||||
|
|
||||||
#: idhub/user/views.py:407
|
#: idhub/user/views.py:407
|
||||||
|
|
|
@ -2789,11 +2789,11 @@ msgid "Data Protection"
|
||||||
msgstr "Proteccion de datos"
|
msgstr "Proteccion de datos"
|
||||||
|
|
||||||
#: idhub/user/views.py:183
|
#: idhub/user/views.py:183
|
||||||
msgid "Comunication with admin"
|
msgid "Comunication with admin required"
|
||||||
msgstr "Comunicación con el admin"
|
msgstr "Se requiere comunicación con el admin"
|
||||||
|
|
||||||
#: idhub/user/views.py:184
|
#: idhub/user/views.py:184
|
||||||
msgid "Service temporary close"
|
msgid "Service temporarily closed"
|
||||||
msgstr "Cierre temporal del servicio"
|
msgstr "Cierre temporal del servicio"
|
||||||
|
|
||||||
#: idhub/user/views.py:407
|
#: idhub/user/views.py:407
|
||||||
|
|
|
@ -6,6 +6,7 @@ black==23.9.1
|
||||||
python-decouple==3.8
|
python-decouple==3.8
|
||||||
jsonschema[format]==4.19.1
|
jsonschema[format]==4.19.1
|
||||||
pandas==2.1.1
|
pandas==2.1.1
|
||||||
|
numpy>=1.21,<2.0
|
||||||
xlrd==2.0.1
|
xlrd==2.0.1
|
||||||
odfpy==1.4.1
|
odfpy==1.4.1
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
|
|
122
schemas/device-snapshot-v1.json
Normal file
122
schemas/device-snapshot-v1.json
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
{
|
||||||
|
"$id": "https://idhub.pangea.org/vc_schemas/device-snapshot-v1.json",
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "DeviceSnapshotV1",
|
||||||
|
"description": "Snapshot create by workbench-script, software for discover hardware in one device.",
|
||||||
|
"name": [
|
||||||
|
{
|
||||||
|
"value": "Snapshot",
|
||||||
|
"lang": "en"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "https://www.w3.org/2018/credentials/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"credentialSubject": {
|
||||||
|
"description": "Define the properties of a digital device snapshot",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"operatorId": {
|
||||||
|
"description": "Indentifier related to the product operator, defined a hash of an Id token (10 chars enough)",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 10
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"description": "Unique identifier of the snapshot.",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 36
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"description": "Defines a snapshot type, e.g., hardwareList, dataDeletion (need to adjust the enum values).",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"hardwareList", "dataDeletion"
|
||||||
|
],
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"software": {
|
||||||
|
"description": "Name of the snapshot software used.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"workbench-script"
|
||||||
|
],
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"deviceId": {
|
||||||
|
"description": "List of identification properties for the device, each with a name and value.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"description": "The type of device identifier information, e.g., ManufacturerSerial, EthernetMacAddress.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"description": "The value of the device identifier information.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "value"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"description": "Date and time of this snapshot.",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"uuid",
|
||||||
|
"type",
|
||||||
|
"timestamp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"description": "Contains supporting evidence about the process which resulted in the issuance of this credential as a result of system operations.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"description": "Type of evidence, linked to credentialSubject.type.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"HardwareList",
|
||||||
|
"DataDeletion"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"description": "Specifies the command executed for evidence generation.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"inxi",
|
||||||
|
"dmidecode",
|
||||||
|
"smartctl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"description": "Output from the executed command.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"description": "Timestamp of the evidence generation if needed.",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"operation",
|
||||||
|
"output"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ SECRET_KEY = config('SECRET_KEY')
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = config('DEBUG', default=False, cast=bool)
|
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||||
|
DEVELOPMENT = config('DEVELOPMENT', default=False, cast=bool)
|
||||||
|
|
||||||
DOMAIN = config("DOMAIN")
|
DOMAIN = config("DOMAIN")
|
||||||
assert DOMAIN not in [None, ''], "DOMAIN var is MANDATORY"
|
assert DOMAIN not in [None, ''], "DOMAIN var is MANDATORY"
|
||||||
|
@ -240,5 +241,6 @@ OIDC_ORGS = config('OIDC_ORGS', '')
|
||||||
ENABLE_EMAIL = config('ENABLE_EMAIL', default=True, cast=bool)
|
ENABLE_EMAIL = config('ENABLE_EMAIL', default=True, cast=bool)
|
||||||
CREATE_TEST_USERS = config('CREATE_TEST_USERS', default=False, cast=bool)
|
CREATE_TEST_USERS = config('CREATE_TEST_USERS', default=False, cast=bool)
|
||||||
ENABLE_2FACTOR_AUTH = config('ENABLE_2FACTOR_AUTH', default=True, cast=bool)
|
ENABLE_2FACTOR_AUTH = config('ENABLE_2FACTOR_AUTH', default=True, cast=bool)
|
||||||
|
ENABLE_DOMAIN_CHECKER = config('ENABLE_DOMAIN_CHECKER', default=True, cast=bool)
|
||||||
COMMIT = config('COMMIT', default='')
|
COMMIT = config('COMMIT', default='')
|
||||||
|
|
||||||
|
|
22
webhook/migrations/0002_token_active_token_label.py
Normal file
22
webhook/migrations/0002_token_active_token_label.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 4.2.5 on 2025-01-27 09:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('webhook', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='token',
|
||||||
|
name='active',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Active'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='token',
|
||||||
|
name='label',
|
||||||
|
field=models.CharField(default='', max_length=250, verbose_name='Label'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,7 +1,10 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
|
||||||
|
|
||||||
class Token(models.Model):
|
class Token(models.Model):
|
||||||
token = models.UUIDField()
|
token = models.UUIDField()
|
||||||
|
label = models.CharField(_("Label"), max_length=250, default="")
|
||||||
|
active = models.BooleanField(_("Active"), default=True)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from webhook.models import Token
|
from webhook.models import Token
|
||||||
|
|
||||||
|
|
||||||
class ButtonColumn(tables.Column):
|
class ButtonRemoveColumn(tables.Column):
|
||||||
attrs = {
|
attrs = {
|
||||||
"a": {
|
"a": {
|
||||||
"type": "button",
|
"type": "button",
|
||||||
|
@ -25,7 +26,7 @@ class ButtonColumn(tables.Column):
|
||||||
|
|
||||||
|
|
||||||
class TokensTable(tables.Table):
|
class TokensTable(tables.Table):
|
||||||
delete = ButtonColumn(
|
delete = ButtonRemoveColumn(
|
||||||
verbose_name=_("Delete"),
|
verbose_name=_("Delete"),
|
||||||
linkify={
|
linkify={
|
||||||
"viewname": "webhook:delete_token",
|
"viewname": "webhook:delete_token",
|
||||||
|
@ -33,11 +34,19 @@ class TokensTable(tables.Table):
|
||||||
},
|
},
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
# active = tables.Column(linkify=lambda record: reverse("webhook:status_token", kwargs={"pk": record.pk}))
|
||||||
|
active = tables.Column(
|
||||||
|
linkify={
|
||||||
|
"viewname": "webhook:status_token",
|
||||||
|
"args": [tables.A("pk")]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
token = tables.Column(verbose_name=_("Token"), empty_values=())
|
token = tables.Column(verbose_name=_("Token"), empty_values=())
|
||||||
|
label = tables.Column(verbose_name=_("Label"), empty_values=())
|
||||||
|
|
||||||
def render_view_user(self):
|
# def render_view_user(self):
|
||||||
return format_html('<i class="bi bi-eye"></i>')
|
# return format_html('<i class="bi bi-eye"></i>')
|
||||||
|
|
||||||
# def render_token(self, record):
|
# def render_token(self, record):
|
||||||
# return record.get_memberships()
|
# return record.get_memberships()
|
||||||
|
@ -63,5 +72,13 @@ class TokensTable(tables.Table):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Token
|
model = Token
|
||||||
template_name = "idhub/custom_table.html"
|
template_name = "idhub/custom_table.html"
|
||||||
fields = ("token", "view_user")
|
fields = ("token", "label", "active")
|
||||||
|
|
||||||
|
def render_active(self, value):
|
||||||
|
"""
|
||||||
|
Render icons custom based on active value
|
||||||
|
"""
|
||||||
|
if value: # if `active` is True
|
||||||
|
return format_html('<i class="bi bi-toggle-on text-primary"></i>')
|
||||||
|
else: # if `active` is False
|
||||||
|
return format_html('<i class="bi bi-toggle-off text-danger"></i>')
|
||||||
|
|
34
webhook/templates/new_token.html
Normal file
34
webhook/templates/new_token.html
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{% extends "idhub/base_admin.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>
|
||||||
|
<i class="{{ icon }}"></i>
|
||||||
|
{{ subtitle }}
|
||||||
|
</h3>
|
||||||
|
{% load django_bootstrap5 %}
|
||||||
|
<form role="form" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if form.errors %}
|
||||||
|
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
|
||||||
|
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
|
||||||
|
<div class="message">
|
||||||
|
{% for field, error in form.errors.items %}
|
||||||
|
{{ error }}<br />
|
||||||
|
{% endfor %}
|
||||||
|
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-4">
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions-no-box">
|
||||||
|
<a class="btn btn-grey" href="{% url 'webhook:tokens' %}">{% translate "Cancel" %}</a>
|
||||||
|
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -7,7 +7,9 @@ app_name = 'webhook'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('verify/', views.webhook_verify, name='verify'),
|
path('verify/', views.webhook_verify, name='verify'),
|
||||||
|
path('sign/', views.webhook_issue, name='sign'),
|
||||||
path('tokens/', views.WebHookTokenView.as_view(), name='tokens'),
|
path('tokens/', views.WebHookTokenView.as_view(), name='tokens'),
|
||||||
path('tokens/new', views.TokenNewView.as_view(), name='new_token'),
|
path('tokens/new', views.TokenNewView.as_view(), name='new_token'),
|
||||||
path('tokens/<int:pk>/del', views.TokenDeleteView.as_view(), name='delete_token'),
|
path('tokens/<int:pk>/del', views.TokenDeleteView.as_view(), name='delete_token'),
|
||||||
|
path('tokens/<int:pk>/status', views.TokenStatusView.as_view(), name='status_token'),
|
||||||
]
|
]
|
||||||
|
|
108
webhook/views.py
108
webhook/views.py
|
@ -3,14 +3,18 @@ import json
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic.edit import DeleteView
|
from django.views.generic.edit import DeleteView, CreateView
|
||||||
from django.views.generic.base import View
|
from django.views.generic.base import View
|
||||||
|
from django.core.cache import cache
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from pyvckit.verify import verify_vp, verify_vc
|
from pyvckit.verify import verify_vp, verify_vc
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
from idhub.mixins import AdminView
|
from idhub.mixins import AdminView
|
||||||
|
from idhub_auth.models import User
|
||||||
|
from idhub.models import DID, Schemas, VerificableCredential
|
||||||
from webhook.models import Token
|
from webhook.models import Token
|
||||||
from webhook.tables import TokensTable
|
from webhook.tables import TokensTable
|
||||||
|
|
||||||
|
@ -18,12 +22,16 @@ from webhook.tables import TokensTable
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def webhook_verify(request):
|
def webhook_verify(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
user = User.objects.filter(is_admin=True).first()
|
||||||
|
if not cache.get("KEY_DIDS") or not user.accept_gdpr:
|
||||||
|
return JsonResponse({'error': 'Temporary out of service'}, status=400)
|
||||||
|
|
||||||
auth_header = request.headers.get('Authorization')
|
auth_header = request.headers.get('Authorization')
|
||||||
if not auth_header or not auth_header.startswith('Bearer '):
|
if not auth_header or not auth_header.startswith('Bearer '):
|
||||||
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
|
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
|
||||||
|
|
||||||
token = auth_header.split(' ')[1]
|
token = auth_header.split(' ')[1].strip("'").strip('"')
|
||||||
tk = Token.objects.filter(token=token).first()
|
tk = Token.objects.filter(token=token, active=True).first()
|
||||||
if not tk:
|
if not tk:
|
||||||
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
|
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
|
||||||
|
|
||||||
|
@ -51,6 +59,66 @@ def webhook_verify(request):
|
||||||
return JsonResponse({'error': 'Invalid request method'}, status=400)
|
return JsonResponse({'error': 'Invalid request method'}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def webhook_issue(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
user = User.objects.filter(is_admin=True).first()
|
||||||
|
if not cache.get("KEY_DIDS") or not user.accept_gdpr:
|
||||||
|
return JsonResponse({'error': 'Temporary out of service'}, status=400)
|
||||||
|
|
||||||
|
auth_header = request.headers.get('Authorization')
|
||||||
|
if not auth_header or not auth_header.startswith('Bearer '):
|
||||||
|
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
|
||||||
|
|
||||||
|
token = auth_header.split(' ')[1].strip("'").strip('"')
|
||||||
|
tk = Token.objects.filter(token=token, active=True).first()
|
||||||
|
if not tk:
|
||||||
|
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({'error': 'Invalid JSON'}, status=400)
|
||||||
|
|
||||||
|
typ = data.get("type")
|
||||||
|
vc = data.get("data")
|
||||||
|
save = data.get("save", True)
|
||||||
|
try:
|
||||||
|
vc = json.dumps(vc)
|
||||||
|
except Exception:
|
||||||
|
return JsonResponse({'error': 'Invalid JSON'}, status=400)
|
||||||
|
|
||||||
|
if not typ or not vc:
|
||||||
|
return JsonResponse({'error': 'Invalid JSON'}, status=400)
|
||||||
|
|
||||||
|
did = DID.objects.filter(user__isnull=True).first()
|
||||||
|
if not did:
|
||||||
|
return JsonResponse({'error': 'Invalid DID'}, status=400)
|
||||||
|
|
||||||
|
schema = Schemas.objects.filter(type=typ).first()
|
||||||
|
if not schema:
|
||||||
|
return JsonResponse({'error': 'Invalid credential'}, status=400)
|
||||||
|
|
||||||
|
cred = VerificableCredential(
|
||||||
|
csv_data=vc,
|
||||||
|
issuer_did=did,
|
||||||
|
schema=schema,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
cred.set_type()
|
||||||
|
vc_signed = cred.issue(did, domain=request.get_host(), save=save)
|
||||||
|
|
||||||
|
if not vc_signed:
|
||||||
|
return JsonResponse({'error': 'Invalid credential'}, status=400)
|
||||||
|
|
||||||
|
return JsonResponse({'status': 'success', "data": vc_signed}, status=200)
|
||||||
|
|
||||||
|
return JsonResponse({'status': 'fail'}, status=200)
|
||||||
|
|
||||||
|
return JsonResponse({'error': 'Invalid request method'}, status=400)
|
||||||
|
|
||||||
|
|
||||||
class WebHookTokenView(AdminView, SingleTableView):
|
class WebHookTokenView(AdminView, SingleTableView):
|
||||||
template_name = "token.html"
|
template_name = "token.html"
|
||||||
title = _("Credential management")
|
title = _("Credential management")
|
||||||
|
@ -86,11 +154,39 @@ class TokenDeleteView(AdminView, DeleteView):
|
||||||
return redirect('webhook:tokens')
|
return redirect('webhook:tokens')
|
||||||
|
|
||||||
|
|
||||||
class TokenNewView(AdminView, View):
|
class TokenStatusView(AdminView, DeleteView):
|
||||||
|
model = Token
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.check_valid_user()
|
self.check_valid_user()
|
||||||
Token.objects.create(token=uuid4())
|
self.pk = kwargs['pk']
|
||||||
|
self.object = get_object_or_404(self.model, pk=self.pk)
|
||||||
|
if self.object.active:
|
||||||
|
self.object.active = False
|
||||||
|
else:
|
||||||
|
self.object.active = True
|
||||||
|
self.object.save()
|
||||||
|
|
||||||
return redirect('webhook:tokens')
|
return redirect('webhook:tokens')
|
||||||
|
|
||||||
|
|
||||||
|
class TokenNewView(AdminView, CreateView):
|
||||||
|
title = _("Token management")
|
||||||
|
section = "Credential"
|
||||||
|
subtitle = _('New Tokens')
|
||||||
|
icon = 'bi bi-key'
|
||||||
|
title = "Token"
|
||||||
|
template_name = "new_token.html"
|
||||||
|
model = Token
|
||||||
|
fields = ("label",)
|
||||||
|
success_url = reverse_lazy('webhook:tokens')
|
||||||
|
# def get(self, request, *args, **kwargs):
|
||||||
|
# self.check_valid_user()
|
||||||
|
# Token.objects.create(token=uuid4())
|
||||||
|
|
||||||
|
# return redirect('webhook:tokens')
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.token = uuid4()
|
||||||
|
form.save()
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
Loading…
Reference in a new issue