Skip to content

Commit

Permalink
feat(download): add credential download endpoint
Browse files Browse the repository at this point in the history
Refs: #24
  • Loading branch information
Phil91 committed Apr 2, 2024
1 parent e08caa2 commit 7ecaa60
Show file tree
Hide file tree
Showing 22 changed files with 421 additions and 130 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,13 @@ public void AttachAndModifyCredential(Guid credentialId, Action<CompanySsiDetail
.Where(x => x.Id == credentialId)
.Select(x => new ValueTuple<VerifiedCredentialTypeId, Guid>(x.VerifiedCredentialTypeId, x.CreatorUserId))
.SingleOrDefaultAsync();

public Task<(bool Exists, bool IsSameCompany, IEnumerable<(DocumentStatusId StatusId, byte[] Content)> Documents)> GetSignedCredentialForCredentialId(Guid credentialId, string bpnl) =>
_dbContext.CompanySsiDetails
.Where(x => x.Id == credentialId)
.Select(x => new ValueTuple<bool, bool, IEnumerable<ValueTuple<DocumentStatusId, byte[]>>>(
true,
x.Bpnl == bpnl,
x.Documents.Where(d => d.DocumentTypeId == DocumentTypeId.VERIFIED_CREDENTIAL).Select(d => new ValueTuple<DocumentStatusId, byte[]>(d.DocumentStatusId, d.DocumentContent))))
.SingleOrDefaultAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ public interface ICredentialRepository
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);
Task<(bool Exists, bool IsSameCompany, IEnumerable<(DocumentStatusId StatusId, byte[] Content)> Documents)> GetSignedCredentialForCredentialId(Guid credentialId, string bpnl);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@
// ********************************************************************************/

// <auto-generated />
using System;

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities;

#nullable disable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/********************************************************************************
* 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.Portal.Backend.Framework.ErrorHandling;
using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess;
using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.ErrorHandling;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity;
using System.Text.Json;

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

public class CredentialBusinessLogic : ICredentialBusinessLogic
{
private readonly IIssuerRepositories _repositories;
private readonly IIdentityData _identityData;

public CredentialBusinessLogic(IIssuerRepositories repositories, IIdentityService identityService)
{
_repositories = repositories;
_identityData = identityService.IdentityData;
}

public async Task<JsonDocument> GetCredentialDocument(Guid credentialId)
{
var (exists, isSameCompany, documents) = await _repositories.GetInstance<ICredentialRepository>().GetSignedCredentialForCredentialId(credentialId, _identityData.Bpnl).ConfigureAwait(false);
if (!exists)
{
throw NotFoundException.Create(CredentialErrors.CREDENTIAL_NOT_FOUND, new[] { new ErrorParameter("credentialId", credentialId.ToString()) });
}

if (!isSameCompany)
{
throw ForbiddenException.Create(CredentialErrors.COMPANY_NOT_ALLOWED);
}

if (documents.Count() != 1)
{
throw ConflictException.Create(CredentialErrors.SIGNED_CREDENTIAL_NOT_FOUND);
}

var credentialContent = documents.Single();
var content = System.Text.Encoding.UTF8.GetString(credentialContent.Content);
return JsonDocument.Parse(content);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic;
using System.Text.Json;

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

public static class RevocationServiceCollectionExtensions
public interface ICredentialBusinessLogic
{
public static IServiceCollection AddRevocationService(this IServiceCollection services) =>
services
.AddTransient<IRevocationBusinessLogic, RevocationBusinessLogic>();
Task<JsonDocument> GetCredentialDocument(Guid credentialId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ await _repositories
d.ExpiryDate,
d.Documents))
.SingleOrDefault(),
(InvalidOperationException _) => throw ConflictException.Create(CredentialErrors.MULTIPLE_SSI_DETAIL))))
(InvalidOperationException _) => throw ConflictException.Create(IssuerErrors.MULTIPLE_SSI_DETAIL))))
.ToList()))
.ToListAsync()
.ConfigureAwait(false);
Expand All @@ -127,7 +127,7 @@ await _repositories
d.ExpiryDate,
d.Documents))
.SingleOrDefault(),
(InvalidOperationException _) => throw ConflictException.Create(CredentialErrors.MULTIPLE_SSI_DETAIL))))
(InvalidOperationException _) => throw ConflictException.Create(IssuerErrors.MULTIPLE_SSI_DETAIL))))
.ToList()))
.ToListAsync()
.ConfigureAwait(false);
Expand Down Expand Up @@ -197,7 +197,7 @@ public async Task ApproveCredential(Guid credentialId, CancellationToken cancell
c.ExpiryDate = expiry;
c.ProcessId = processId;
});
var typeValue = data.Type.GetEnumValue() ?? throw UnexpectedConditionException.Create(CredentialErrors.CREDENTIAL_TYPE_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialType", data.Type.ToString()) });
var typeValue = data.Type.GetEnumValue() ?? throw UnexpectedConditionException.Create(IssuerErrors.CREDENTIAL_TYPE_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialType", data.Type.ToString()) });
var mailParameters = new MailParameter[]
{
new("requestName", typeValue),
Expand All @@ -216,7 +216,7 @@ private void UpdateIssuanceDate(Guid credentialId, SsiApprovalData data,
var frameworkCredential = data.Schema!.Deserialize<FrameworkCredential>();
if (frameworkCredential == null)
{
throw UnexpectedConditionException.Create(CredentialErrors.SCHEMA_NOT_FRAMEWORK);
throw UnexpectedConditionException.Create(IssuerErrors.SCHEMA_NOT_FRAMEWORK);
}

var newCredential = frameworkCredential with { IssuanceDate = _dateTimeProvider.OffsetNow };
Expand All @@ -235,42 +235,42 @@ private static void ValidateApprovalData(Guid credentialId, bool exists, SsiAppr
{
if (!exists)
{
throw NotFoundException.Create(CredentialErrors.SSI_DETAILS_NOT_FOUND, new ErrorParameter[] { new("credentialId", credentialId.ToString()) });
throw NotFoundException.Create(IssuerErrors.SSI_DETAILS_NOT_FOUND, new ErrorParameter[] { new("credentialId", credentialId.ToString()) });
}

if (data.Status != CompanySsiDetailStatusId.PENDING)
{
throw ConflictException.Create(CredentialErrors.CREDENTIAL_NOT_PENDING, new ErrorParameter[] { new("credentialId", credentialId.ToString()), new("status", CompanySsiDetailStatusId.PENDING.ToString()) });
throw ConflictException.Create(IssuerErrors.CREDENTIAL_NOT_PENDING, new ErrorParameter[] { new("credentialId", credentialId.ToString()), new("status", CompanySsiDetailStatusId.PENDING.ToString()) });
}

if (string.IsNullOrWhiteSpace(data.Bpn))
{
throw UnexpectedConditionException.Create(CredentialErrors.BPN_NOT_SET);
throw UnexpectedConditionException.Create(IssuerErrors.BPN_NOT_SET);
}

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

if (data.Kind != VerifiedCredentialTypeKindId.FRAMEWORK && data.Kind != VerifiedCredentialTypeKindId.MEMBERSHIP && data.Kind != VerifiedCredentialTypeKindId.BPN)
{
throw ConflictException.Create(CredentialErrors.KIND_NOT_SUPPORTED, new ErrorParameter[] { new("kind", data.Kind != null ? data.Kind.Value.ToString() : "empty 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(CredentialErrors.EMPTY_VERSION);
throw ConflictException.Create(IssuerErrors.EMPTY_VERSION);
}

if (data.ProcessId is not null)
{
throw UnexpectedConditionException.Create(CredentialErrors.ALREADY_LINKED_PROCESS);
throw UnexpectedConditionException.Create(IssuerErrors.ALREADY_LINKED_PROCESS);
}

if (data.Schema is null)
{
throw UnexpectedConditionException.Create(CredentialErrors.SCHEMA_NOT_SET);
throw UnexpectedConditionException.Create(IssuerErrors.SCHEMA_NOT_SET);
}
}

Expand All @@ -282,7 +282,7 @@ private DateTimeOffset GetExpiryDate(DateTimeOffset? expiryDate)

if (expiry < now)
{
throw ConflictException.Create(CredentialErrors.EXPIRY_DATE_IN_PAST);
throw ConflictException.Create(IssuerErrors.EXPIRY_DATE_IN_PAST);
}

return expiry > future ? future : expiry;
Expand All @@ -295,15 +295,15 @@ public async Task RejectCredential(Guid credentialId, CancellationToken cancella
var (exists, status, type, processId, processStepIds) = await companySsiRepository.GetSsiRejectionData(credentialId).ConfigureAwait(false);
if (!exists)
{
throw NotFoundException.Create(CredentialErrors.SSI_DETAILS_NOT_FOUND, new ErrorParameter[] { new("credentialId", credentialId.ToString()) });
throw NotFoundException.Create(IssuerErrors.SSI_DETAILS_NOT_FOUND, new ErrorParameter[] { new("credentialId", credentialId.ToString()) });
}

if (status != CompanySsiDetailStatusId.PENDING)
{
throw ConflictException.Create(CredentialErrors.CREDENTIAL_NOT_PENDING, new ErrorParameter[] { new("credentialId", credentialId.ToString()), new("status", CompanySsiDetailStatusId.PENDING.ToString()) });
throw ConflictException.Create(IssuerErrors.CREDENTIAL_NOT_PENDING, new ErrorParameter[] { new("credentialId", credentialId.ToString()), new("status", CompanySsiDetailStatusId.PENDING.ToString()) });
}

var typeValue = type.GetEnumValue() ?? throw UnexpectedConditionException.Create(CredentialErrors.CREDENTIAL_TYPE_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialType", type.ToString()) });
var typeValue = type.GetEnumValue() ?? throw UnexpectedConditionException.Create(IssuerErrors.CREDENTIAL_TYPE_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialType", type.ToString()) });
var content = JsonSerializer.Serialize(new { Type = type, CredentialId = credentialId }, Options);
await _portalService.AddNotification(content, _identity.IdentityId, NotificationTypeId.CREDENTIAL_REJECTED, cancellationToken).ConfigureAwait(false);

Expand Down Expand Up @@ -401,33 +401,33 @@ public async Task<Guid> CreateFrameworkCredential(CreateFrameworkCredentialReque
var result = await companyCredentialDetailsRepository.CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(requestData.UseCaseFrameworkVersionId, requestData.UseCaseFrameworkId).ConfigureAwait(false);
if (!result.Exists)
{
throw ControllerArgumentException.Create(CredentialErrors.EXTERNAL_TYPE_DETAIL_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialExternalTypeDetailId", requestData.UseCaseFrameworkId.ToString()) });
throw ControllerArgumentException.Create(IssuerErrors.EXTERNAL_TYPE_DETAIL_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialExternalTypeDetailId", requestData.UseCaseFrameworkId.ToString()) });
}

if (result.Expiry < _dateTimeProvider.OffsetNow)
{
throw ControllerArgumentException.Create(CredentialErrors.EXPIRY_DATE_IN_PAST);
throw ControllerArgumentException.Create(IssuerErrors.EXPIRY_DATE_IN_PAST);
}

if (string.IsNullOrWhiteSpace(result.Version))
{
throw ControllerArgumentException.Create(CredentialErrors.EMPTY_VERSION);
throw ControllerArgumentException.Create(IssuerErrors.EMPTY_VERSION);
}

if (string.IsNullOrWhiteSpace(result.Template))
{
throw ControllerArgumentException.Create(CredentialErrors.EMPTY_TEMPLATE);
throw ControllerArgumentException.Create(IssuerErrors.EMPTY_TEMPLATE);
}

if (result.ExternalTypeIds.Count() != 1)
{
throw ControllerArgumentException.Create(CredentialErrors.MULTIPLE_USE_CASES);
throw ControllerArgumentException.Create(IssuerErrors.MULTIPLE_USE_CASES);
}

var externalTypeId = result.ExternalTypeIds.Single().GetEnumValue();
if (externalTypeId is null)
{
throw ControllerArgumentException.Create(CredentialErrors.EMPTY_EXTERNAL_TYPE_ID);
throw ControllerArgumentException.Create(IssuerErrors.EMPTY_EXTERNAL_TYPE_ID);
}

var holderDid = await GetHolderInformation(requestData.Holder, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -460,7 +460,7 @@ private async Task<string> GetHolderInformation(string didDocumentLocation, Canc
{
if (!Uri.TryCreate(didDocumentLocation, UriKind.Absolute, out var uri) || uri.Scheme != "https" || !string.IsNullOrEmpty(uri.Query) || !string.IsNullOrEmpty(uri.Fragment) || UrlPathInvalidCharsRegex.IsMatch(uri.AbsolutePath))
{
throw ControllerArgumentException.Create(CredentialErrors.INVALID_DID_LOCATION, null, nameof(didDocumentLocation));
throw ControllerArgumentException.Create(IssuerErrors.INVALID_DID_LOCATION, null, nameof(didDocumentLocation));
}

var client = _clientFactory.CreateClient("didDocumentDownload");
Expand All @@ -469,7 +469,7 @@ private async Task<string> GetHolderInformation(string didDocumentLocation, Canc
var did = await result.Content.ReadFromJsonAsync<DidDocument>(Options, cancellationToken).ConfigureAwait(false);
if (did == null)
{
throw ConflictException.Create(CredentialErrors.DID_NOT_SET);
throw ConflictException.Create(IssuerErrors.DID_NOT_SET);
}

return did.Id;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/********************************************************************************
* 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 Microsoft.AspNetCore.Mvc;
using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Web;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Extensions;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models;
using System.Text.Json;

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

public static class CredentialController
{
public static RouteGroupBuilder MapCredentialApi(this RouteGroupBuilder group)
{
var issuer = group.MapGroup("/credential");

issuer.MapGet("{credentialId}", ([FromRoute] Guid credentialId, [FromServices] ICredentialBusinessLogic logic) => logic.GetCredentialDocument(credentialId))
.WithSwaggerDescription("The endpoint enables users to download the credential (full json) of their own company.",
"Example: GET: api/credential/{credentialId}")
.RequireAuthorization(r =>
{
r.RequireRole("view_credential");
r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn));
})
.WithDefaultResponses()
.Produces(StatusCodes.Status200OK, typeof(JsonDocument), Constants.JsonContentType)
.Produces(StatusCodes.Status409Conflict, typeof(ErrorResponse), Constants.JsonContentType);

return issuer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,25 @@

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

public static class CredentialServiceCollectionExtensions
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCredentialService(this IServiceCollection services, IConfigurationSection section) =>
internal static IServiceCollection AddServices(this IServiceCollection services, IConfiguration config) =>
services
.AddIssuerService(config.GetSection("Credential"))
.AddRevocationService()
.AddRevocationService()
.AddCredentialService();

private static IServiceCollection AddIssuerService(this IServiceCollection services, IConfigurationSection section) =>
services
.ConfigureCredentialSettings(section)
.AddTransient<IIssuerBusinessLogic, IssuerBusinessLogic>();

private static IServiceCollection AddRevocationService(this IServiceCollection services) =>
services
.AddTransient<IRevocationBusinessLogic, RevocationBusinessLogic>();

private static IServiceCollection AddCredentialService(this IServiceCollection services) =>
services
.AddTransient<ICredentialBusinessLogic, CredentialBusinessLogic>();
}
Loading

0 comments on commit 7ecaa60

Please sign in to comment.