-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [FC-0006] add Verifiable Credentials feature
- Loading branch information
1 parent
761a167
commit 9cb0612
Showing
88 changed files
with
3,841 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
from django import forms | ||
from django.contrib import admin | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
from .issuance.models import IssuanceConfiguration, IssuanceLine | ||
from .issuance.utils import get_issuers | ||
from .toggles import is_verifiable_credentials_enabled | ||
|
||
|
||
class IssuersListFilter(admin.SimpleListFilter): | ||
title = _("Issuer ID") | ||
|
||
parameter_name = "issuer_id" | ||
|
||
def lookups(self, request, model_admin): | ||
return [(issuer["issuer_id"], issuer["issuer_name"] or issuer["id"]) for issuer in get_issuers()] | ||
|
||
def queryset(self, request, queryset): | ||
if not self.value(): | ||
return queryset | ||
|
||
return queryset.filter(issuer_id=self.value()) | ||
|
||
|
||
class IssuanceLineAdmin(admin.ModelAdmin): | ||
""" | ||
Issuance line admin setup. | ||
""" | ||
|
||
list_display = ( | ||
"uuid", | ||
"user_credential", | ||
"get_issuer_id", | ||
"storage_id", | ||
"processed", | ||
"status_index", | ||
"status", | ||
) | ||
readonly_fields = [ | ||
"uuid", | ||
"status_index", | ||
"get_issuer_id", | ||
] | ||
list_filter = [ | ||
IssuersListFilter, | ||
"storage_id", | ||
"status", | ||
"processed", | ||
] | ||
search_fields = ("uuid",) | ||
|
||
@admin.display(description="issuer id") | ||
def get_issuer_id(self, issiance_line): | ||
limit = 30 | ||
if len(issiance_line.issuer_id) > limit: | ||
return f"{issiance_line.issuer_id[:limit]}..." | ||
return issiance_line.issuer_id | ||
|
||
|
||
class IssuanceConfigurationForm(forms.ModelForm): | ||
def clean_enabled(self): | ||
""" | ||
Do not allow to disable the last issuer. | ||
""" | ||
# don't validate if new: | ||
if not self.instance.pk: | ||
return self.cleaned_data["enabled"] | ||
|
||
enabled_count = self.instance.__class__.objects.filter(enabled=True).count() | ||
if enabled_count < 2 and self.cleaned_data["enabled"] is False: | ||
raise forms.ValidationError(_("At least one Issuer must be always enabled!")) | ||
|
||
return self.cleaned_data["enabled"] | ||
|
||
|
||
class IssuanceConfigurationAdmin(admin.ModelAdmin): | ||
""" | ||
Issuance configuration admin setup. | ||
""" | ||
|
||
form = IssuanceConfigurationForm | ||
|
||
list_display = [ | ||
"issuer_id", | ||
"issuer_name", | ||
"enabled", | ||
] | ||
|
||
def has_delete_permission(self, request, obj=None): | ||
return False | ||
|
||
|
||
if is_verifiable_credentials_enabled(): | ||
admin.site.register(IssuanceLine, IssuanceLineAdmin) | ||
admin.site.register(IssuanceConfiguration, IssuanceConfigurationAdmin) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
from django.apps import AppConfig | ||
|
||
from .toggles import is_verifiable_credentials_enabled | ||
|
||
|
||
class VerifiableCredentialsConfig(AppConfig): | ||
name = "credentials.apps.verifiable_credentials" | ||
verbose_name = "Verifiable Credentials" | ||
|
||
def ready(self): | ||
""" | ||
Performs initial registrations for checks, signals, etc. | ||
""" | ||
if is_verifiable_credentials_enabled(): | ||
from . import signals # pylint: disable=unused-import,import-outside-toplevel | ||
from .checks import vc_settings_checks # pylint: disable=unused-import,import-outside-toplevel |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
from django.core.checks import Error, Tags, register | ||
|
||
from .settings import vc_settings | ||
from .toggles import ENABLE_VERIFIABLE_CREDENTIALS | ||
|
||
|
||
@register(Tags.compatibility) | ||
def vc_settings_checks(*args, **kwargs): | ||
""" | ||
Checks the consistency of the verifiable_credentials settings. | ||
Raises compatibility Errors upon: | ||
- No default data models defined | ||
- No default storages defined | ||
- DEFAULT_ISSUER[ID] is not set | ||
- DEFAULT_ISSUER[KEY] is not set | ||
Returns: | ||
List of any Errors. | ||
""" | ||
errors = [] | ||
|
||
if not vc_settings.DEFAULT_DATA_MODELS: | ||
errors.append( | ||
Error( | ||
"No default data models defined.", | ||
hint="Add at least one data model to the DEFAULT_DATA_MODELS setting.", | ||
id="verifiable_credentials.E001", | ||
) | ||
) | ||
|
||
if not vc_settings.DEFAULT_STORAGES: | ||
errors.append( | ||
Error( | ||
"No default storages defined.", | ||
hint="Add at least one storage to the DEFAULT_STORAGES setting.", | ||
id="verifiable_credentials.E003", | ||
) | ||
) | ||
|
||
if not vc_settings.DEFAULT_ISSUER.get("ID"): | ||
errors.append( | ||
Error( | ||
f"DEFAULT_ISSUER[ID] is mandatory when {ENABLE_VERIFIABLE_CREDENTIALS.name} is True.", | ||
hint=" Set DEFAULT_ISSUER[ID] to a valid DID string.", | ||
id="verifiable_credentials.E004", | ||
) | ||
) | ||
|
||
if not vc_settings.DEFAULT_ISSUER.get("KEY"): | ||
errors.append( | ||
Error( | ||
f"DEFAULT_ISSUER[KEY] is mandatory when {ENABLE_VERIFIABLE_CREDENTIALS.name} is True.", | ||
hint="Set DEFAULT_ISSUER[KEY] to a valid key string.", | ||
id="verifiable_credentials.E005", | ||
) | ||
) | ||
|
||
return errors |
103 changes: 103 additions & 0 deletions
103
credentials/apps/verifiable_credentials/composition/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
""" | ||
Composition hierarchy. | ||
CredentialDataModel | ||
| | ||
|_ VerifiableCredentialsDataModel + StatusList2021EntryMixin | ||
|_ OpenBadgesDataModel + StatusList2021EntryMixin | ||
|_ StatusListDataModel | ||
""" | ||
import inspect | ||
from collections import OrderedDict | ||
|
||
from rest_framework import serializers | ||
|
||
from .schemas import IssuerSchema | ||
|
||
|
||
class CredentialDataModel(serializers.Serializer): # pylint: disable=abstract-method | ||
""" | ||
Basic credential construction machinery. | ||
""" | ||
|
||
VERSION = None | ||
ID = None | ||
NAME = None | ||
|
||
context = serializers.SerializerMethodField( | ||
method_name="collect_context", help_text="https://www.w3.org/TR/vc-data-model/#contexts" | ||
) | ||
type = serializers.SerializerMethodField(help_text="https://www.w3.org/TR/vc-data-model/#types") | ||
issuer = IssuerSchema(source="*", help_text="https://www.w3.org/TR/vc-data-model/#issuer") | ||
issued = serializers.DateTimeField(source="modified", help_text="https://www.w3.org/2018/credentials/#issued") | ||
issuanceDate = serializers.DateTimeField( | ||
source="modified", | ||
help_text="Deprecated (requred by the didkit for now) https://www.w3.org/2018/credentials/#issuanceDate", | ||
) | ||
validFrom = serializers.DateTimeField(source="modified", help_text="https://www.w3.org/2018/credentials/#validFrom") | ||
validUntil = serializers.DateTimeField( | ||
source="expiration_date", help_text="https://www.w3.org/2018/credentials/#validUntil" | ||
) | ||
|
||
@classmethod | ||
def get_context(cls): | ||
""" | ||
Provide root context for all verifiable credentials. | ||
""" | ||
return [ | ||
"https://www.w3.org/2018/credentials/v1", | ||
] | ||
|
||
@classmethod | ||
def get_types(cls): | ||
""" | ||
Provide root types for all verifiable credentials. | ||
""" | ||
return [ | ||
"VerifiableCredential", | ||
] | ||
|
||
def collect_context(self, __): | ||
""" | ||
Collect contexts. | ||
- include default root context | ||
- include data model context | ||
""" | ||
return self._collect_hierarchically(class_method="get_context") | ||
|
||
def get_type(self, issuance_line): | ||
""" | ||
Collect corresponding types. | ||
- include default root type(s) | ||
- include data model type(s) | ||
- include credential-specific type(s) | ||
""" | ||
data_model_types = self._collect_hierarchically(class_method="get_types") | ||
credential_types = self.resolve_credential_type(issuance_line) | ||
return data_model_types + credential_types | ||
|
||
def resolve_credential_type(self, issuance_line): # pylint: disable=unused-argument | ||
""" | ||
Map Open edX credential type to data model types. | ||
Decides: which types should be included based on the source Open edX credential type. | ||
See: | ||
https://w3c.github.io/vc-imp-guide/#creating-new-credential-types | ||
https://schema.org/EducationalOccupationalCredential | ||
""" | ||
return [] | ||
|
||
def _collect_hierarchically(self, class_method): | ||
""" | ||
Call given method through model MRO and collect returned values. | ||
""" | ||
values = OrderedDict() | ||
reversed_mro_classes = reversed(inspect.getmro(type(self))) | ||
for base_class in reversed_mro_classes: | ||
if hasattr(base_class, class_method): | ||
values_list = getattr(base_class, class_method)() | ||
for value in values_list: | ||
values[value] = base_class.__name__ | ||
return list(values.keys()) |
97 changes: 97 additions & 0 deletions
97
credentials/apps/verifiable_credentials/composition/open_badges.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
""" | ||
Open Badges 3.0.* data model. | ||
See specification: https://1edtech.github.io/openbadges-specification/ob_v3p0.html | ||
""" | ||
|
||
from django.utils.translation import gettext as _ | ||
from rest_framework import serializers | ||
|
||
from ..composition.status_list import CredentialWithStatusList2021DataModel | ||
|
||
|
||
class AchievementSchema(serializers.Serializer): # pylint: disable=abstract-method | ||
""" | ||
Open Badges achievement. | ||
https://1edtech.github.io/openbadges-specification/ob_v3p0.html#achievement-0 | ||
""" | ||
|
||
TYPE = "Achievement" | ||
|
||
id = serializers.CharField(source="user_credential.uuid") | ||
type = serializers.CharField(default=TYPE) | ||
name = serializers.CharField(source="user_credential.credential.title") | ||
description = serializers.SerializerMethodField(source="user_credential.credential.program.title") | ||
|
||
class Meta: | ||
read_only_fields = "__all__" | ||
|
||
def get_description(self, issuance_line): | ||
return ( | ||
issuance_line.user_credential.attributes.filter(name="description").values_list("value", flat=True).first() | ||
) | ||
|
||
|
||
class CredentialSubjectSchema(serializers.Serializer): # pylint: disable=abstract-method | ||
""" | ||
Open Badges credential subject. | ||
""" | ||
|
||
TYPE = "AchievementSubject" | ||
|
||
id = serializers.CharField(source="subject_id") | ||
type = serializers.CharField(default=TYPE) | ||
achievement = AchievementSchema(source="*") | ||
|
||
class Meta: | ||
read_only_fields = "__all__" | ||
|
||
|
||
class OpenBadgesDataModel(CredentialWithStatusList2021DataModel): # pylint: disable=abstract-method | ||
""" | ||
Open Badges data model. | ||
""" | ||
|
||
VERSION = "3.0" | ||
ID = "obv3" | ||
NAME = _("Open Badges Specification v3.0") | ||
|
||
id = serializers.UUIDField( | ||
source="uuid", format="urn", help_text="https://www.w3.org/TR/vc-data-model/#identifiers" | ||
) | ||
name = serializers.CharField(source="credential_name") | ||
credentialSubject = CredentialSubjectSchema( | ||
source="*", help_text="https://1edtech.github.io/openbadges-specification/ob_v3p0.html#credentialsubject-0" | ||
) | ||
|
||
class Meta: | ||
read_only_fields = "__all__" | ||
|
||
@classmethod | ||
def get_context(cls): | ||
return [ | ||
"https://purl.imsglobal.org/spec/ob/v3p0/context.json", | ||
] | ||
|
||
@classmethod | ||
def get_types(cls): | ||
return [ | ||
"OpenBadgeCredential", | ||
] | ||
|
||
|
||
class OpenBadges301DataModel(OpenBadgesDataModel): # pylint: disable=abstract-method | ||
""" | ||
Open Badges data model. | ||
""" | ||
|
||
VERSION = "3.0.1" | ||
ID = "obv301" | ||
NAME = _("Open Badges Specification v3.0.1") | ||
|
||
@classmethod | ||
def get_context(cls): | ||
return [ | ||
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json", | ||
"https://purl.imsglobal.org/spec/ob/v3p0/extensions.json", | ||
] |
Oops, something went wrong.