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:
```
python manage.py initial_datas
python manage.py demo_data
```
6. Start the development server:
```
@ -79,7 +79,7 @@ MEDIA_ROOT=/tmp/media/
# Currently unused but will be used in the future
# 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
# 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:
```
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.
- **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.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from decouple import config
from idhub.models import Schemas
from django.core.cache import cache
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 webhook.models import Token
User = get_user_model()
class Command(BaseCommand):
help = "Insert minimum datas for the project"
help = "Insert minimum data for the project"
DOMAIN = settings.DOMAIN
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):
ADMIN_EMAIL = settings.INITIAL_ADMIN_EMAIL
ADMIN_PASSWORD = settings.INITIAL_ADMIN_PASSWORD
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')
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)
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:
self.create_organizations()
@ -45,6 +62,61 @@ class Command(BaseCommand):
su = User.objects.create_superuser(email=email, password=password)
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):
u = User.objects.create(email=email, password=password)
@ -105,6 +177,7 @@ class Command(BaseCommand):
assert dname
assert title
except Exception:
ldata = {}
title = ''
_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,6 +33,7 @@ class UserView(LoginRequiredMixin):
]
def get(self, request, *args, **kwargs):
if settings.ENABLE_DOMAIN_CHECKER:
err_txt = "User domain is {} which does not match server domain {}".format(
request.get_host(), settings.DOMAIN
)
@ -55,6 +56,7 @@ class UserView(LoginRequiredMixin):
return url or response
def post(self, request, *args, **kwargs):
if settings.ENABLE_DOMAIN_CHECKER:
err_txt = "User domain is {} which does not match server domain {}".format(
request.get_host(), settings.DOMAIN
)

View file

@ -680,10 +680,18 @@ class VerificableCredential(models.Model):
credential_subject = ujson.loads(data).get("credentialSubject", {})
return credential_subject.items()
def issue(self, did, domain):
def issue(self, did, domain, save=True):
if self.status == self.Status.ISSUED:
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.issued_on = datetime.datetime.now().astimezone(pytz.utc)
@ -700,6 +708,9 @@ class VerificableCredential(models.Model):
if not valid:
return
if not save:
return vc_str
self.data = self.user.encrypt_data(vc_str)
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):
template_name = "idhub/user/waiting.html"
title = _("Comunication with admin")
subtitle = _('Service temporary close')
title = _("Comunication with admin required")
subtitle = _('Service temporarily closed')
section = ""
icon = 'bi bi-file-earmark-medical'
success_url = reverse_lazy('idhub:user_dashboard')

View file

@ -2796,11 +2796,11 @@ msgid "Data Protection"
msgstr "Protecció de dades"
#: idhub/user/views.py:183
msgid "Comunication with admin"
msgstr "Comunicació amb l'admin"
msgid "Comunication with admin required"
msgstr "Es requereix comunicació amb l'admin"
#: idhub/user/views.py:184
msgid "Service temporary close"
msgid "Service temporarily closed"
msgstr "Tancament temporal del servei"
#: idhub/user/views.py:407

View file

@ -2789,11 +2789,11 @@ msgid "Data Protection"
msgstr "Proteccion de datos"
#: idhub/user/views.py:183
msgid "Comunication with admin"
msgstr "Comunicación con el admin"
msgid "Comunication with admin required"
msgstr "Se requiere comunicación con el admin"
#: idhub/user/views.py:184
msgid "Service temporary close"
msgid "Service temporarily closed"
msgstr "Cierre temporal del servicio"
#: idhub/user/views.py:407

View file

@ -6,6 +6,7 @@ black==23.9.1
python-decouple==3.8
jsonschema[format]==4.19.1
pandas==2.1.1
numpy>=1.21,<2.0
xlrd==2.0.1
odfpy==1.4.1
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!
DEBUG = config('DEBUG', default=False, cast=bool)
DEVELOPMENT = config('DEVELOPMENT', default=False, cast=bool)
DOMAIN = config("DOMAIN")
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)
CREATE_TEST_USERS = config('CREATE_TEST_USERS', default=False, 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='')

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.utils.translation import gettext_lazy as _
# Create your models here.
class Token(models.Model):
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
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from webhook.models import Token
class ButtonColumn(tables.Column):
class ButtonRemoveColumn(tables.Column):
attrs = {
"a": {
"type": "button",
@ -25,7 +26,7 @@ class ButtonColumn(tables.Column):
class TokensTable(tables.Table):
delete = ButtonColumn(
delete = ButtonRemoveColumn(
verbose_name=_("Delete"),
linkify={
"viewname": "webhook:delete_token",
@ -33,11 +34,19 @@ class TokensTable(tables.Table):
},
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=())
label = tables.Column(verbose_name=_("Label"), empty_values=())
def render_view_user(self):
return format_html('<i class="bi bi-eye"></i>')
# def render_view_user(self):
# return format_html('<i class="bi bi-eye"></i>')
# def render_token(self, record):
# return record.get_memberships()
@ -63,5 +72,13 @@ class TokensTable(tables.Table):
class Meta:
model = Token
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 = [
path('verify/', views.webhook_verify, name='verify'),
path('sign/', views.webhook_issue, name='sign'),
path('tokens/', views.WebHookTokenView.as_view(), name='tokens'),
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>/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.utils.translation import gettext_lazy as _
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.core.cache import cache
from django.http import JsonResponse
from django_tables2 import SingleTableView
from pyvckit.verify import verify_vp, verify_vc
from uuid import uuid4
from django.urls import reverse_lazy
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.tables import TokensTable
@ -18,12 +22,16 @@ from webhook.tables import TokensTable
@csrf_exempt
def webhook_verify(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]
tk = Token.objects.filter(token=token).first()
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)
@ -51,6 +59,66 @@ def webhook_verify(request):
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):
template_name = "token.html"
title = _("Credential management")
@ -86,11 +154,39 @@ class TokenDeleteView(AdminView, DeleteView):
return redirect('webhook:tokens')
class TokenNewView(AdminView, View):
class TokenStatusView(AdminView, DeleteView):
model = Token
def get(self, request, *args, **kwargs):
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')
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)