Skip to content

Commit

Permalink
feat(issuer): add filter to /api/issuer (#120)
Browse files Browse the repository at this point in the history
* add optional filter for approval type to endpoitn GET /api/issuer
* add Get document endpoint
----------------------
Refs: #111 #27
Reviewed-By: Evelyn Gurschler <evelyn.gurschler@bmw.de>
  • Loading branch information
Phil91 authored May 23, 2024
1 parent 402be5e commit ea5d91a
Show file tree
Hide file tree
Showing 20 changed files with 324 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/********************************************************************************
* 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.Entities.Entities;

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

/// <summary>
/// Possible filter options for the <see cref="CompanySsiDetail"/> pagination
/// </summary>
public enum CompanySsiDetailApprovalType
{
/// <summary>
/// The credential request will be automatically approved
/// </summary>
Automatic = 1,

/// <summary>
/// The operator needs to approve the credential request
/// </summary>
Manual = 2,
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,12 @@ public Task<bool> CheckSsiDetailsExistsForCompany(string bpnl, VerifiedCredentia
.SingleOrDefaultAsync();

/// <inheritdoc />
public IQueryable<CompanySsiDetail> GetAllCredentialDetails(CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId) =>
public IQueryable<CompanySsiDetail> GetAllCredentialDetails(CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailApprovalType? approvalType) =>
_context.CompanySsiDetails.AsNoTracking()
.Where(c =>
(!companySsiDetailStatusId.HasValue || c.CompanySsiDetailStatusId == companySsiDetailStatusId.Value) &&
(!credentialTypeId.HasValue || c.VerifiedCredentialTypeId == credentialTypeId));
(!credentialTypeId.HasValue || c.VerifiedCredentialTypeId == credentialTypeId) &&
(!approvalType.HasValue || (approvalType.Value == CompanySsiDetailApprovalType.Automatic && c.VerifiedCredentialType!.VerifiedCredentialTypeAssignedKind!.VerifiedCredentialTypeKindId == VerifiedCredentialTypeKindId.FRAMEWORK) || (approvalType.Value == CompanySsiDetailApprovalType.Manual && c.VerifiedCredentialType!.VerifiedCredentialTypeAssignedKind!.VerifiedCredentialTypeKindId != VerifiedCredentialTypeKindId.FRAMEWORK)));

/// <inheritdoc />
public IAsyncEnumerable<OwnedVerifiedCredentialData> GetOwnCredentialDetails(string bpnl) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,15 @@

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

public class CredentialRepository : ICredentialRepository
public class CredentialRepository(IssuerDbContext dbContext) : ICredentialRepository
{
private readonly IssuerDbContext _dbContext;

public CredentialRepository(IssuerDbContext dbContext)
{
_dbContext = dbContext;
}

public Task<Guid?> GetWalletCredentialId(Guid credentialId) =>
_dbContext.CompanySsiDetails.Where(x => x.Id == credentialId)
dbContext.CompanySsiDetails.Where(x => x.Id == credentialId)
.Select(x => x.ExternalCredentialId)
.SingleOrDefaultAsync();

public Task<(HolderWalletData HolderWalletData, string? Credential, EncryptionTransformationData EncryptionInformation, string? CallbackUrl)> GetCredentialData(Guid credentialId) =>
_dbContext.CompanySsiDetails
dbContext.CompanySsiDetails
.Where(x => x.Id == credentialId)
.Select(x => new ValueTuple<HolderWalletData, string?, EncryptionTransformationData, string?>(
new HolderWalletData(x.CompanySsiProcessData!.HolderWalletUrl, x.CompanySsiProcessData.ClientId),
Expand All @@ -51,19 +44,19 @@ public CredentialRepository(IssuerDbContext dbContext)
.SingleOrDefaultAsync();

public Task<(bool Exists, Guid CredentialId)> GetDataForProcessId(Guid processId) =>
_dbContext.CompanySsiDetails
dbContext.CompanySsiDetails
.Where(c => c.ProcessId == processId)
.Select(c => new ValueTuple<bool, Guid>(true, c.Id))
.SingleOrDefaultAsync();

public Task<(VerifiedCredentialTypeKindId CredentialTypeKindId, JsonDocument Schema)> GetCredentialStorageInformationById(Guid credentialId) =>
_dbContext.CompanySsiDetails
dbContext.CompanySsiDetails
.Where(c => c.Id == credentialId)
.Select(c => new ValueTuple<VerifiedCredentialTypeKindId, JsonDocument>(c.CompanySsiProcessData!.CredentialTypeKindId, c.CompanySsiProcessData.Schema))
.SingleOrDefaultAsync();

public Task<(Guid? ExternalCredentialId, VerifiedCredentialTypeKindId KindId, bool HasEncryptionInformation, string? CallbackUrl)> GetExternalCredentialAndKindId(Guid credentialId) =>
_dbContext.CompanySsiDetails
dbContext.CompanySsiDetails
.Where(c => c.Id == credentialId)
.Select(c => new ValueTuple<Guid?, VerifiedCredentialTypeKindId, bool, string?>(
c.ExternalCredentialId,
Expand All @@ -73,13 +66,13 @@ public CredentialRepository(IssuerDbContext dbContext)
.SingleOrDefaultAsync();

public Task<(string Bpn, string? CallbackUrl)> GetCallbackUrl(Guid credentialId) =>
_dbContext.CompanySsiProcessData
dbContext.CompanySsiProcessData
.Where(x => x.CompanySsiDetailId == credentialId)
.Select(x => new ValueTuple<string, string?>(x.CompanySsiDetail!.Bpnl, x.CallbackUrl))
.SingleOrDefaultAsync();

public Task<(bool Exists, bool IsSameBpnl, Guid? ExternalCredentialId, CompanySsiDetailStatusId StatusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> Documents)> GetRevocationDataById(Guid credentialId, string bpnl) =>
_dbContext.CompanySsiDetails
dbContext.CompanySsiDetails
.Where(x =>
x.Id == credentialId)
.Select(x => new ValueTuple<bool, bool, Guid?, CompanySsiDetailStatusId, IEnumerable<(Guid, DocumentStatusId)>>(
Expand All @@ -94,22 +87,34 @@ public void AttachAndModifyCredential(Guid credentialId, Action<CompanySsiDetail
{
var entity = new CompanySsiDetail(credentialId, string.Empty, default!, default!, null!, null!, default!);
initialize?.Invoke(entity);
_dbContext.CompanySsiDetails.Attach(entity);
dbContext.CompanySsiDetails.Attach(entity);
modify(entity);
}

public Task<(VerifiedCredentialTypeId TypeId, string RequesterId)> GetCredentialNotificationData(Guid credentialId) =>
_dbContext.CompanySsiDetails
dbContext.CompanySsiDetails
.Where(x => x.Id == credentialId)
.Select(x => new ValueTuple<VerifiedCredentialTypeId, string>(x.VerifiedCredentialTypeId, x.CreatorUserId))
.SingleOrDefaultAsync();

public Task<(bool Exists, bool IsSameCompany, IEnumerable<(DocumentStatusId StatusId, byte[] Content)> Documents)> GetSignedCredentialForCredentialId(Guid credentialId, string bpnl) =>
_dbContext.CompanySsiDetails
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();

public Task<(bool Exists, bool IsSameCompany, string FileName, DocumentStatusId StatusId, byte[] Content, MediaTypeId MediaTypeId)> GetDocumentById(Guid documentId, string bpnl) =>
dbContext.Documents
.Where(x => x.Id == documentId)
.Select(x => new ValueTuple<bool, bool, string, DocumentStatusId, byte[], MediaTypeId>(
true,
x.CompanySsiDetails.Any(c => c.Bpnl == bpnl),
x.DocumentName,
x.DocumentStatusId,
x.DocumentContent,
x.MediaTypeId))
.SingleOrDefaultAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ public interface ICompanySsiDetailsRepository
/// </summary>
/// <param name="companySsiDetailStatusId">The status of the details</param>
/// <param name="credentialTypeId">OPTIONAL: The type of the credential that should be returned</param>
/// <param name="approvalType">OPTIONAL: The approval type of the credential</param>
/// <returns>Returns data to create the pagination</returns>
IQueryable<CompanySsiDetail> GetAllCredentialDetails(CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId);
IQueryable<CompanySsiDetail> GetAllCredentialDetails(CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailApprovalType? approvalType);

/// <summary>
/// Gets all credentials for a specific bpn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ public interface ICredentialRepository
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);
Task<(bool Exists, bool IsSameCompany, string FileName, DocumentStatusId StatusId, byte[] Content, MediaTypeId MediaTypeId)> GetDocumentById(Guid documentId, string bpnl);
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken)
_logger.LogInformation("Start BaseEntityBatch Seeder");
await SeedBaseEntity(cancellationToken);
await SeedTable<CompanySsiProcessData>("company_ssi_process_datas", x => x.CompanySsiDetailId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
await SeedTable<CompanySsiDetailAssignedDocument>("company_ssi_detail_assigned_documents", x => new { x.DocumentId, x.CompanySsiDetailId }, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
await SeedTable<VerifiedCredentialTypeAssignedKind>("verified_credential_type_assigned_kinds", x => new { x.VerifiedCredentialTypeId, x.VerifiedCredentialTypeKindId }, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
await SeedTable<VerifiedCredentialTypeAssignedUseCase>("verified_credential_type_assigned_use_cases", x => new { x.VerifiedCredentialTypeId, x.UseCaseId }, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
await SeedTable<VerifiedCredentialTypeAssignedExternalType>("verified_credential_type_assigned_external_types", x => new { x.VerifiedCredentialTypeId, x.VerifiedCredentialExternalTypeId }, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling;
using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess;
using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Extensions;
using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.ErrorHandling;
using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity;
using System.Text.Json;
Expand Down Expand Up @@ -59,4 +61,25 @@ public async Task<JsonDocument> GetCredentialDocument(Guid credentialId)
using var stream = new MemoryStream(credentialContent);
return await JsonDocument.ParseAsync(stream).ConfigureAwait(ConfigureAwaitOptions.None);
}

public async Task<(string FileName, byte[] Content, string MediaType)> GetCredentialDocumentById(Guid documentId)
{
var (exists, isSameCompany, fileName, documentStatusId, content, mediaTypeId) = await _repositories.GetInstance<ICredentialRepository>().GetDocumentById(documentId, _identityData.Bpnl).ConfigureAwait(ConfigureAwaitOptions.None);
if (!exists)
{
throw NotFoundException.Create(CredentialErrors.DOCUMENT_NOT_FOUND, new[] { new ErrorParameter("documentId", documentId.ToString()) });
}

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

if (documentStatusId == DocumentStatusId.INACTIVE)
{
throw ConflictException.Create(CredentialErrors.DOCUMENT_INACTIVE, new[] { new ErrorParameter("documentId", documentId.ToString()) });
}

return (fileName, content, mediaTypeId.MapToMediaType());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic;
public interface ICredentialBusinessLogic
{
Task<JsonDocument> GetCredentialDocument(Guid credentialId);
Task<(string FileName, byte[] Content, string MediaType)> GetCredentialDocumentById(Guid documentId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public interface IIssuerBusinessLogic

IAsyncEnumerable<CertificateParticipationData> GetSsiCertificatesAsync();

Task<Pagination.Response<CredentialDetailData>> GetCredentials(int page, int size, CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailSorting? sorting);
Task<Pagination.Response<CredentialDetailData>> GetCredentials(int page, int size, CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailApprovalType? approvalType, CompanySsiDetailSorting? sorting);
IAsyncEnumerable<OwnedVerifiedCredentialData> GetCredentialsForBpn();

Task ApproveCredential(Guid credentialId, CancellationToken cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ public IAsyncEnumerable<CertificateParticipationData> GetSsiCertificatesAsync()
.GetSsiCertificates(_identity.Bpnl, _dateTimeProvider.OffsetNow);

/// <inheritdoc />
public Task<Pagination.Response<CredentialDetailData>> GetCredentials(int page, int size, CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailSorting? sorting)
public Task<Pagination.Response<CredentialDetailData>> GetCredentials(int page, int size, CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailApprovalType? approvalType, CompanySsiDetailSorting? sorting)
{
var query = _repositories
.GetInstance<ICompanySsiDetailsRepository>()
.GetAllCredentialDetails(companySsiDetailStatusId, credentialTypeId);
.GetAllCredentialDetails(companySsiDetailStatusId, credentialTypeId, approvalType);
var sortedQuery = sorting switch
{
CompanySsiDetailSorting.BpnlAsc or null => query.OrderBy(c => c.Bpnl),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ public static RouteGroupBuilder MapCredentialApi(this RouteGroupBuilder group)
.Produces(StatusCodes.Status200OK, typeof(JsonDocument), Constants.JsonContentType)
.Produces(StatusCodes.Status409Conflict, typeof(ErrorResponse), Constants.JsonContentType);

issuer.MapGet("/documents/{documentId}", async ([FromRoute] Guid documentId, [FromServices] ICredentialBusinessLogic logic) =>
{
var (fileName, content, mediaType) = await logic.GetCredentialDocumentById(documentId);
return Results.File(content, contentType: mediaType, fileDownloadName: fileName);
})
.WithSwaggerDescription("The endpoint enables users to download the credential (full json) of their own company.",
"Example: GET: api/credential/documents/{documentId}")
.RequireAuthorization(r =>
{
r.RequireRole("view_credential_requests");
r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity));
r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn));
})
.WithDefaultResponses()
.Produces(StatusCodes.Status200OK, typeof(FileContentResult), Constants.JsonContentType)
.Produces(StatusCodes.Status409Conflict, typeof(ErrorResponse), Constants.JsonContentType);

return issuer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ public static RouteGroupBuilder MapIssuerApi(this RouteGroupBuilder group)
[FromQuery] int? size,
[FromQuery] CompanySsiDetailStatusId? companySsiDetailStatusId,
[FromQuery] VerifiedCredentialTypeId? credentialTypeId,
[FromQuery] CompanySsiDetailApprovalType? approvalType,
[FromQuery] CompanySsiDetailSorting? sorting) => logic.GetCredentials(page ?? 0, size ?? 15,
companySsiDetailStatusId, credentialTypeId, sorting))
companySsiDetailStatusId, credentialTypeId, approvalType, sorting))
.WithSwaggerDescription("Gets all outstanding, existing and inactive credentials",
"Example: GET: /api/issuer",
"The page to get",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ public class CredentialErrorMessageContainer : IErrorMessageContainer
{ CredentialErrors.SCHEMA_NOT_FRAMEWORK, "The schema must be a framework credential" },
{ CredentialErrors.CREDENTIAL_NOT_FOUND, "Credential {credentialId} does not exist" },
{ CredentialErrors.COMPANY_NOT_ALLOWED, "Not allowed to display the credential" },
{ CredentialErrors.SIGNED_CREDENTIAL_NOT_FOUND, "There must be exactly one signed credential" }
{ CredentialErrors.SIGNED_CREDENTIAL_NOT_FOUND, "There must be exactly one signed credential" },
{ CredentialErrors.DOCUMENT_NOT_FOUND, "Document {documentId} does not exist" },
{ CredentialErrors.DOCUMENT_INACTIVE, "Document {documentId} is inactive" },
{ CredentialErrors.DOCUMENT_OTHER_COMPANY, "Not allowed to access document of another company" }
}.ToImmutableDictionary(x => (int)x.Key, x => x.Value);

public Type Type { get => typeof(CredentialErrors); }
Expand Down Expand Up @@ -100,5 +103,8 @@ public enum CredentialErrors
SCHEMA_NOT_FRAMEWORK,
CREDENTIAL_NOT_FOUND,
COMPANY_NOT_ALLOWED,
SIGNED_CREDENTIAL_NOT_FOUND
SIGNED_CREDENTIAL_NOT_FOUND,
DOCUMENT_NOT_FOUND,
DOCUMENT_INACTIVE,
DOCUMENT_OTHER_COMPANY
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,12 @@ private void InitializeClaims(ClaimsPrincipal principal)
}

_identityDataBuilder.AddIdentityId(preferredUserName ?? clientId!);
bool isCompanyUser;
if (isCompanyUser = Guid.TryParse(preferredUserName, out var companyUserId))
var isCompanyUser = Guid.TryParse(preferredUserName, out var companyUserId);
if (isCompanyUser)
{
_identityDataBuilder.AddCompanyUserId(companyUserId);
}

_identityDataBuilder.AddIsServiceAccount(!isCompanyUser);
_identityDataBuilder.Status = IClaimsIdentityDataBuilderStatus.Complete;
}
Expand Down
Loading

0 comments on commit ea5d91a

Please sign in to comment.