Compare commits

...

40 commits

Author SHA1 Message Date
pedro 052ee0f2b5 Merge pull request 'label_token' (#8) from label_token into release
Reviewed-on: #8
2025-01-30 13:02:14 +00:00
Cayo Puigdefabregas 6a5d83a450 write label for token 2025-01-30 13:51:11 +01:00
Cayo Puigdefabregas 672b3f3a8e add label and active in tokens 2025-01-30 13:51:11 +01:00
pedro 8ee11d8820 docker: add open_service support 2025-01-30 13:42:07 +01:00
pedro 4c28d5d7bc Merge pull request 'add key for open service' (#9) from open_service into release
Reviewed-on: #9
2025-01-30 12:38:58 +00:00
Cayo Puigdefabregas f39061206f accept gdpr for admin 2025-01-30 13:19:38 +01:00
pedro e25b68035d demo purpose: use open_service 2025-01-30 13:03:42 +01:00
Cayo Puigdefabregas 3b7fbaf99c fix name 2025-01-30 13:01:28 +01:00
Cayo Puigdefabregas c45f8ff530 fix port and ip 2025-01-30 12:57:31 +01:00
Cayo Puigdefabregas 7155a309b5 fix flow for a bad key 2025-01-30 10:37:50 +01:00
Cayo Puigdefabregas 48cb351263 add key for open service 2025-01-30 10:20:53 +01:00
pedro c482a6e1ba rename initial_datas to demo_data on docs 2025-01-29 19:36:05 +01:00
pedro b109fb730a Merge pull request 'add predefined_token in initial_datas' (#6) from predefined_token into release
Reviewed-on: #6
2025-01-29 17:09:49 +00:00
pedro f09efcdf10 adapt docker to demo situation
about predefined token and did
2025-01-29 18:08:48 +01:00
pedro 32be93d294 improve error messages when service is unavailable 2025-01-29 18:03:00 +01:00
pedro 3d77e9983e idhub: demo vault and default did
use DEMO
2025-01-29 18:02:17 +01:00
pedro c8729973b7 demo_data: cleanup test
commiter: cayo
2025-01-29 17:22:24 +01:00
Cayo Puigdefabregas 0f258c9076 add keys_did.json to examples] 2025-01-29 17:14:45 +01:00
Cayo Puigdefabregas 97e2bb36c4 drop pdbs 2025-01-29 17:14:45 +01:00
Cayo Puigdefabregas 84cfe72362 command predetermined diddocument 2025-01-29 17:14:45 +01:00
Cayo Puigdefabregas 730d689430 rename initial_data to demo_data 2025-01-29 17:14:45 +01:00
Cayo Puigdefabregas b6162e2491 add create_default_did in initial_data 2025-01-29 17:14:45 +01:00
Cayo Puigdefabregas 8b3f4704e7 signed if is authorized 2025-01-29 17:14:45 +01:00
Cayo Puigdefabregas 1ae201bc94 add predefined_token in initial_datas 2025-01-29 17:14:45 +01:00
pedro 273440818f Merge pull request 'idhub-docker' (#5) from idhub-docker into release
Reviewed-on: #5
2025-01-29 16:13:47 +00:00
pedro cea449c878 finish docker integration 2025-01-29 17:11:58 +01:00
pedro a50b28518d docker compose: add DeviceSnapshotV1 2025-01-23 11:14:19 +01:00
pedro 411e8806bb docker: copy src files into image 2025-01-22 16:23:02 +01:00
pedro 6f9a06faa1 add docker integration for localhost 2025-01-09 09:24:12 +01:00
pedro 6e4f3d7be3 bugfix requirement error
error was

```
idhub-1  | ValueError: numpy.dtype size changed, may indicate binary incompatibility. Expected 96 from C header, got 88 from PyObject
idhub-1 exited with code 1
```

> The solution is to pin down the numpy version to any before the 2.0.0

related https://stackoverflow.com/questions/78634235/numpy-dtype-size-changed-may-indicate-binary-incompatibility-expected-96-from
2025-01-09 09:23:45 +01:00
pedro d9a0de1c68 add env var ENABLE_DOMAIN_CHECKER
when using localhost and standalone idhub instance we could avoid this
checker to facilitate testing and deployment
2025-01-09 09:19:18 +01:00
cayop 60890ae8f1 Merge pull request 'cred_snapshots' (#4) from cred_snapshots into release
Reviewed-on: #4
2025-01-07 16:27:17 +00:00
Cayo Puigdefabregas 2c3225f08b update cache_context 2025-01-07 17:23:55 +01:00
Cayo Puigdefabregas 4c07920a6a fix sign snapshots 2024-12-05 19:25:22 +01:00
Cayo Puigdefabregas 3a7eb0f97e issue with out save credential 2024-11-29 10:59:36 +01:00
Cayo Puigdefabregas baa4d87a11 sign snapschot credential from webhook 2024-11-25 20:18:47 +01:00
Cayo Puigdefabregas 9ebd3b18a4 create credential snapshots 2024-11-21 12:39:34 +01:00
Cayo Puigdefabregas baf5c1e924 fix schema workbench and credential 2024-11-09 19:00:39 +01:00
Cayo Puigdefabregas 717b4ab6d5 change data 2024-11-06 10:50:09 +01:00
Cayo Puigdefabregas 8cfa83f921 add snapshot schema and credential template 2024-11-06 10:08:37 +01:00
29 changed files with 864 additions and 63 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
.git
db.sqlite

45
.env.example Normal file
View 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'

View file

@ -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
View 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
View 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
View 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
View 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 "${@}"

View file

@ -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
View file

@ -0,0 +1 @@
{"label": "DEMO", "key_material": "{\"kty\": \"OKP\", \"crv\": \"Ed25519\", \"x\": \"IRqDfIumhbKKHhqMjOngikQmGoT1cZ6LPP-JjXa8CsY\", \"d\": \"AZXUEnJYFbGcn3Ebzy3vQWYFzx6rdnoHKilaMYUWuHA\", \"kid\": \"Generated\"}"}

View file

@ -7,34 +7,51 @@ 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.create_admin_users(ADMIN_EMAIL, ADMIN_PASSWORD) self.predefined_did = kwargs['predefined_did']
if settings.CREATE_TEST_USERS: # on demo situation, encrypted vault is hardcoded with password DEMO
for u in range(1, 6): cache.set("KEY_DIDS", "DEMO", None)
user = 'user{}@example.org'.format(u)
self.create_users(user, '1234')
self.org = Organization.objects.create( self.org = Organization.objects.create(
name=self.DOMAIN, name=self.DOMAIN,
domain=self.DOMAIN, domain=self.DOMAIN,
main=True main=True
) )
self.org.set_encrypted_sensitive_data()
self.org.save()
self.create_admin_users(ADMIN_EMAIL, ADMIN_PASSWORD)
if settings.CREATE_TEST_USERS:
for u in range(1, 6):
user = 'user{}@example.org'.format(u)
self.create_users(user, '1234')
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 = ''

View 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)

View file

@ -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()

View file

@ -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

View 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"
}
}

View file

@ -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')

View file

@ -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

View file

@ -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

View file

@ -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

View 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"
]
}
}
}
}
]
}

View file

@ -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='')

View 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'),
),
]

View file

@ -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)

View file

@ -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>')

View 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 %}

View file

@ -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'),
] ]

View file

@ -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)