Merge pull request 'webhook' (#2) from webhook into release

Reviewed-on: #2
This commit is contained in:
cayop 2024-06-14 14:20:17 +00:00
commit 4a0f234e6c
18 changed files with 453 additions and 1 deletions

View file

@ -174,6 +174,35 @@ IdHub's repository is organized into several directories, each serving a specifi
- **utils**: A utility folder containing various helper scripts and tools developed by us but that are independent of idHub. Even so, IdHub uses them and needs them (examples of this are the validation system for the data that is loades by excel, or the system that manages the sskit) - **utils**: A utility folder containing various helper scripts and tools developed by us but that are independent of idHub. Even so, IdHub uses them and needs them (examples of this are the validation system for the data that is loades by excel, or the system that manages the sskit)
## Webhook
You need define a token un the admin section "/webhool/tokens"
For define one query here there are a python example:
```
import requests
import json
url = "https://api.example.com/webhook/verify/"
data = {
"type": "credential",
"data": {
'@context': ['https://www.....
}
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, data=json.dumps(data))
response.status_code == 200
response.json()
```
The response of verification can be ```{'status': 'success'}``` or ```{'status': 'fail'}```
If no there are *type* in data or this is not a *credential* then, the verification proccess hope a *presentation*
The field *data* have the credential or presentation.
## Documentation ## Documentation
For detailed documentation, visit [Documentation Link](http://idhub.pangea.org/help/). For detailed documentation, visit [Documentation Link](http://idhub.pangea.org/help/).

View file

@ -0,0 +1,84 @@
# Generated by Django 4.2.5 on 2024-06-13 08:08
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('idhub', '0004_alter_event_type'),
]
operations = [
migrations.AlterField(
model_name='file_datas',
name='created_at',
field=models.DateTimeField(auto_now=True, verbose_name='Date'),
),
migrations.AlterField(
model_name='file_datas',
name='file_name',
field=models.CharField(max_length=250, verbose_name='File'),
),
migrations.AlterField(
model_name='file_datas',
name='success',
field=models.BooleanField(default=True, verbose_name='Success'),
),
migrations.AlterField(
model_name='schemas',
name='_description',
field=models.CharField(
db_column='description',
max_length=250,
null=True,
verbose_name='Description',
),
),
migrations.AlterField(
model_name='schemas',
name='_name',
field=models.TextField(db_column='name', null=True, verbose_name='Name'),
),
migrations.AlterField(
model_name='schemas',
name='created_at',
field=models.DateTimeField(auto_now=True, verbose_name='Date'),
),
migrations.AlterField(
model_name='schemas',
name='file_schema',
field=models.CharField(max_length=250, verbose_name='Schema'),
),
migrations.AlterField(
model_name='verificablecredential',
name='issued_on',
field=models.DateTimeField(null=True, verbose_name='Issued on'),
),
migrations.AlterField(
model_name='verificablecredential',
name='status',
field=models.PositiveSmallIntegerField(
choices=[(1, 'Enabled'), (2, 'Issued'), (3, 'Revoked'), (4, 'Expired')],
default=1,
verbose_name='Status',
),
),
migrations.AlterField(
model_name='verificablecredential',
name='type',
field=models.CharField(max_length=250, verbose_name='Type'),
),
migrations.AlterField(
model_name='verificablecredential',
name='user',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='vcredentials',
to=settings.AUTH_USER_MODEL,
verbose_name='User',
),
),
]

View file

@ -155,6 +155,11 @@
</li> </li>
</ul> </ul>
</li> </li>
<li class="nav-item">
<a id="wallet" class="nav-link{% if path == 'tokens' %} active2{% endif %}" href="{% url 'webhook:tokens' %}">
{% trans "Webhook Tokens" %}
</a>
</li>
</ul> </ul>
</li> </li>
<li class="nav-item"> <li class="nav-item">

View file

@ -0,0 +1,22 @@
# Generated by Django 4.2.5 on 2024-06-13 08:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('idhub_auth', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='user',
name='is_active',
field=models.BooleanField(default=True, verbose_name='is active'),
),
migrations.AlterField(
model_name='user',
name='is_admin',
field=models.BooleanField(default=False, verbose_name='is admin'),
),
]

View file

@ -82,7 +82,8 @@ INSTALLED_APPS = [
'idhub_auth', 'idhub_auth',
'oidc4vp', 'oidc4vp',
'idhub', 'idhub',
'promotion' 'promotion',
'webhook'
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View file

@ -26,4 +26,5 @@ urlpatterns = [
path('', include('idhub.urls')), path('', include('idhub.urls')),
path('oidc4vp/', include('oidc4vp.urls')), path('oidc4vp/', include('oidc4vp.urls')),
path('promotion/', include('promotion.urls')), path('promotion/', include('promotion.urls')),
path('webhook/', include('webhook.urls')),
] ]

0
webhook/__init__.py Normal file
View file

3
webhook/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
webhook/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class WebhookConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'webhook'

1
webhook/forms.py Normal file
View file

@ -0,0 +1 @@

View file

@ -0,0 +1,27 @@
# Generated by Django 4.2.5 on 2024-06-13 08:08
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='Token',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('token', models.UUIDField()),
],
),
]

View file

7
webhook/models.py Normal file
View file

@ -0,0 +1,7 @@
from django.db import models
# Create your models here.
class Token(models.Model):
token = models.UUIDField()

67
webhook/tables.py Normal file
View file

@ -0,0 +1,67 @@
import django_tables2 as tables
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from webhook.models import Token
class ButtonColumn(tables.Column):
attrs = {
"a": {
"type": "button",
"class": "text-danger",
"title": "Remove",
}
}
# it makes no sense to order a column of buttons
orderable = False
# django_tables will only call the render function if it doesn't find
# any empty values in the data, so we stop it from matching the data
# to any value considered empty
empty_values = ()
def render(self):
return format_html('<i class="bi bi-trash"></i>')
class TokensTable(tables.Table):
delete = ButtonColumn(
verbose_name=_("Delete"),
linkify={
"viewname": "webhook:delete_token",
"args": [tables.A("pk")]
},
orderable=False
)
token = tables.Column(verbose_name=_("Token"), empty_values=())
def render_view_user(self):
return format_html('<i class="bi bi-eye"></i>')
# def render_token(self, record):
# return record.get_memberships()
# def order_membership(self, queryset, is_descending):
# # TODO: Test that this doesn't return more rows than it should
# queryset = queryset.order_by(
# ("-" if is_descending else "") + "memberships__type"
# )
# return (queryset, True)
# def render_role(self, record):
# return record.get_roles()
# def order_role(self, queryset, is_descending):
# queryset = queryset.order_by(
# ("-" if is_descending else "") + "roles"
# )
# return (queryset, True)
class Meta:
model = Token
template_name = "idhub/custom_table.html"
fields = ("token", "view_user")

View file

@ -0,0 +1,14 @@
{% extends "idhub/base_admin.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %}
<h3>
<i class="{{ icon }}"></i>
{{ subtitle }}
</h3>
{% render_table table %}
<div class="form-actions-no-box">
<a class="btn btn-green-admin" href="{% url 'webhook:new_token' %}">{% translate "Generate a new token" %} <i class="bi bi-plus"></i></a>
</div>
{% endblock %}

76
webhook/tests.py Normal file
View file

@ -0,0 +1,76 @@
import json
from uuid import uuid4
from django.test import TestCase
from django.core.cache import cache
from django.urls import reverse
from django.conf import settings
from idhub_auth.models import User
from oidc4vp.models import Organization
from webhook.models import Token
class AdminDashboardViewTest(TestCase):
def setUp(self):
cache.set("KEY_DIDS", '1234', None)
self.user = User.objects.create_user(
email='normaluser@example.org',
password='testpass12',
)
self.user.accept_gdpr=True
self.user.save()
self.admin_user = User.objects.create_superuser(
email='adminuser@example.org',
password='adminpass12')
self.admin_user.accept_gdpr=True
self.admin_user.save()
self.org = Organization.objects.create(name="testserver", main=True)
settings.DOMAIN = self.org.name
settings.ENABLE_EMAIL = False
self.client.login(email='adminuser@example.org', password='adminpass12')
def get_credential(self):
return {'@context': ['https://www.w3.org/2018/credentials/v1', 'https://idhub.pangea.org/context/base.jsonld', 'https://idhub.pangea.org/context/membership-card.jsonld'], 'type': ['VerifiableCredential', 'VerifiableAttestation', 'MembershipCard'], 'id': 'http://localhost/credentials/1', 'issuer': {'id': 'did:web:idhub.pangea.org:dids:z6Mko7ParpJZzBopW48DY8125Tcz9hMCaH7YzteWKnFcVzhu', 'name': 'Pangea'}, 'issuanceDate': '2024-06-11T14:41:12Z', 'issued': '2024-06-11T14:41:12Z', 'validFrom': '2024-06-11T14:41:12Z', 'name': [{'value': 'Membership Card', 'lang': 'en'}, {'value': 'Carnet de soci/a', 'lang': 'ca_ES'}, {'value': 'Carnet de socio/a', 'lang': 'es'}], 'description': [{'value': "The membership card specifies an individual's subscription or enrollment in specific services or benefits issued by an organization.", 'lang': 'en'}, {'value': "El carnet de soci especifica la subscripció o la inscripció d'un individu en serveis o beneficis específics emesos per una organització.", 'lang': 'ca_ES'}, {'value': 'El carnet de socio especifica la suscripción o inscripción de un individuo en servicios o beneficios específicos emitidos por uns organización.', 'lang': 'es'}], 'credentialSubject': {'id': 'did:web:localhost:did-registry:z6MkoAQJ96ppDQFw1idhvcR9NssPJQLFBoVDD2L62r7fh5yS', 'firstName': 'Pedro', 'lastName': 'Lagasta', 'email': 'user1@example.org', 'typeOfPerson': 'natural', 'organisation': 'Pangea', 'membershipType': 'Employee', 'membershipId': '1a', 'affiliatedSince': '2023-01-01'}, 'credentialSchema': {'id': 'https://idhub.pangea.org/vc_schemas/membership-card.json', 'type': 'FullJsonSchemaValidator2021'}, 'proof': {'type': 'Ed25519Signature2018', 'proofPurpose': 'assertionMethod', 'verificationMethod': 'did:web:idhub.pangea.org:dids:z6Mko7ParpJZzBopW48DY8125Tcz9hMCaH7YzteWKnFcVzhu#z6Mko7ParpJZzBopW48DY8125Tcz9hMCaH7YzteWKnFcVzhu', 'created': '2024-06-11T14:59:37Z', 'jws': 'eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..kC2_QSkWRQ_fZ6C0_lRWvf4xuuxOueeCMb6dQFPKyn1h3gAHg_tb98RETIZeaQD6759wjJuH-IPJpvo4vzCDDA'}}
def test_render_tokens_page(self):
response = self.client.get('/webhook/tokens/', follow=True)
self.assertEqual(response.status_code, 200)
def test_new_token(self):
response = self.client.get('/webhook/tokens/new', follow=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(Token.objects.count(), 1)
tk = Token.objects.first()
url = "/webhook/tokens/{}/del".format(tk.id)
response = self.client.get(url, follow=True)
self.assertEqual(Token.objects.count(), 0)
def test_verify(self):
token = uuid4()
Token.objects.create(token=token)
data = {
"type": "credential",
"data": self.get_credential()
}
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = self.client.post(
reverse('webhook:verify'),
content_type='application/json',
data=data,
headers=headers
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'status': 'success'})
self.client.headers = None

13
webhook/urls.py Normal file
View file

@ -0,0 +1,13 @@
from webhook import views
from django.urls import path
app_name = 'webhook'
urlpatterns = [
path('verify/', views.webhook_verify, name='verify'),
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'),
]

96
webhook/views.py Normal file
View file

@ -0,0 +1,96 @@
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.base import View
from django.http import JsonResponse
from django_tables2 import SingleTableView
from pyvckit.verify import verify_vp, verify_vc
from uuid import uuid4
from idhub.mixins import AdminView
from webhook.models import Token
from webhook.tables import TokensTable
@csrf_exempt
def webhook_verify(request):
if request.method == 'POST':
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()
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")
try:
vc = json.dumps(vc)
except Exception:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
func = verify_vp
if typ == "credential":
func = verify_vc
if func(vc):
return JsonResponse({'status': 'success'}, 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")
section = "Credential"
subtitle = _('Managament Tokens')
icon = 'bi bi-key'
model = Token
table_class = TokensTable
def get_queryset(self):
"""
Override the get_queryset method to filter events based on the user type.
"""
return Token.objects.filter().order_by("-id")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'tokens': Token.objects,
})
return context
class TokenDeleteView(AdminView, DeleteView):
model = Token
def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
self.object.delete()
return redirect('webhook:tokens')
class TokenNewView(AdminView, View):
def get(self, request, *args, **kwargs):
self.check_valid_user()
Token.objects.create(token=uuid4())
return redirect('webhook:tokens')