Skip to content

Commit

Permalink
feat(revocation): adjust revocation for holder
Browse files Browse the repository at this point in the history
Refs: #15
  • Loading branch information
Phil91 committed Apr 24, 2024
1 parent 7baa61b commit 5639bd5
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 249 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,13 @@ public CredentialRepository(IssuerDbContext dbContext)
.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) =>
public Task<(bool Exists, bool IsSameBpnl, Guid? ExternalCredentialId, CompanySsiDetailStatusId StatusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> Documents)> GetRevocationDataById(Guid credentialId, string bpnl) =>
_dbContext.CompanySsiDetails
.Where(x => x.Id == credentialId)
.Select(x => new ValueTuple<bool, Guid?, CompanySsiDetailStatusId, IEnumerable<(Guid, DocumentStatusId)>>(
.Where(x =>
x.Id == credentialId)
.Select(x => new ValueTuple<bool, bool, Guid?, CompanySsiDetailStatusId, IEnumerable<(Guid, DocumentStatusId)>>(
true,
x.Bpnl == bpnl,
x.ExternalCredentialId,
x.CompanySsiDetailStatusId,
x.Documents.Select(d => new ValueTuple<Guid, DocumentStatusId>(d.Id, d.DocumentStatusId))))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +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);
Task<(bool Exists, bool IsSameBpnl, Guid? ExternalCredentialId, CompanySsiDetailStatusId StatusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> Documents)> GetRevocationDataById(Guid credentialId, string bpnl);
void AttachAndModifyCredential(Guid credentialId, Action<CompanySsiDetail>? initialize, Action<CompanySsiDetail> modify);
Task<(VerifiedCredentialTypeId TypeId, string RequesterId)> GetCredentialNotificationData(Guid credentialId);
Task<(bool Exists, bool IsSameCompany, IEnumerable<(DocumentStatusId StatusId, byte[] Content)> Documents)> GetSignedCredentialForCredentialId(Guid credentialId, string bpnl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,4 @@ public interface IWalletService
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);
}
16 changes: 0 additions & 16 deletions src/externalservices/Wallet.Service/Services/WalletService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,20 +123,4 @@ await client.PatchAsJsonAsync($"/api/v2.0.0/credentials/{externalCredentialId}",
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
Expand Up @@ -17,12 +17,9 @@
* 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);
Task RevokeCredential(Guid credentialId, bool revokeForIssuer, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,21 +253,13 @@ private static void ValidateApprovalData(Guid credentialId, bool exists, SsiAppr
throw UnexpectedConditionException.Create(IssuerErrors.BPN_NOT_SET);
}

if (data.DetailData == null && data.Kind == VerifiedCredentialTypeKindId.FRAMEWORK)
{
throw ConflictException.Create(IssuerErrors.EXTERNAL_TYPE_DETAIL_ID_NOT_SET);
}
ValidateFrameworkCredential(data);

if (data.Kind != VerifiedCredentialTypeKindId.FRAMEWORK && data.Kind != VerifiedCredentialTypeKindId.MEMBERSHIP && data.Kind != VerifiedCredentialTypeKindId.BPN)
if (Enum.GetValues<VerifiedCredentialTypeKindId>().All(x => x != data.Kind))
{
throw ConflictException.Create(IssuerErrors.KIND_NOT_SUPPORTED, new ErrorParameter[] { new("kind", data.Kind != null ? data.Kind.Value.ToString() : "empty kind") });
}

if (data.Kind == VerifiedCredentialTypeKindId.FRAMEWORK && string.IsNullOrWhiteSpace(data.DetailData!.Version))
{
throw ConflictException.Create(IssuerErrors.EMPTY_VERSION);
}

if (data.ProcessId is not null)
{
throw UnexpectedConditionException.Create(IssuerErrors.ALREADY_LINKED_PROCESS);
Expand All @@ -279,6 +271,24 @@ private static void ValidateApprovalData(Guid credentialId, bool exists, SsiAppr
}
}

private static void ValidateFrameworkCredential(SsiApprovalData data)
{
if (data.Kind != VerifiedCredentialTypeKindId.FRAMEWORK)
{
return;
}

if (data.DetailData == null)
{
throw ConflictException.Create(IssuerErrors.EXTERNAL_TYPE_DETAIL_ID_NOT_SET);
}

if (string.IsNullOrWhiteSpace(data.DetailData!.Version))
{
throw ConflictException.Create(IssuerErrors.EMPTY_VERSION);
}
}

private DateTimeOffset GetExpiryDate(DateTimeOffset? expiryDate)
{
var now = _dateTimeProvider.OffsetNow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.ErrorHandling;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services;

namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic;
Expand All @@ -32,71 +33,51 @@ public class RevocationBusinessLogic : IRevocationBusinessLogic
{
private readonly IIssuerRepositories _repositories;
private readonly IWalletService _walletService;
private readonly IIdentityData _identityData;

public RevocationBusinessLogic(IIssuerRepositories repositories, IWalletService walletService)
public RevocationBusinessLogic(IIssuerRepositories repositories, IWalletService walletService, IIdentityService identityService)
{
_repositories = repositories;
_walletService = walletService;
_identityData = identityService.IdentityData;
}

public async Task RevokeIssuerCredential(Guid credentialId, CancellationToken cancellationToken)
public async Task RevokeCredential(Guid credentialId, bool revokeForIssuer, CancellationToken cancellationToken)
{
// check for is issuer
var credentialRepository = _repositories.GetInstance<ICredentialRepository>();
var data = await RevokeCredentialInternal(credentialId, credentialRepository).ConfigureAwait(false);
if (data.StatusId != CompanySsiDetailStatusId.ACTIVE)
{
return;
}

// call walletService
await _walletService.RevokeCredentialForIssuer(data.ExternalCredentialId, cancellationToken).ConfigureAwait(false);
UpdateData(credentialId, data.StatusId, data.Documents, credentialRepository);
}

public async Task RevokeHolderCredential(Guid credentialId, TechnicalUserDetails walletInformation, CancellationToken cancellationToken)
{
// check for is holder
var credentialRepository = _repositories.GetInstance<ICredentialRepository>();
var data = await RevokeCredentialInternal(credentialId, credentialRepository).ConfigureAwait(false);
if (data.StatusId != CompanySsiDetailStatusId.ACTIVE)
{
return;
}

// call walletService
await _walletService.RevokeCredentialForHolder(walletInformation.WalletUrl, walletInformation.ClientId, walletInformation.ClientSecret, data.ExternalCredentialId, cancellationToken).ConfigureAwait(false);
UpdateData(credentialId, data.StatusId, data.Documents, credentialRepository);
}

private static async Task<(Guid ExternalCredentialId, CompanySsiDetailStatusId StatusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> Documents)> RevokeCredentialInternal(Guid credentialId, ICredentialRepository credentialRepository)
{
var data = await credentialRepository.GetRevocationDataById(credentialId)
var data = await credentialRepository.GetRevocationDataById(credentialId, _identityData.Bpnl)
.ConfigureAwait(false);
if (!data.Exists)
{
throw NotFoundException.Create(RevocationDataErrors.CREDENTIAL_NOT_FOUND, new ErrorParameter[] { new("credentialId", credentialId.ToString()) });
}

if (!revokeForIssuer && !data.IsSameBpnl)
{
throw ForbiddenException.Create(RevocationDataErrors.NOT_ALLOWED_TO_REVOKE_CREDENTIAL);
}

if (data.ExternalCredentialId is null)
{
throw ConflictException.Create(RevocationDataErrors.EXTERNAL_CREDENTIAL_ID_NOT_SET, new ErrorParameter[] { new("credentialId", credentialId.ToString()) });
}

return (data.ExternalCredentialId.Value, data.StatusId, data.Documents);
}
if (data.StatusId != CompanySsiDetailStatusId.ACTIVE)
{
return;
}

private void UpdateData(Guid credentialId, CompanySsiDetailStatusId statusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> documentData, ICredentialRepository credentialRepository)
{
// call walletService
await _walletService.RevokeCredentialForIssuer(data.ExternalCredentialId.Value, cancellationToken).ConfigureAwait(false);
_repositories.GetInstance<IDocumentRepository>().AttachAndModifyDocuments(
documentData.Select(d => new ValueTuple<Guid, Action<Document>?, Action<Document>>(
data.Documents.Select(d => new ValueTuple<Guid, Action<Document>?, Action<Document>>(
d.DocumentId,
document => document.DocumentStatusId = d.DocumentStatusId,
document => document.DocumentStatusId = DocumentStatusId.INACTIVE
)));

credentialRepository.AttachAndModifyCredential(credentialId,
x => x.CompanySsiDetailStatusId = statusId,
x => x.CompanySsiDetailStatusId = data.StatusId,
x => x.CompanySsiDetailStatusId = CompanySsiDetailStatusId.REVOKED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@ public static RouteGroupBuilder MapRevocationApi(this RouteGroupBuilder group)
{
var revocation = group.MapGroup("/revocation");

revocation.MapPost("issuer/credentials/{credentialId}", ([FromRoute] Guid credentialId, CancellationToken cancellationToken, [FromServices] IRevocationBusinessLogic logic) => logic.RevokeIssuerCredential(credentialId, cancellationToken))
revocation.MapPost("issuer/credentials/{credentialId}", ([FromRoute] Guid credentialId, CancellationToken cancellationToken, [FromServices] IRevocationBusinessLogic logic) => logic.RevokeCredential(credentialId, true, cancellationToken))
.WithSwaggerDescription("Revokes an credential which was issued by the given issuer",
"POST: api/revocation/issuer/credentials/{credentialId}",
"Id of the credential that should be revoked")
.RequireAuthorization(r =>
{
r.RequireRole("revoke_credentials_issuer");
// r.RequireRole("revoke_credentials_issuer");
r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn));
r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity));
})
.WithDefaultResponses()
.Produces(StatusCodes.Status200OK, typeof(Guid));
revocation.MapPost("credentials/{credentialId}", ([FromRoute] Guid credentialId, [FromBody] TechnicalUserDetails data, CancellationToken cancellationToken, [FromServices] IRevocationBusinessLogic logic) => logic.RevokeHolderCredential(credentialId, data, cancellationToken))
revocation.MapPost("credentials/{credentialId}", ([FromRoute] Guid credentialId, CancellationToken cancellationToken, [FromServices] IRevocationBusinessLogic logic) => logic.RevokeCredential(credentialId, false, cancellationToken))
.WithSwaggerDescription("Revokes an credential of an holder",
"POST: api/revocation/credentials/{credentialId}",
"Id of the credential that should be revoked",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public class RevocationErrorMessageContainer : IErrorMessageContainer
{
private static readonly IReadOnlyDictionary<int, string> _messageContainer = new Dictionary<RevocationDataErrors, string> {
{ RevocationDataErrors.CREDENTIAL_NOT_FOUND, "Credential {credentialId} does not exist" },
{ RevocationDataErrors.EXTERNAL_CREDENTIAL_ID_NOT_SET, "External Credential Id must be set for {credentialId}" }
{ RevocationDataErrors.EXTERNAL_CREDENTIAL_ID_NOT_SET, "External Credential Id must be set for {credentialId}" },
{ RevocationDataErrors.NOT_ALLOWED_TO_REVOKE_CREDENTIAL, "Not allowed to revoke credential" }
}.ToImmutableDictionary(x => (int)x.Key, x => x.Value);

public Type Type { get => typeof(RevocationDataErrors); }
Expand All @@ -38,5 +39,6 @@ public class RevocationErrorMessageContainer : IErrorMessageContainer
public enum RevocationDataErrors
{
CREDENTIAL_NOT_FOUND,
EXTERNAL_CREDENTIAL_ID_NOT_SET
EXTERNAL_CREDENTIAL_ID_NOT_SET,
NOT_ALLOWED_TO_REVOKE_CREDENTIAL
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public CredentialExpiryProcessHandler(IIssuerRepositories repositories, IWalletS
public async Task<(IEnumerable<ProcessStepTypeId>? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> RevokeCredential(Guid credentialId, CancellationToken cancellationToken)
{
var credentialRepository = _repositories.GetInstance<ICredentialRepository>();
var data = await credentialRepository.GetRevocationDataById(credentialId)
var data = await credentialRepository.GetRevocationDataById(credentialId, string.Empty)
.ConfigureAwait(false);
if (!data.Exists)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,58 +325,4 @@ public async Task RevokeCredentialForIssuer_WithConflict_ThrowsServiceExceptionW
}

#endregion

#region RevokeCredentialForHolder

[Fact]
public async Task RevokeCredentialForHolder_WithValid_DoesNotThrowException()
{
// Arrange
var id = Guid.NewGuid();
var response = new CreateCredentialResponse(id);
var httpMessageHandlerMock = new HttpMessageHandlerMock(HttpStatusCode.OK, new StringContent(JsonSerializer.Serialize(response)));
var httpClient = new HttpClient(httpMessageHandlerMock)
{
BaseAddress = new Uri("https://base.address.com")
};
A.CallTo(() => _basicAuthTokenService.GetBasicAuthorizedClient<WalletService>(A<BasicAuthSettings>._, A<CancellationToken>._))
.Returns(httpClient);

// Act
await _sut.RevokeCredentialForHolder("https://test.de", "test123", "cl1", Guid.NewGuid(), CancellationToken.None).ConfigureAwait(false);

// Assert
httpMessageHandlerMock.RequestMessage.Should().Match<HttpRequestMessage>(x =>
x.Content is JsonContent &&
(x.Content as JsonContent)!.ObjectType == typeof(RevokeCredentialRequest) &&
((x.Content as JsonContent)!.Value as RevokeCredentialRequest)!.Payload.Revoke);
}

[Theory]
[InlineData(HttpStatusCode.Conflict, "{ \"message\": \"Framework test!\" }", "call to external system revoke-credential failed with statuscode 409 - Message: { \"message\": \"Framework test!\" }")]
[InlineData(HttpStatusCode.BadRequest, "{ \"test\": \"123\" }", "call to external system revoke-credential failed with statuscode 400 - Message: { \"test\": \"123\" }")]
[InlineData(HttpStatusCode.BadRequest, "this is no json", "call to external system revoke-credential failed with statuscode 400 - Message: this is no json")]
[InlineData(HttpStatusCode.Forbidden, null, "call to external system revoke-credential failed with statuscode 403")]
public async Task RevokeCredentialForHolder_WithConflict_ThrowsServiceExceptionWithErrorContent(HttpStatusCode statusCode, string? content, string message)
{
// Arrange
var httpMessageHandlerMock = content == null
? new HttpMessageHandlerMock(statusCode)
: new HttpMessageHandlerMock(statusCode, new StringContent(content));
var httpClient = new HttpClient(httpMessageHandlerMock)
{
BaseAddress = new Uri("https://base.address.com")
};
A.CallTo(() => _basicAuthTokenService.GetBasicAuthorizedClient<WalletService>(A<BasicAuthSettings>._, A<CancellationToken>._)).Returns(httpClient);

// Act
async Task Act() => await _sut.RevokeCredentialForHolder("https://test.de", "test123", "cl1", Guid.NewGuid(), CancellationToken.None).ConfigureAwait(false);

// Assert
var ex = await Assert.ThrowsAsync<ServiceException>(Act);
ex.Message.Should().Be(message);
ex.StatusCode.Should().Be(statusCode);
}

#endregion
}
Loading

0 comments on commit 5639bd5

Please sign in to comment.