Skip to content

Commit

Permalink
feat(revocation): add endpoints to revoke credentials
Browse files Browse the repository at this point in the history
* add endpoint for issuer to revoke a credential
* add endpoint for holder to revoke a credential
* add logic to revoke credentials when they are expired

Refs: #14 #15 #16
  • Loading branch information
Phil91 committed Mar 27, 2024
1 parent 336e342 commit 58caa17
Show file tree
Hide file tree
Showing 44 changed files with 1,313 additions and 111 deletions.
1 change: 0 additions & 1 deletion DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ nuget/nuget/-/JsonSchema.Net/6.0.3, MIT AND OFL-1.1 AND CC-BY-SA-4.0, approved,
nuget/nuget/-/Laraue.EfCoreTriggers.Common/7.1.0, MIT, approved, #10247
nuget/nuget/-/Laraue.EfCoreTriggers.PostgreSql/7.1.0, MIT, approved, #10248
nuget/nuget/-/Mono.TextTemplating/2.2.1, MIT, approved, clearlydefined
nuget/nuget/-/Newtonsoft.Json/12.0.2, MIT AND BSD-3-Clause, approved, #11114
nuget/nuget/-/Newtonsoft.Json/13.0.1, MIT AND BSD-3-Clause, approved, #3266
nuget/nuget/-/Newtonsoft.Json/13.0.3, MIT AND BSD-3-Clause, approved, #3266
nuget/nuget/-/Npgsql.EntityFrameworkCore.PostgreSQL/7.0.11, PostgreSQL AND MIT AND Apache-2.0, approved, #10081
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,15 @@ public async Task ExecuteAsync(CancellationToken stoppingToken)

var now = dateTimeProvider.OffsetNow;
var companySsiDetailsRepository = repositories.GetInstance<ICompanySsiDetailsRepository>();
var processStepRepository = repositories.GetInstance<IProcessStepRepository>();
var inactiveVcsToDelete = now.AddDays(-(_settings.InactiveVcsToDeleteInWeeks * 7));
var expiredVcsToDelete = now.AddMonths(-_settings.ExpiredVcsToDeleteInMonth);

var credentials = outerLoopRepositories.GetInstance<ICompanySsiDetailsRepository>()
.GetExpiryData(now, inactiveVcsToDelete, expiredVcsToDelete);
await foreach (var credential in credentials.WithCancellation(stoppingToken).ConfigureAwait(false))
{
await ProcessCredentials(credential, companySsiDetailsRepository, repositories, portalService,
stoppingToken);
await ProcessCredentials(credential, companySsiDetailsRepository, repositories, portalService, processStepRepository, stoppingToken);
}
}
catch (Exception ex)
Expand All @@ -104,6 +104,7 @@ private static async Task ProcessCredentials(
ICompanySsiDetailsRepository companySsiDetailsRepository,
IIssuerRepositories repositories,
IPortalService portalService,
IProcessStepRepository processStepRepository,
CancellationToken cancellationToken)
{
if (data.ScheduleData.IsVcToDelete)
Expand All @@ -112,7 +113,7 @@ private static async Task ProcessCredentials(
}
else if (data.ScheduleData.IsVcToDecline)
{
await HandleDecline(data, companySsiDetailsRepository, portalService, cancellationToken).ConfigureAwait(false);
HandleDecline(data.Id, companySsiDetailsRepository, processStepRepository);
}
else
{
Expand All @@ -123,30 +124,21 @@ private static async Task ProcessCredentials(
await repositories.SaveAsync().ConfigureAwait(false);
}

private static async ValueTask HandleDecline(
CredentialExpiryData data,
private static void HandleDecline(
Guid credentialId,
ICompanySsiDetailsRepository companySsiDetailsRepository,
IPortalService portalService,
CancellationToken cancellationToken)
IProcessStepRepository processStepRepository)
{
var content = JsonSerializer.Serialize(new { Type = data.VerifiedCredentialTypeId, CredentialId = data.Id }, Options);
await portalService.AddNotification(content, data.RequesterId, NotificationTypeId.CREDENTIAL_REJECTED, cancellationToken);
companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(data.Id, c =>
var processId = processStepRepository.CreateProcess(ProcessTypeId.DECLINE_CREDENTIAL).Id;
processStepRepository.CreateProcessStep(ProcessStepTypeId.REVOKE_CREDENTIAL, ProcessStepStatusId.TODO, processId);
companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(credentialId, c =>
{
c.CompanySsiDetailStatusId = data.CompanySsiDetailStatusId;
c.ProcessId = null;
},
c =>
{
c.CompanySsiDetailStatusId = CompanySsiDetailStatusId.INACTIVE;
c.ProcessId = processId;
});

var typeValue = data.VerifiedCredentialTypeId.GetEnumValue() ?? throw new UnexpectedConditionException($"VerifiedCredentialType {data.VerifiedCredentialTypeId} does not exists");
var mailParameters = new Dictionary<string, string>
{
{ "requestName", typeValue },
{ "reason", "The credential is already expired" }
};
await portalService.TriggerMail("CredentialRejected", data.RequesterId, mailParameters, cancellationToken);
}

private static async ValueTask HandleNotification(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@

namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models;

public record DocumentData
(
public record DocumentData(
Guid DocumentId,
string DocumentName,
DocumentTypeId argDocumentTypeId);
DocumentTypeId DocumentTypeId);
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Microsoft.EntityFrameworkCore;
using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums;
using System.Text.Json;

Expand Down Expand Up @@ -76,4 +77,28 @@ public CredentialRepository(IssuerDbContext dbContext)
.Where(x => x.CompanySsiDetailId == credentialId)
.Select(x => new ValueTuple<string, string?>(x.CompanySsiDetail!.Bpnl, x.CallbackUrl))
.SingleOrDefaultAsync();

public Task<(bool Exists, Guid? ExternalCredentialId, CompanySsiDetailStatusId StatusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> Documents)> GetRevocationDataById(Guid credentialId) =>
_dbContext.CompanySsiDetails
.Where(x => x.Id == credentialId)
.Select(x => new ValueTuple<bool, Guid?, CompanySsiDetailStatusId, IEnumerable<(Guid, DocumentStatusId)>>(
true,
x.ExternalCredentialId,
x.CompanySsiDetailStatusId,
x.Documents.Select(d => new ValueTuple<Guid, DocumentStatusId>(d.Id, d.DocumentStatusId))))
.SingleOrDefaultAsync();

public void AttachAndModifyCredential(Guid credentialId, Action<CompanySsiDetail>? initialize, Action<CompanySsiDetail> modify)
{
var entity = new CompanySsiDetail(credentialId, string.Empty, default!, default!, null!, Guid.Empty, default!);
initialize?.Invoke(entity);
_dbContext.CompanySsiDetails.Attach(entity);
modify(entity);
}

public Task<(VerifiedCredentialTypeId TypeId, Guid RequesterId)> GetCredentialNotificationData(Guid credentialId) =>
_dbContext.CompanySsiDetails
.Where(x => x.Id == credentialId)
.Select(x => new ValueTuple<VerifiedCredentialTypeId, Guid>(x.VerifiedCredentialTypeId, x.CreatorUserId))
.SingleOrDefaultAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,17 @@ public void AssignDocumentToCompanySsiDetails(Guid documentId, Guid companySsiDe
var document = new CompanySsiDetailAssignedDocument(documentId, companySsiDetailId);
_dbContext.CompanySsiDetailAssignedDocuments.Add(document);
}

public void AttachAndModifyDocuments(IEnumerable<(Guid DocumentId, Action<Document>? Initialize, Action<Document> Modify)> documentData)
{
var initial = documentData.Select(x =>
{
var document = new Document(x.DocumentId, null!, null!, null!, default, default, default, default);
x.Initialize?.Invoke(document);
return (Document: document, x.Modify);
}
).ToList();
_dbContext.AttachRange(initial.Select(x => x.Document));
initial.ForEach(x => x.Modify(x.Document));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
********************************************************************************/

using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums;
using System.Text.Json;

Expand All @@ -31,4 +32,7 @@ public interface ICredentialRepository
Task<(VerifiedCredentialTypeKindId CredentialTypeKindId, JsonDocument Schema)> GetCredentialStorageInformationById(Guid credentialId);
Task<(Guid? ExternalCredentialId, VerifiedCredentialTypeKindId KindId, bool HasEncryptionInformation, string? CallbackUrl)> GetExternalCredentialAndKindId(Guid credentialId);
Task<(string Bpn, string? CallbackUrl)> GetCallbackUrl(Guid credentialId);
Task<(bool Exists, Guid? ExternalCredentialId, CompanySsiDetailStatusId StatusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> Documents)> GetRevocationDataById(Guid credentialId);
void AttachAndModifyCredential(Guid credentialId, Action<CompanySsiDetail>? initialize, Action<CompanySsiDetail> modify);
Task<(VerifiedCredentialTypeId TypeId, Guid RequesterId)> GetCredentialNotificationData(Guid credentialId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ public interface IDocumentRepository
Document CreateDocument(string documentName, byte[] documentContent, byte[] hash, MediaTypeId mediaTypeId, DocumentTypeId documentTypeId, Action<Document>? setupOptionalFields);

void AssignDocumentToCompanySsiDetails(Guid documentId, Guid companySsiDetailId);
void AttachAndModifyDocuments(IEnumerable<(Guid DocumentId, Action<Document>? Initialize, Action<Document> Modify)> documentData);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums;

public enum ProcessStepTypeId
{
// Issuer Process
// CREATE CREDENTIAL PROCESS
CREATE_CREDENTIAL = 1,
SIGN_CREDENTIAL = 2,
SAVE_CREDENTIAL_DOCUMENT = 3,
CREATE_CREDENTIAL_FOR_HOLDER = 4,
TRIGGER_CALLBACK = 5,

// DECLINE PROCESS
REVOKE_CREDENTIAL = 100,
TRIGGER_NOTIFICATION = 101,
TRIGGER_MAIL = 102
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums;
public enum ProcessTypeId
{
CREATE_CREDENTIAL = 1,
DECLINE_CREDENTIAL = 2
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@
{
"id": "1268a76a-ca19-4dd8-b932-01f24071d562",
"verified_credential_external_type_id": 3,
"version": "1.0.0",
"version": "1.0",
"template": null,
"valid_from": "2023-06-01 00:00:00.000000 +00:00",
"expiry": "2023-09-30 00:00:00.000000 +00:00"
},
{
"id": "1268a76a-ca19-4dd8-b932-01f24071d563",
"verified_credential_external_type_id": 1,
"version": "2.0.0",
"version": "2.0",
"template": null,
"valid_from": "2023-06-01 00:00:00.000000 +00:00",
"expiry": "2023-12-23 00:00:00.000000 +00:00"
},
{
"id": "1268a76a-ca19-4dd8-b932-01f24071d564",
"verified_credential_external_type_id": 1,
"version": "3.0.0",
"version": "3.0",
"template": null,
"valid_from": "2024-01-01 00:00:00.000000 +00:00",
"expiry": "2024-12-31 00:00:00.000000 +00:00"
},
{
"id": "1268a76a-ca19-4dd8-b932-01f24071d565",
"verified_credential_external_type_id": 5,
"version": "1.0.0",
"version": "1.0",
"template": null,
"valid_from": "2024-01-01 00:00:00.000000 +00:00",
"expiry": "2024-12-31 00:00:00.000000 +00:00"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@
{
"id": "1268a76a-ca19-4dd8-b932-01f24071d560",
"verified_credential_external_type_id": 1,
"version": "1.0.0",
"version": "1.0",
"template": "https://catena-x.net/fileadmin/user_upload/04_Einfuehren_und_umsetzen/Governance_Framework/231016_Catena-X_Use_Case_Framework_Traceability.pdf",
"valid_from": "2023-06-01 00:00:00.000000 +00:00",
"expiry": "2023-09-30 00:00:00.000000 +00:00"
},
{
"id": "1268a76a-ca19-4dd8-b932-01f24071d561",
"verified_credential_external_type_id": 2,
"version": "1.0.0",
"version": "1.0",
"template": "https://catena-x.net/fileadmin/user_upload/04_Einfuehren_und_umsetzen/Governance_Framework/231016_Catena-X_Use_Case_Framework_PCF.pdf",
"valid_from": "2023-06-01 00:00:00.000000 +00:00",
"expiry": "2023-09-30 00:00:00.000000 +00:00"
},
{
"id": "37aa6259-b452-4d50-b09e-827929dcfa15",
"verified_credential_external_type_id": 6,
"version": "1.0.0",
"version": "1.0",
"template": "https://catena-x.net/fileadmin/user_upload/04_Einfuehren_und_umsetzen/Governance_Framework/231016_Catena-X_Use_Case_Framework_PCF.pdf",
"valid_from": "2023-10-16 00:00:00.000000 +00:00",
"expiry": "2023-10-16 00:00:00.000000 +00:00"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/********************************************************************************
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

using System.Text.Json.Serialization;

namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models;

public record RevokeCredentialRequest(
[property: JsonPropertyName("payload")] RevokePayload Payload
);

public record RevokePayload(
[property: JsonPropertyName("revoke")] bool Revoke
);
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services;

public interface IBasicAuthTokenService
{
Task<HttpClient> GetBasicAuthorizedClient<T>(BasicAuthSettings settings, CancellationToken cancellationToken);
Task<HttpClient> GetBasicAuthorizedClient<T>(BasicAuthSettings settings, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ public interface IWalletService
Task<string> SignCredential(Guid credentialId, CancellationToken cancellationToken);
Task<Guid> CreateCredentialForHolder(string holderWalletUrl, string clientId, string clientSecret, string credential, CancellationToken cancellationToken);
Task<JsonDocument> GetCredential(Guid externalCredentialId, CancellationToken cancellationToken);
Task RevokeCredentialForIssuer(Guid externalCredentialId, CancellationToken cancellationToken);
Task RevokeCredentialForHolder(string holderWalletUrl, string clientId, string clientSecret, Guid externalCredentialId, CancellationToken cancellationToken);
}
26 changes: 26 additions & 0 deletions src/externalservices/Wallet.Service/Services/WalletService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,30 @@ public async Task<Guid> CreateCredentialForHolder(string holderWalletUrl, string

return response.Id;
}

public async Task RevokeCredentialForIssuer(Guid externalCredentialId, CancellationToken cancellationToken)
{
var client = await _basicAuthTokenService.GetBasicAuthorizedClient<WalletService>(_settings, cancellationToken);
var data = new RevokeCredentialRequest(new RevokePayload(true));
await client.PatchAsJsonAsync($"/api/v2.0.0/credentials/{externalCredentialId}", data, Options, cancellationToken)
.CatchingIntoServiceExceptionFor("revoke-credential", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE,
async x => (false, await x.Content.ReadAsStringAsync().ConfigureAwait(false)))
.ConfigureAwait(false);
}

public async Task RevokeCredentialForHolder(string holderWalletUrl, string clientId, string clientSecret, Guid externalCredentialId, CancellationToken cancellationToken)
{
var authSettings = new BasicAuthSettings
{
ClientId = clientId,
ClientSecret = clientSecret,
TokenAddress = $"{holderWalletUrl}/oauth/token"
};
var client = await _basicAuthTokenService.GetBasicAuthorizedClient<WalletService>(authSettings, cancellationToken);
var data = new RevokeCredentialRequest(new RevokePayload(true));
await client.PatchAsJsonAsync($"/api/v2.0.0/credentials/{externalCredentialId}", data, Options, cancellationToken)
.CatchingIntoServiceExceptionFor("revoke-credential", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE,
async x => (false, await x.Content.ReadAsStringAsync().ConfigureAwait(false)))
.ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/********************************************************************************
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models;

namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic;

public interface IRevocationBusinessLogic
{
Task RevokeIssuerCredential(Guid credentialId, CancellationToken cancellationToken);
Task RevokeHolderCredential(Guid credentialId, TechnicalUserDetails walletInformation, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -197,15 +197,17 @@ public async Task ApproveCredential(Guid credentialId, CancellationToken cancell
c.ProcessId = processId;
});
var typeValue = data.Type.GetEnumValue() ?? throw UnexpectedConditionException.Create(CredentialErrors.CREDENTIAL_TYPE_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialType", data.Type.ToString()) });
var content = JsonSerializer.Serialize(new { data.Type, CredentialId = credentialId }, Options);
await _portalService.AddNotification(content, _identity.IdentityId, NotificationTypeId.CREDENTIAL_APPROVAL, cancellationToken).ConfigureAwait(false);
var mailParameters = new Dictionary<string, string>
{
{ "requestName", typeValue },
{ "credentialType", typeValue },
{ "expiryDate", expiry.ToString("o", CultureInfo.InvariantCulture) }
};
await _portalService.TriggerMail("CredentialApproval", _identity.IdentityId, mailParameters, cancellationToken).ConfigureAwait(false);

var content = JsonSerializer.Serialize(new { data.Type, CredentialId = credentialId }, Options);
await _portalService.AddNotification(content, _identity.IdentityId, NotificationTypeId.CREDENTIAL_APPROVAL, cancellationToken).ConfigureAwait(false);

await _repositories.SaveAsync().ConfigureAwait(false);
}

Expand Down Expand Up @@ -505,8 +507,9 @@ private async Task<Guid> HandleCredentialProcessCreation(
c.ClientId = technicalUserDetails.ClientId;
c.ClientSecret = secret;
c.InitializationVector = initializationVector;
c.HolderWalletUrl = technicalUserDetails.WalletUrl;
c.EncryptionMode = cryptoConfig.Index;
c.InitializationVector = initializationVector;
c.CallbackUrl = callbackUrl;
});

Expand Down
Loading

0 comments on commit 58caa17

Please sign in to comment.