Merge pull request 'webhook' (#2) from webhook into release
Reviewed-on: #2
This commit is contained in:
commit
4a0f234e6c
29
README.md
29
README.md
|
@ -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)
|
||||
|
||||
## 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
|
||||
|
||||
For detailed documentation, visit [Documentation Link](http://idhub.pangea.org/help/).
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -155,6 +155,11 @@
|
|||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -82,7 +82,8 @@ INSTALLED_APPS = [
|
|||
'idhub_auth',
|
||||
'oidc4vp',
|
||||
'idhub',
|
||||
'promotion'
|
||||
'promotion',
|
||||
'webhook'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
|
@ -26,4 +26,5 @@ urlpatterns = [
|
|||
path('', include('idhub.urls')),
|
||||
path('oidc4vp/', include('oidc4vp.urls')),
|
||||
path('promotion/', include('promotion.urls')),
|
||||
path('webhook/', include('webhook.urls')),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class WebhookConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'webhook'
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -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()),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,7 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Token(models.Model):
|
||||
token = models.UUIDField()
|
|
@ -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")
|
||||
|
|
@ -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 %}
|
|
@ -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
|
|
@ -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'),
|
||||
]
|
|
@ -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')
|
||||
|
Loading…
Reference in New Issue