From ea5d91a30b18d70c0bcc46555141db6762f6af56 Mon Sep 17 00:00:00 2001 From: Phil Schneider Date: Thu, 23 May 2024 10:08:23 +0200 Subject: [PATCH] feat(issuer): add filter to /api/issuer (#120) * add optional filter for approval type to endpoitn GET /api/issuer * add Get document endpoint ---------------------- Refs: #111 #27 Reviewed-By: Evelyn Gurschler --- .../Models/CompanySsiApprovalType.cs | 38 ++++++ .../CompanySsiDetailsRepository.cs | 5 +- .../Repositories/CredentialRepository.cs | 41 ++++--- .../ICompanySsiDetailsRepository.cs | 3 +- .../Repositories/ICredentialRepository.cs | 1 + .../Seeder/BatchInsertSeeder.cs | 1 + .../BusinessLogic/CredentialBusinessLogic.cs | 23 ++++ .../BusinessLogic/ICredentialBusinessLogic.cs | 1 + .../BusinessLogic/IIssuerBusinessLogic.cs | 2 +- .../BusinessLogic/IssuerBusinessLogic.cs | 4 +- .../Controllers/CredentialController.cs | 17 +++ .../Controllers/IssuerController.cs | 3 +- .../CredentialErrorMessageContainer.cs | 10 +- .../Identity/MandatoryIdentityClaimHandler.cs | 5 +- .../CompanySsiDetailsRepositoryTests.cs | 6 +- .../CredentialRepositoryTests.cs | 113 ++++++++++++++++++ ...ny_ssi_detail_assigned_documents.test.json | 6 + .../Data/company_ssi_process_datas.test.json | 3 +- .../CredentialBusinessLogicTests.cs | 73 +++++++++++ .../Controllers/IssuerControllerTests.cs | 9 +- 20 files changed, 324 insertions(+), 40 deletions(-) create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/CompanySsiApprovalType.cs create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_detail_assigned_documents.test.json diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/CompanySsiApprovalType.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/CompanySsiApprovalType.cs new file mode 100644 index 00000000..ec988c96 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/CompanySsiApprovalType.cs @@ -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; + +/// +/// Possible filter options for the pagination +/// +public enum CompanySsiDetailApprovalType +{ + /// + /// The credential request will be automatically approved + /// + Automatic = 1, + + /// + /// The operator needs to approve the credential request + /// + Manual = 2, +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs index e526c526..d3c5b297 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs @@ -169,11 +169,12 @@ public Task CheckSsiDetailsExistsForCompany(string bpnl, VerifiedCredentia .SingleOrDefaultAsync(); /// - public IQueryable GetAllCredentialDetails(CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId) => + public IQueryable 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))); /// public IAsyncEnumerable GetOwnCredentialDetails(string bpnl) => diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs index 7fca89d2..29b3dd16 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs @@ -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 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( new HolderWalletData(x.CompanySsiProcessData!.HolderWalletUrl, x.CompanySsiProcessData.ClientId), @@ -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(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(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( c.ExternalCredentialId, @@ -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(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>( @@ -94,22 +87,34 @@ public void AttachAndModifyCredential(Guid credentialId, Action GetCredentialNotificationData(Guid credentialId) => - _dbContext.CompanySsiDetails + dbContext.CompanySsiDetails .Where(x => x.Id == credentialId) .Select(x => new ValueTuple(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>>( true, x.Bpnl == bpnl, x.Documents.Where(d => d.DocumentTypeId == DocumentTypeId.VERIFIED_CREDENTIAL).Select(d => new ValueTuple(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( + true, + x.CompanySsiDetails.Any(c => c.Bpnl == bpnl), + x.DocumentName, + x.DocumentStatusId, + x.DocumentContent, + x.MediaTypeId)) + .SingleOrDefaultAsync(); } diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs index 8d6ee8a4..6d418935 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs @@ -85,8 +85,9 @@ public interface ICompanySsiDetailsRepository /// /// The status of the details /// OPTIONAL: The type of the credential that should be returned + /// OPTIONAL: The approval type of the credential /// Returns data to create the pagination - IQueryable GetAllCredentialDetails(CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId); + IQueryable GetAllCredentialDetails(CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailApprovalType? approvalType); /// /// Gets all credentials for a specific bpn diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs index b1c0236e..4ea53609 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs @@ -36,4 +36,5 @@ public interface ICredentialRepository void AttachAndModifyCredential(Guid credentialId, Action? initialize, Action 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); } diff --git a/src/database/SsiCredentialIssuer.Migrations/Seeder/BatchInsertSeeder.cs b/src/database/SsiCredentialIssuer.Migrations/Seeder/BatchInsertSeeder.cs index dc841bbb..5204f3b5 100644 --- a/src/database/SsiCredentialIssuer.Migrations/Seeder/BatchInsertSeeder.cs +++ b/src/database/SsiCredentialIssuer.Migrations/Seeder/BatchInsertSeeder.cs @@ -63,6 +63,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) _logger.LogInformation("Start BaseEntityBatch Seeder"); await SeedBaseEntity(cancellationToken); await SeedTable("company_ssi_process_datas", x => x.CompanySsiDetailId, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + await SeedTable("company_ssi_detail_assigned_documents", x => new { x.DocumentId, x.CompanySsiDetailId }, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); await SeedTable("verified_credential_type_assigned_kinds", x => new { x.VerifiedCredentialTypeId, x.VerifiedCredentialTypeKindId }, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); await SeedTable("verified_credential_type_assigned_use_cases", x => new { x.VerifiedCredentialTypeId, x.UseCaseId }, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); await SeedTable("verified_credential_type_assigned_external_types", x => new { x.VerifiedCredentialTypeId, x.VerifiedCredentialExternalTypeId }, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialBusinessLogic.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialBusinessLogic.cs index cc43ebae..7e991bdc 100644 --- a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialBusinessLogic.cs +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialBusinessLogic.cs @@ -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; @@ -59,4 +61,25 @@ public async Task 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().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()); + } } diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/ICredentialBusinessLogic.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/ICredentialBusinessLogic.cs index 5d937718..d4d368bd 100644 --- a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/ICredentialBusinessLogic.cs +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/ICredentialBusinessLogic.cs @@ -24,4 +24,5 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic; public interface ICredentialBusinessLogic { Task GetCredentialDocument(Guid credentialId); + Task<(string FileName, byte[] Content, string MediaType)> GetCredentialDocumentById(Guid documentId); } diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IIssuerBusinessLogic.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IIssuerBusinessLogic.cs index 18420bd0..c430d257 100644 --- a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IIssuerBusinessLogic.cs +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IIssuerBusinessLogic.cs @@ -30,7 +30,7 @@ public interface IIssuerBusinessLogic IAsyncEnumerable GetSsiCertificatesAsync(); - Task> GetCredentials(int page, int size, CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailSorting? sorting); + Task> GetCredentials(int page, int size, CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailApprovalType? approvalType, CompanySsiDetailSorting? sorting); IAsyncEnumerable GetCredentialsForBpn(); Task ApproveCredential(Guid credentialId, CancellationToken cancellationToken); diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs index 5602418a..6ebf58c3 100644 --- a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs @@ -96,11 +96,11 @@ public IAsyncEnumerable GetSsiCertificatesAsync() .GetSsiCertificates(_identity.Bpnl, _dateTimeProvider.OffsetNow); /// - public Task> GetCredentials(int page, int size, CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailSorting? sorting) + public Task> GetCredentials(int page, int size, CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailApprovalType? approvalType, CompanySsiDetailSorting? sorting) { var query = _repositories .GetInstance() - .GetAllCredentialDetails(companySsiDetailStatusId, credentialTypeId); + .GetAllCredentialDetails(companySsiDetailStatusId, credentialTypeId, approvalType); var sortedQuery = sorting switch { CompanySsiDetailSorting.BpnlAsc or null => query.OrderBy(c => c.Bpnl), diff --git a/src/issuer/SsiCredentialIssuer.Service/Controllers/CredentialController.cs b/src/issuer/SsiCredentialIssuer.Service/Controllers/CredentialController.cs index 2474365d..c1e34af0 100644 --- a/src/issuer/SsiCredentialIssuer.Service/Controllers/CredentialController.cs +++ b/src/issuer/SsiCredentialIssuer.Service/Controllers/CredentialController.cs @@ -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; } } diff --git a/src/issuer/SsiCredentialIssuer.Service/Controllers/IssuerController.cs b/src/issuer/SsiCredentialIssuer.Service/Controllers/IssuerController.cs index 2e65a66a..49caa9fa 100644 --- a/src/issuer/SsiCredentialIssuer.Service/Controllers/IssuerController.cs +++ b/src/issuer/SsiCredentialIssuer.Service/Controllers/IssuerController.cs @@ -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", diff --git a/src/issuer/SsiCredentialIssuer.Service/ErrorHandling/CredentialErrorMessageContainer.cs b/src/issuer/SsiCredentialIssuer.Service/ErrorHandling/CredentialErrorMessageContainer.cs index ce4dcfe9..7f603ceb 100644 --- a/src/issuer/SsiCredentialIssuer.Service/ErrorHandling/CredentialErrorMessageContainer.cs +++ b/src/issuer/SsiCredentialIssuer.Service/ErrorHandling/CredentialErrorMessageContainer.cs @@ -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); } @@ -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 } diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/MandatoryIdentityClaimHandler.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/MandatoryIdentityClaimHandler.cs index 2bc6efdc..cdf9569b 100644 --- a/src/issuer/SsiCredentialIssuer.Service/Identity/MandatoryIdentityClaimHandler.cs +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/MandatoryIdentityClaimHandler.cs @@ -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; } diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/CompanySsiDetailsRepositoryTests.cs b/tests/database/SsiCredentialIssuer.DbAccess.Tests/CompanySsiDetailsRepositoryTests.cs index 4e07b019..5b57223f 100644 --- a/tests/database/SsiCredentialIssuer.DbAccess.Tests/CompanySsiDetailsRepositoryTests.cs +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/CompanySsiDetailsRepositoryTests.cs @@ -113,7 +113,7 @@ public async Task GetAllCredentialDetails_WithValidData_ReturnsExpected() var sut = await CreateSut(); // Act - var result = await sut.GetAllCredentialDetails(null, null).ToListAsync(); + var result = await sut.GetAllCredentialDetails(null, null, null).ToListAsync(); // Assert result.Should().NotBeNull(); @@ -138,7 +138,7 @@ public async Task GetAllCredentialDetails_WithWithStatusId_ReturnsExpected() var sut = await CreateSut(); // Act - var result = await sut.GetAllCredentialDetails(CompanySsiDetailStatusId.PENDING, null).ToListAsync(); + var result = await sut.GetAllCredentialDetails(CompanySsiDetailStatusId.PENDING, null, null).ToListAsync(); // Assert result.Should().NotBeNull().And.HaveCount(4); @@ -159,7 +159,7 @@ public async Task GetAllCredentialDetails_WithWithCredentialType_ReturnsExpected var sut = await CreateSut(); // Act - var result = await sut.GetAllCredentialDetails(null, VerifiedCredentialTypeId.PCF_FRAMEWORK).ToListAsync(); + var result = await sut.GetAllCredentialDetails(null, VerifiedCredentialTypeId.PCF_FRAMEWORK, null).ToListAsync(); // Assert result.Should().NotBeNull().And.ContainSingle().Which.Bpnl.Should().Be(ValidBpnl); diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/CredentialRepositoryTests.cs b/tests/database/SsiCredentialIssuer.DbAccess.Tests/CredentialRepositoryTests.cs index 6d80e5a4..5e377452 100644 --- a/tests/database/SsiCredentialIssuer.DbAccess.Tests/CredentialRepositoryTests.cs +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/CredentialRepositoryTests.cs @@ -138,6 +138,71 @@ public async Task GetExternalCredentialAndKindId_ReturnsExpectedDocument() #endregion + #region GetCallbackUrl + + [Fact] + public async Task GetCallbackUrl_ReturnsExpectedDocument() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetCallbackUrl(new Guid("9f5b9934-4014-4099-91e9-7b1aee696b03")); + + // Assert + result.Bpn.Should().Be("BPNL00000003AYRE"); + result.CallbackUrl.Should().Be("https://example.org/callback"); + } + + #endregion + + #region GetRevocationDataById + + [Fact] + public async Task GetRevocationDataById_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetRevocationDataById(new Guid("9f5b9934-4014-4099-91e9-7b1aee696b03"), "BPNL00000003AYRE"); + + // Assert + result.Exists.Should().BeTrue(); + result.IsSameBpnl.Should().BeTrue(); + result.StatusId.Should().Be(CompanySsiDetailStatusId.PENDING); + result.ExternalCredentialId.Should().Be(new Guid("bd474c60-e7ce-450f-bdf4-73604546fc5e")); + } + + [Fact] + public async Task GetRevocationDataById_WithoutExisting_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetRevocationDataById(Guid.NewGuid(), "BPNL00000003AYRE"); + + // Assert + result.Exists.Should().BeFalse(); + } + + [Fact] + public async Task GetRevocationDataById_WithNotMatchingBpnl_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetRevocationDataById(new Guid("9f5b9934-4014-4099-91e9-7b1aee696b03"), "BPNL000000WRONG"); + + // Assert + result.Exists.Should().BeTrue(); + result.IsSameBpnl.Should().BeFalse(); + } + + #endregion + #region AttachAndModifyCredential [Fact] @@ -161,6 +226,54 @@ public async Task AttachAndModifyCredential_ReturnsExpectedResult() #endregion + #region GetDocumentById + + [Fact] + public async Task GetDocumentById_ReturnsExpectedDocument() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetDocumentById(new Guid("e020787d-1e04-4c0b-9c06-bd1cd44724b1"), "BPNL00000003AYRE"); + + // Assert + result.Exists.Should().BeTrue(); + result.IsSameCompany.Should().BeTrue(); + result.MediaTypeId.Should().Be(MediaTypeId.PNG); + } + + [Fact] + public async Task GetDocumentById_WithWrongBpn_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetDocumentById(new Guid("e020787d-1e04-4c0b-9c06-bd1cd44724b1"), "BPNL0000000WRONG"); + + // Assert + result.Exists.Should().BeTrue(); + result.IsSameCompany.Should().BeFalse(); + result.MediaTypeId.Should().Be(MediaTypeId.PNG); + } + + [Fact] + public async Task GetDocumentById_WithNotExisting_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetDocumentById(Guid.NewGuid(), "BPNL00000003AYRE"); + + // Assert + result.Exists.Should().BeFalse(); + result.IsSameCompany.Should().BeFalse(); + } + + #endregion + #region Setup private async Task CreateSut() diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_detail_assigned_documents.test.json b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_detail_assigned_documents.test.json new file mode 100644 index 00000000..37abc42f --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_detail_assigned_documents.test.json @@ -0,0 +1,6 @@ +[ + { + "document_id": "e020787d-1e04-4c0b-9c06-bd1cd44724b1", + "company_ssi_detail_id": "9f5b9934-4014-4099-91e9-7b1aee696b03" + } +] diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_process_datas.test.json b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_process_datas.test.json index 6921b95b..9f32b87e 100644 --- a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_process_datas.test.json +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_process_datas.test.json @@ -7,6 +7,7 @@ "credential_type_kind_id": 1, "encryption_mode": 1, "holder_wallet_url": "https://example.org/wallet", - "client_id": "c123" + "client_id": "c123", + "callback_url": "https://example.org/callback" } ] \ No newline at end of file diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/CredentialBusinessLogicTests.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/CredentialBusinessLogicTests.cs index f140e1a9..e79bcb54 100644 --- a/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/CredentialBusinessLogicTests.cs +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/CredentialBusinessLogicTests.cs @@ -19,6 +19,7 @@ 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.BusinessLogic; @@ -26,6 +27,7 @@ using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; using System.Text; using System.Text.Json; +using System.Text.Unicode; namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.BusinessLogic; @@ -33,6 +35,7 @@ public class CredentialBusinessLogicTests { private static readonly string Bpnl = "BPNL00000001TEST"; private readonly Guid CredentialId = Guid.NewGuid(); + private readonly Guid DocumentId = Guid.NewGuid(); private readonly IFixture _fixture; @@ -64,6 +67,8 @@ public CredentialBusinessLogicTests() _sut = new CredentialBusinessLogic(_issuerRepositories, _identityService); } + #region GetCredentialDocument + [Fact] public async Task GetCredentialDocument_WithNotExisting_ThrowsNotFoundException() { @@ -124,4 +129,72 @@ public async Task GetCredentialDocument_WithValid_ReturnsExpected() // Assert doc.RootElement.GetRawText().Should().Be("{\"test\":\"test\"}"); } + + #endregion + + #region GetCredentialDocumentById + + [Fact] + public async Task GetCredentialDocumentById_WithNotExisting_ThrowsNotFoundException() + { + // Arrange + A.CallTo(() => _credentialRepository.GetDocumentById(DocumentId, Bpnl)) + .Returns(default((bool, bool, string, DocumentStatusId, byte[], MediaTypeId))); + async Task Act() => await _sut.GetCredentialDocumentById(DocumentId); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be(CredentialErrors.DOCUMENT_NOT_FOUND.ToString()); + } + + [Fact] + public async Task GetCredentialDocumentById_WithDifferentCompany_ThrowsForbiddenException() + { + // Arrange + A.CallTo(() => _credentialRepository.GetDocumentById(DocumentId, Bpnl)) + .Returns((true, false, string.Empty, DocumentStatusId.ACTIVE, null!, MediaTypeId.JSON)); + async Task Act() => await _sut.GetCredentialDocumentById(DocumentId); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be(CredentialErrors.DOCUMENT_OTHER_COMPANY.ToString()); + } + + [Fact] + public async Task GetCredentialDocumentById_WithoutInactiveDocument_ThrowsConflictException() + { + // Arrange + A.CallTo(() => _credentialRepository.GetDocumentById(DocumentId, Bpnl)) + .Returns((true, true, string.Empty, DocumentStatusId.INACTIVE, null!, MediaTypeId.JSON)); + async Task Act() => await _sut.GetCredentialDocumentById(DocumentId); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be(CredentialErrors.DOCUMENT_INACTIVE.ToString()); + } + + [Fact] + public async Task GetCredentialDocumentById_WithValid_ReturnsExpected() + { + // Arrange + var json = JsonDocument.Parse("{\"test\":\"test\"}"); + var schema = JsonSerializer.Serialize(json, JsonSerializerOptions.Default); + A.CallTo(() => _credentialRepository.GetDocumentById(DocumentId, Bpnl)) + .Returns((true, true, "test.json", DocumentStatusId.ACTIVE, Encoding.UTF8.GetBytes(schema), MediaTypeId.JSON)); + + // Act + var doc = await _sut.GetCredentialDocumentById(DocumentId); + + // Assert + doc.MediaType.Should().Be(MediaTypeId.JSON.MapToMediaType()); + doc.FileName.Should().Be("test.json"); + } + + #endregion } diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/Controllers/IssuerControllerTests.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/Controllers/IssuerControllerTests.cs index 6ddd8436..ffdcc5b1 100644 --- a/tests/issuer/SsiCredentialIssuer.Service.Tests/Controllers/IssuerControllerTests.cs +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/Controllers/IssuerControllerTests.cs @@ -26,7 +26,7 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Controllers; -public class IssuerControllerTests : IClassFixture +public class IssuerControllerTests(IntegrationTestFactory factory) : IClassFixture { private static readonly JsonSerializerOptions JsonOptions = new() { @@ -36,12 +36,7 @@ public class IssuerControllerTests : IClassFixture }; private const string BaseUrl = "/api/issuer"; - private readonly HttpClient _client; - - public IssuerControllerTests(IntegrationTestFactory factory) - { - _client = factory.CreateClient(); - } + private readonly HttpClient _client = factory.CreateClient(); #region GetCertificateTypes