diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6c7aaf31ee..542f515b6a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,7 +14,7 @@ Link to Github issue. Please delete options that are not relevant. -- [ ] I have followed the [contributing guidelines](https://github.com/eclipse-tractusx/portal-assets/blob/main/developer/Technical%20Documentation/Dev%20Process/How%20to%20contribute.md#commit-and-pr-guidelines) +- [ ] I have followed the [contributing guidelines](https://github.com/eclipse-tractusx/portal-assets/blob/main/docs/developer/Technical%20Documentation/Dev%20Process/How%20to%20contribute.md#commit-and-pr-guidelines) - [ ] I have performed [IP checks](https://eclipse-tractusx.github.io/docs/release/trg-7/trg-7-04#checking-libraries-using-the-eclipse-dash-license-tool) for added or updated 3rd party libraries - [ ] I have created and linked IP issues or requested their creation by a committer - [ ] I have performed a self-review of my own code diff --git a/CHANGELOG.md b/CHANGELOG.md index 543288e1aa..b4691cbe09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,29 +2,69 @@ New features, fixed bugs, known defects and other noteworthy changes to each release of the Catena-X Portal Backend. +## 1.8.0-RC4 + +### Change +* updated response body of the GET: /api/administration/user/owncompany/users endpoint by changing the "role" section to an array to include role client information ![Tag](https://img.shields.io/static/v1?label=&message=BreakingChange&color=yellow&style=flat) +* pattern harmonization of 'company name' insert endpoints + * POST: api/administration/invitation + * POST: api/administration/registration/network/partnerRegistration + * POST: api/registration/application/{applicationId}/companyDetailsWithAddress + * PUT: /api/apps/appreleaseprocess/{appID} + * POST: /api/apps/appreleaseprocess/createapp +* pattern harmonization of 'company name' search endpoints + * GET: api/administration/registration/applicationsWithStatus + * GET: api/administration/registration/applications +* adjusted business logic of post/put BPN endpoints to allow the post/put of lowercase BPNs and ensure the transition to all uppercase by the backend logic (connector controller, registration controller, user controller) + +### Feature +* Administration Service + * API endpoints for user account creation backend logic updated to set the providerID (unique username on the IdP which holds the user identity) is getting stored inside the portal db + * POST: /api/administration/identityprovider/owncompany/usersfile + * POST: /api/administration/registration/network/{externalId}/partnerRegistration + * POST: /api/administration/invitation + * POST: /api/administration/user/owncompany/users + * POST: /api/administration/user/owncompany/identityprovider/{identityProviderId}/users + * POST: /api/administration/user/owncompany/identityprovider/{identityProviderId}/usersfile + * POST: /api/administration/user/owncompany/usersfile + * POST: /api/registration/application/{applicationId}/inviteNewUser + * added additional user identity provider attributes (such as idpDisplayName and providerID) for all GET user account data + * GET: /api/administration/user/owncompany/users?page=0&size=5 + * GET: /api/administration/user/owncompany/users/{userId} + * GET: /api/administration/user/ownUser + +### Technical Support +* fixed sonar cloud finding to use correct pagination params + +### Bugfix +* changed claimTypes static class of clientId claim to client_id + +### Known Knowns +n/a + ## 1.8.0-RC3 ### Change -- External Interface Details - - BPDM interface refactored - bpdm push process was updated to support the new interface spec of the bpdm gate service - - Clearinghouse interface updated - possible generated clearinghouse service error content is getting saved inside the application comment level -- Email Template "cx_admin_invitation" enhanced by adding the section and link of the decline url (portal-frontend implementation) +* External Interface Details + * BPDM interface refactored - bpdm push process was updated to support the new interface spec of the bpdm gate service + * Clearinghouse interface updated - possible generated clearinghouse service error content is getting saved inside the application comment level +* Email Template "cx_admin_invitation" enhanced by adding the section and link of the decline url (portal-frontend implementation) ### Feature -- Onboarding Service Provider Function - - enabled deactivation of managed idps (administration service) via the existing idp status update endpoint - - enabled deletion of managed idps (administration service) via the existing idp delete endpoint - - added new endpoint to enable customer to decline their own company application which was created by an osp +* Onboarding Service Provider Function + * enabled deactivation of managed idps (administration service) via the existing idp status update endpoint + * enabled deletion of managed idps (administration service) via the existing idp delete endpoint + * added new endpoint to enable customer to decline their own company application which was created by an osp ### Technical Support -- Release workflow updated by adding additional image tag of type semver -- Upgraded external packages with security vulnerabilities +* Release workflow updated by adding additional image tag of type semver +* Upgraded external packages with security vulnerabilities ### Bugfix -- Endpoint authorization on valid companyId added for - - POST: /api/apps/appreleaseprocess/consent/{appId}/agreementConsents - - POST: /api/services/servicerelease/consent/{serviceId}/agreementConsents -- Adjusted endpoint GET: api/administration/serviceaccount/owncompany/serviceaccounts to filter for active service accounts by default if no parameter is submitted +* Endpoint authorization on valid companyId added for + * POST: /api/apps/appreleaseprocess/consent/{appId}/agreementConsents + * POST: /api/services/servicerelease/consent/{serviceId}/agreementConsents +* Adjusted endpoint GET: api/administration/serviceaccount/owncompany/serviceaccounts to filter for active service accounts by default if no parameter is submitted ## 1.8.0-RC2 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 1a3e96b3ff..6aedd1f21c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,6 +20,6 @@ 1.8.0 - RC3 + RC4 diff --git a/src/administration/Administration.Service/BusinessLogic/ConnectorsBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/ConnectorsBusinessLogic.cs index 21bafbde04..236d7a3e6e 100644 --- a/src/administration/Administration.Service/BusinessLogic/ConnectorsBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/ConnectorsBusinessLogic.cs @@ -343,7 +343,7 @@ public IAsyncEnumerable GetCompanyConnectorEndPointAsync( } return _portalRepositories.GetInstance() - .GetConnectorEndPointDataAsync(bpns) + .GetConnectorEndPointDataAsync(bpns.Select(x => x.ToUpper())) .PreSortedGroupBy(data => data.BusinessPartnerNumber) .Select(group => new ConnectorEndPointData( diff --git a/src/administration/Administration.Service/BusinessLogic/IUserBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/IUserBusinessLogic.cs index 4e264eaabe..17b8904bb9 100644 --- a/src/administration/Administration.Service/BusinessLogic/IUserBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/IUserBusinessLogic.cs @@ -33,7 +33,7 @@ public interface IUserBusinessLogic Task> GetOwnCompanyUserDatasAsync(int page, int size, GetOwnCompanyUsersFilter filter); [Obsolete("to be replaced by UserRolesBusinessLogic.GetAppRolesAsync. Remove as soon frontend is adjusted")] IAsyncEnumerable GetClientRolesAsync(Guid appId, string? languageShortName = null); - Task GetOwnCompanyUserDetailsAsync(Guid userId); + Task GetOwnCompanyUserDetailsAsync(Guid userId); Task AddOwnCompanyUsersBusinessPartnerNumbersAsync(Guid userId, IEnumerable businessPartnerNumbers); Task AddOwnCompanyUsersBusinessPartnerNumberAsync(Guid userId, string businessPartnerNumber); Task GetOwnUserDetails(); diff --git a/src/administration/Administration.Service/BusinessLogic/IdentityProviderBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/IdentityProviderBusinessLogic.cs index 4c1d72a10a..f5f6d03592 100644 --- a/src/administration/Administration.Service/BusinessLogic/IdentityProviderBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/IdentityProviderBusinessLogic.cs @@ -380,7 +380,6 @@ public async ValueTask DeleteCompanyIdentityProviderAsync(Guid identityProviderI } identityProviderRepository.DeleteIdentityProvider(identityProviderId); - await _portalRepositories.SaveAsync().ConfigureAwait(false); } @@ -743,7 +742,7 @@ private async ValueTask UploadOwnCompanyUsersIdenti { var userRepository = _portalRepositories.GetInstance(); var companyId = _identityData.CompanyId; - var (sharedIdpAlias, existingAliase) = await GetCompanyAliasDataAsync(companyId).ConfigureAwait(false); + var (sharedIdp, existingAliase) = await GetCompanyAliasDataAsync(companyId).ConfigureAwait(false); using var stream = document.OpenReadStream(); @@ -755,8 +754,8 @@ private async ValueTask UploadOwnCompanyUsersIdenti { numIdps = ParseCSVFirstLineReturningNumIdps(line); }, - line => ParseCSVLine(line, numIdps, existingAliase), - lines => ProcessOwnCompanyUsersIdentityProviderLinkDataInternalAsync(lines, userRepository, companyId, sharedIdpAlias, cancellationToken), + line => ParseCSVLine(line, numIdps, existingAliase.Select(x => x.Alias)), + lines => ProcessOwnCompanyUsersIdentityProviderLinkDataInternalAsync(lines, userRepository, companyId, sharedIdp, existingAliase, cancellationToken), cancellationToken ).ConfigureAwait(false); @@ -782,7 +781,8 @@ private UserUpdateError CreateUserUpdateError(int line, Exception error) => IAsyncEnumerable<(Guid CompanyUserId, UserProfile UserProfile, IEnumerable IdentityProviderLinks)> userProfileLinkDatas, IUserRepository userRepository, Guid companyId, - string? sharedIdpAlias, + (Guid IdentityProviderId, string Alias) sharedIdp, + IEnumerable<(Guid IdentityProviderId, string Alias)> existingIdps, [EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var (companyUserId, profile, identityProviderLinks) in userProfileLinkDatas) @@ -799,12 +799,12 @@ private UserUpdateError CreateUserUpdateError(int line, Exception error) => foreach (var identityProviderLink in identityProviderLinks) { - updated |= await UpdateIdentityProviderLinksAsync(iamUserId, companyUserId, identityProviderLink, existingLinks, sharedIdpAlias).ConfigureAwait(false); + updated |= await UpdateIdentityProviderLinksAsync(iamUserId, companyUserId, identityProviderLink, existingLinks, sharedIdp, existingIdps).ConfigureAwait(false); } if (existingProfile != profile) { - await UpdateUserProfileAsync(userRepository, iamUserId, companyUserId, profile, existingLinks, sharedIdpAlias).ConfigureAwait(false); + await UpdateUserProfileAsync(userRepository, iamUserId, companyUserId, profile, existingLinks, sharedIdp).ConfigureAwait(false); updated = true; } success = updated; @@ -821,15 +821,15 @@ private UserUpdateError CreateUserUpdateError(int line, Exception error) => } } - private async ValueTask<(string? SharedIdpAlias, IEnumerable ValidAliase)> GetCompanyAliasDataAsync(Guid companyId) + private async ValueTask<((Guid IdentityProviderId, string Alias) SharedIdp, IEnumerable<(Guid IdentityProviderId, string Alias)> ValidAliase)> GetCompanyAliasDataAsync(Guid companyId) { var identityProviderCategoryData = await _portalRepositories.GetInstance() .GetCompanyIdentityProviderCategoryDataUntracked(companyId) .Where(data => data.Alias != null) - .Select(data => (data.TypeId, Alias: data.Alias!)) + .Select(data => (data.IdentityProviderId, data.TypeId, Alias: data.Alias!)) .ToListAsync().ConfigureAwait(false); - var sharedIdpAlias = identityProviderCategoryData.SingleOrDefault(data => data.TypeId == IdentityProviderTypeId.SHARED).Alias; - var validAliase = identityProviderCategoryData.Select(data => data.Alias).ToList(); + var sharedIdpAlias = identityProviderCategoryData.Where(data => data.TypeId == IdentityProviderTypeId.SHARED).Select(data => (data.IdentityProviderId, data.Alias)).SingleOrDefault(); + var validAliase = identityProviderCategoryData.Select(data => (data.IdentityProviderId, data.Alias)).ToList(); return (sharedIdpAlias, validAliase); } @@ -851,7 +851,13 @@ private UserUpdateError CreateUserUpdateError(int line, Exception error) => ); } - private async ValueTask UpdateIdentityProviderLinksAsync(string iamUserId, Guid companyUserId, IdentityProviderLink identityProviderLink, IEnumerable existingLinks, string? sharedIdpAlias) + private async ValueTask UpdateIdentityProviderLinksAsync( + string iamUserId, + Guid companyUserId, + IdentityProviderLink identityProviderLink, + IEnumerable existingLinks, + (Guid IdentityProviderId, string Alias) sharedIdp, + IEnumerable<(Guid IdentityProviderId, string Alias)> existingIdps) { var (alias, userId, userName) = identityProviderLink; @@ -863,7 +869,7 @@ private async ValueTask UpdateIdentityProviderLinksAsync(string iamUserId, return false; } - if (alias == sharedIdpAlias) + if (alias == sharedIdp.Alias) { throw new ControllerArgumentException($"unexpected update of shared identityProviderLink, alias '{alias}', companyUser '{companyUserId}', providerUserId: '{userId}', providerUserName: '{userName}'"); } @@ -873,23 +879,49 @@ private async ValueTask UpdateIdentityProviderLinksAsync(string iamUserId, await _provisioningManager.DeleteProviderUserLinkToCentralUserAsync(iamUserId, alias).ConfigureAwait(false); } await _provisioningManager.AddProviderUserLinkToCentralUserAsync(iamUserId, identityProviderLink).ConfigureAwait(false); + await InsertUpdateCompanyUserAssignedIdentityProvider(companyUserId, existingIdps.Single(x => x.Alias == alias).IdentityProviderId, identityProviderLink).ConfigureAwait(false); return true; } - private async ValueTask UpdateUserProfileAsync(IUserRepository userRepository, string iamUserId, Guid companyUserId, UserProfile profile, IEnumerable existingLinks, string? sharedIdpAlias) + private async Task InsertUpdateCompanyUserAssignedIdentityProvider(Guid companyUserId, Guid identityProviderId, IdentityProviderLink providerLink) + { + var userRepository = _portalRepositories.GetInstance(); + var data = await userRepository.GetCompanyUserAssignedIdentityProvider(companyUserId, identityProviderId).ConfigureAwait(false); + if (data == default) + { + userRepository.AddCompanyUserAssignedIdentityProvider(companyUserId, identityProviderId, providerLink.UserId, providerLink.UserName); + } + else + { + userRepository.AttachAndModifyUserAssignedIdentityProvider(companyUserId, identityProviderId, + uaip => + { + uaip.ProviderId = data.ProviderId; + uaip.UserName = data.Username; + }, + uaip => + { + uaip.ProviderId = providerLink.UserId; + uaip.UserName = providerLink.UserName; + }); + } + } + + private async ValueTask UpdateUserProfileAsync(IUserRepository userRepository, string iamUserId, Guid companyUserId, UserProfile profile, IEnumerable existingLinks, (Guid IdentityProviderId, string Alias) sharedIdp) { var (firstName, lastName, email) = (profile.FirstName ?? "", profile.LastName ?? "", profile.Email ?? ""); await _provisioningManager.UpdateCentralUserAsync(iamUserId, firstName, lastName, email).ConfigureAwait(false); - if (sharedIdpAlias != null) + if (sharedIdp != default) { - var sharedIdpLink = existingLinks.FirstOrDefault(link => link.Alias == sharedIdpAlias); + var sharedIdpLink = existingLinks.FirstOrDefault(link => link.Alias == sharedIdp.Alias); if (sharedIdpLink != default) { - await _provisioningManager.UpdateSharedRealmUserAsync(sharedIdpAlias, sharedIdpLink.UserId, firstName, lastName, email).ConfigureAwait(false); + await _provisioningManager.UpdateSharedRealmUserAsync(sharedIdp.Alias, sharedIdpLink.UserId, firstName, lastName, email).ConfigureAwait(false); } } + userRepository.AttachAndModifyCompanyUser(companyUserId, null, companyUser => { companyUser.Firstname = profile.FirstName; diff --git a/src/administration/Administration.Service/BusinessLogic/InvitationBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/InvitationBusinessLogic.cs index 1e4f2c7ccf..2db4192c97 100644 --- a/src/administration/Administration.Service/BusinessLogic/InvitationBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/InvitationBusinessLogic.cs @@ -21,6 +21,7 @@ using Microsoft.Extensions.Options; using Org.Eclipse.TractusX.Portal.Backend.Administration.Service.Models; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models; using Org.Eclipse.TractusX.Portal.Backend.Mailing.SendMail; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; @@ -29,11 +30,13 @@ using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Models; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Service; +using System.Text.RegularExpressions; namespace Org.Eclipse.TractusX.Portal.Backend.Administration.Service.BusinessLogic; public class InvitationBusinessLogic : IInvitationBusinessLogic { + private static readonly Regex Company = new(ValidationExpressions.Company, RegexOptions.Compiled, TimeSpan.FromSeconds(1)); private readonly IProvisioningManager _provisioningManager; private readonly IUserProvisioningService _userProvisioningService; private readonly IPortalRepositories _portalRepositories; @@ -72,6 +75,10 @@ public Task ExecuteInvitation(CompanyInvitationData invitationData) { throw new ControllerArgumentException("organisationName must not be empty", "organisationName"); } + if (!string.IsNullOrEmpty(invitationData.organisationName) && !Company.IsMatch(invitationData.organisationName)) + { + throw new ControllerArgumentException("OrganisationName length must be 3-40 characters and *+=#%\\s not used as one of the first three characters in the Organisation name", "organisationName"); + } return ExecuteInvitationInternalAsync(invitationData); } diff --git a/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs index 1f3bc23156..0d8b899652 100644 --- a/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs @@ -41,6 +41,7 @@ public class NetworkBusinessLogic : INetworkBusinessLogic { private static readonly Regex Name = new(ValidationExpressions.Name, RegexOptions.Compiled, TimeSpan.FromSeconds(1)); private static readonly Regex ExternalID = new("^[A-Za-z0-9\\-+_/,.]{6,36}$", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); + private static readonly Regex Company = new(ValidationExpressions.Company, RegexOptions.Compiled, TimeSpan.FromSeconds(1)); private readonly IPortalRepositories _portalRepositories; private readonly IIdentityData _identityData; @@ -59,6 +60,10 @@ public NetworkBusinessLogic(IPortalRepositories portalRepositories, IIdentitySer public async Task HandlePartnerRegistration(PartnerRegistrationData data) { + if (!string.IsNullOrEmpty(data.Name) && !Company.IsMatch(data.Name)) + { + throw new ControllerArgumentException("OrganisationName length must be 3-40 characters and *+=#%\\s not used as one of the first three characters in the Organisation name", "organisationName"); + } var ownerCompanyId = _identityData.CompanyId; var networkRepository = _portalRepositories.GetInstance(); var companyRepository = _portalRepositories.GetInstance(); diff --git a/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs index 127a01af8d..f8ba83d385 100644 --- a/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs @@ -43,7 +43,9 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Administration.Service.BusinessLog public sealed class RegistrationBusinessLogic : IRegistrationBusinessLogic { - private static readonly Regex BpnRegex = new(@"(\w|\d){16}", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); + private static readonly Regex BpnRegex = new(ValidationExpressions.Bpn, RegexOptions.Compiled, TimeSpan.FromSeconds(1)); + + private static readonly Regex Company = new(ValidationExpressions.Company, RegexOptions.Compiled, TimeSpan.FromSeconds(1)); private readonly IPortalRepositories _portalRepositories; private readonly RegistrationSettings _settings; @@ -91,6 +93,10 @@ private async Task GetCompanyWithAddressAsyncInternal(Gu { throw NotFoundException.Create(AdministrationRegistrationErrors.APPLICATION_NOT_FOUND, new ErrorParameter[] { new("applicationId", applicationId.ToString()) }); } + if (!string.IsNullOrEmpty(companyWithAddress.Name) && !Company.IsMatch(companyWithAddress.Name)) + { + throw new ControllerArgumentException("OrganisationName length must be 3-40 characters and *+=#%\\s not used as one of the first three characters in the Organisation name", "organisationName"); + } return new CompanyWithAddressData( companyWithAddress.CompanyId, @@ -123,6 +129,10 @@ private async Task GetCompanyWithAddressAsyncInternal(Gu public Task> GetCompanyApplicationDetailsAsync(int page, int size, CompanyApplicationStatusFilter? companyApplicationStatusFilter = null, string? companyName = null) { + if (!string.IsNullOrEmpty(companyName) && !Company.IsMatch(companyName)) + { + throw new ControllerArgumentException("CompanyName length must be 3-40 characters and *+=#%\\s not used as one of the first three characters in the company name", "companyName"); + } var applications = _portalRepositories.GetInstance() .GetCompanyApplicationsFilteredQuery( companyName?.Length >= 3 ? companyName : null, @@ -161,6 +171,10 @@ private async Task GetCompanyWithAddressAsyncInternal(Gu public Task> GetAllCompanyApplicationsDetailsAsync(int page, int size, string? companyName = null) { + if (!string.IsNullOrEmpty(companyName) && !Company.IsMatch(companyName)) + { + throw new ControllerArgumentException("CompanyName length must be 3-40 characters and *+=#%\\s not used as one of the first three characters in the company name", "companyName"); + } var applications = _portalRepositories.GetInstance().GetAllCompanyApplicationsDetailsQuery(companyName); return Pagination.CreateResponseAsync( @@ -215,7 +229,7 @@ public Task UpdateCompanyBpn(Guid applicationId, string bpn) private async Task UpdateCompanyBpnInternal(Guid applicationId, string bpn) { var result = await _portalRepositories.GetInstance() - .GetBpnForIamUserUntrackedAsync(applicationId, bpn).ToListAsync().ConfigureAwait(false); + .GetBpnForIamUserUntrackedAsync(applicationId, bpn.ToUpper()).ToListAsync().ConfigureAwait(false); if (!result.Exists(item => item.IsApplicationCompany)) { throw new NotFoundException($"application {applicationId} not found"); @@ -262,7 +276,7 @@ private async Task UpdateCompanyBpnInternal(Guid applicationId, string bpn) .ConfigureAwait(false); _portalRepositories.GetInstance().AttachAndModifyCompany(applicationCompanyData.CompanyId, null, - c => { c.BusinessPartnerNumber = bpn; }); + c => { c.BusinessPartnerNumber = bpn.ToUpper(); }); var registrationValidationFailed = context.Checklist[ApplicationChecklistEntryTypeId.REGISTRATION_VERIFICATION] == new ValueTuple(ApplicationChecklistEntryStatusId.FAILED, null); @@ -290,7 +304,7 @@ private async Task UpdateCompanyBpnInternal(Guid applicationId, string bpn) public async Task ProcessClearinghouseResponseAsync(ClearinghouseResponseData data, CancellationToken cancellationToken) { _logger.LogInformation("Process SelfDescription called with the following data {Data}", data); - var result = await _portalRepositories.GetInstance().GetSubmittedApplicationIdsByBpn(data.BusinessPartnerNumber).ToListAsync(cancellationToken).ConfigureAwait(false); + var result = await _portalRepositories.GetInstance().GetSubmittedApplicationIdsByBpn(data.BusinessPartnerNumber.ToUpper()).ToListAsync(cancellationToken).ConfigureAwait(false); if (!result.Any()) { throw new NotFoundException($"No companyApplication for BPN {data.BusinessPartnerNumber} is not in status SUBMITTED"); diff --git a/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs index ca36ccdf4e..fd1a04f1d8 100644 --- a/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/UserBusinessLogic.cs @@ -17,9 +17,9 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Org.Eclipse.TractusX.Portal.Backend.Administration.Service.Models; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Async; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Framework.Models; using Org.Eclipse.TractusX.Portal.Backend.Mailing.SendMail; @@ -32,6 +32,7 @@ using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Models; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Service; +using System.Text.RegularExpressions; namespace Org.Eclipse.TractusX.Portal.Backend.Administration.Service.BusinessLogic; @@ -40,6 +41,7 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Administration.Service.BusinessLog /// public class UserBusinessLogic : IUserBusinessLogic { + private static readonly Regex BpnRegex = new(ValidationExpressions.Bpn, RegexOptions.Compiled, TimeSpan.FromSeconds(1)); private readonly IProvisioningManager _provisioningManager; private readonly IUserProvisioningService _userProvisioningService; private readonly IProvisioningDBAccess _provisioningDbAccess; @@ -196,7 +198,7 @@ public async Task CreateOwnCompanyIdpUserAsync(Guid identityProviderId, Us throw result.Error; } - var mailParameters = new Dictionary() + var mailParameters = new Dictionary { { "companyName", displayName }, { "nameCreatedBy", nameCreatedBy }, @@ -220,40 +222,56 @@ public async Task CreateOwnCompanyIdpUserAsync(Guid identityProviderId, Us { _logger.LogError(e, "Error sending email to {Email} after creating user {UserName}", userCreationInfo.Email, userCreationInfo.UserName); } + return result.CompanyUserId; } public Task> GetOwnCompanyUserDatasAsync(int page, int size, GetOwnCompanyUsersFilter filter) { - var companyUsers = _portalRepositories.GetInstance().GetOwnCompanyUserQuery( - _identityData.CompanyId, - filter.CompanyUserId, - filter.FirstName, - filter.LastName, - filter.Email, - _settings.CompanyUserStatusIds - ); - return Pagination.CreateResponseAsync( + async Task?> GetCompanyUserData(int skip, int take) + { + var companyData = await _portalRepositories.GetInstance().GetOwnCompanyUserData( + _identityData.CompanyId, + filter.CompanyUserId, + filter.FirstName, + filter.LastName, + filter.Email, + _settings.CompanyUserStatusIds + )(skip, take).ConfigureAwait(false); + + if (companyData == null) + return null; + + var displayNames = await companyData.Data + .SelectMany(x => x.IdpUserIds) + .Select(x => x.Alias ?? throw new ConflictException("Alias must not be null")) + .Distinct() + .ToImmutableDictionaryAsync(GetDisplayName).ConfigureAwait(false); + + return new Pagination.Source( + companyData.Count, + companyData.Data.Select(d => new CompanyUserData( + d.CompanyUserId, + d.UserStatusId, + d.FirstName, + d.LastName, + d.Email, + d.Roles, + d.IdpUserIds.Select(x => + new IdpUserId( + displayNames[x.Alias!], + x.Alias!, + x.UserId))))); + } + return Pagination.CreateResponseAsync( page, size, _settings.ApplicationsMaxPageSize, - (int skip, int take) => new Pagination.AsyncSource( - companyUsers.CountAsync(), - companyUsers.OrderByDescending(companyUser => companyUser.Identity!.DateCreated) - .Skip(skip) - .Take(take) - .Select(companyUser => new CompanyUserData( - companyUser.Id, - companyUser.Identity!.UserStatusId, - companyUser.Identity!.IdentityAssignedRoles.Select(x => x.UserRole!).Select(userRole => userRole.UserRoleText)) - { - FirstName = companyUser.Firstname, - LastName = companyUser.Lastname, - Email = companyUser.Email - }) - .AsAsyncEnumerable())); + GetCompanyUserData); } + private async Task GetDisplayName(string alias) => await _provisioningManager.GetIdentityProviderDisplayName(alias).ConfigureAwait(false) ?? throw new ConflictException($"Display Name should not be null for alias: {alias}"); + [Obsolete("to be replaced by UserRolesBusinessLogic.GetAppRolesAsync. Remove as soon frontend is adjusted")] public async IAsyncEnumerable GetClientRolesAsync(Guid appId, string? languageShortName = null) { @@ -274,7 +292,7 @@ public async IAsyncEnumerable GetClientRolesAsync(Guid appId, strin } } - public async Task GetOwnCompanyUserDetailsAsync(Guid userId) + public async Task GetOwnCompanyUserDetailsAsync(Guid userId) { var companyId = _identityData.CompanyId; var details = await _portalRepositories.GetInstance().GetOwnCompanyUserDetailsUntrackedAsync(userId, companyId).ConfigureAwait(false); @@ -282,27 +300,41 @@ public async Task GetOwnCompanyUserDetailsAsync(Guid userId) { throw new NotFoundException($"no company-user data found for user {userId} in company {companyId}"); } - return details; + + return new CompanyUserDetailData( + details.CompanyUserId, + details.CreatedAt, + details.BusinessPartnerNumbers, + details.CompanyName, + details.UserStatusId, + details.AssignedRoles, + await Task.WhenAll(details.IdpUserIds.Select(async x => + new IdpUserId( + await GetDisplayName(x.Alias ?? throw new ConflictException("Alias must not be null")).ConfigureAwait(false), + x.Alias, + x.UserId))).ConfigureAwait(false)); } public async Task AddOwnCompanyUsersBusinessPartnerNumbersAsync(Guid userId, IEnumerable businessPartnerNumbers) { - if (businessPartnerNumbers.Any(businessPartnerNumber => businessPartnerNumber.Length > 20)) + if (businessPartnerNumbers.Any(bpn => !BpnRegex.IsMatch(bpn))) { - throw new ControllerArgumentException("businessPartnerNumbers must not exceed 20 characters", nameof(businessPartnerNumbers)); + throw new ControllerArgumentException("BPN must contain exactly 16 characters and must be prefixed with BPNL", nameof(businessPartnerNumbers)); } + var companyId = _identityData.CompanyId; var (assignedBusinessPartnerNumbers, isValidUser) = await _portalRepositories.GetInstance().GetOwnCompanyUserWithAssignedBusinessPartnerNumbersUntrackedAsync(userId, companyId).ConfigureAwait(false); if (!isValidUser) { throw new NotFoundException($"user {userId} not found in company {companyId}"); } + var iamUserId = await _provisioningManager.GetUserByUserName(userId.ToString()).ConfigureAwait(false) ?? throw new ConflictException("user {userId} not found in keycloak"); var businessPartnerRepository = _portalRepositories.GetInstance(); await _provisioningManager.AddBpnAttributetoUserAsync(iamUserId, businessPartnerNumbers).ConfigureAwait(false); foreach (var businessPartnerToAdd in businessPartnerNumbers.Except(assignedBusinessPartnerNumbers)) { - businessPartnerRepository.CreateCompanyUserAssignedBusinessPartner(userId, businessPartnerToAdd); + businessPartnerRepository.CreateCompanyUserAssignedBusinessPartner(userId, businessPartnerToAdd.ToUpper()); } return await _portalRepositories.SaveAsync(); @@ -321,7 +353,20 @@ public async Task GetOwnUserDetails() { throw new NotFoundException($"no company-user data found for user {userId}"); } - return details; + + return new CompanyOwnUserDetails( + details.CompanyUserId, + details.CreatedAt, + details.BusinessPartnerNumbers, + details.CompanyName, + details.UserStatusId, + details.AssignedRoles, + details.AdminDetails, + await Task.WhenAll(details.IdpUserIds.Select(async x => + new IdpUserId( + await GetDisplayName(x.Alias ?? throw new ConflictException("Alias must not be null")).ConfigureAwait(false), + x.Alias, + x.UserId))).ConfigureAwait(false)); } public async Task UpdateOwnUserDetails(Guid companyUserId, OwnCompanyUserEditableDetails ownCompanyUserEditableDetails) @@ -331,14 +376,15 @@ public async Task UpdateOwnUserDetails(Guid companyUserId, O { throw new ForbiddenException($"invalid userId {companyUserId} for user {userId}"); } + var userRepository = _portalRepositories.GetInstance(); var userData = await userRepository.GetUserWithCompanyIdpAsync(companyUserId).ConfigureAwait(false); if (userData == null) { throw new ArgumentOutOfRangeException($"user {companyUserId} is not a shared idp user"); } - var companyUser = userData.CompanyUser; + var companyUser = userData.CompanyUser; var iamUserId = await _provisioningManager.GetUserByUserName(companyUserId.ToString()).ConfigureAwait(false) ?? throw new ConflictException($"user {companyUserId} not found in keycloak"); var iamIdpAlias = userData.IamIdpAlias; var userIdShared = await _provisioningManager.GetProviderUserIdForCentralUserIdAsync(iamIdpAlias, iamUserId).ConfigureAwait(false); @@ -346,6 +392,7 @@ public async Task UpdateOwnUserDetails(Guid companyUserId, O { throw new NotFoundException($"no shared realm userid found for {iamUserId} in realm {iamIdpAlias}"); } + await _provisioningManager.UpdateSharedRealmUserAsync( iamIdpAlias, userIdShared, @@ -469,6 +516,7 @@ private async Task DeleteIamUserAsync(string? sharedIdpAlias, string iamUserId) await _provisioningManager.DeleteSharedRealmUserAsync(sharedIdpAlias, userIdShared).ConfigureAwait(false); } } + await _provisioningManager.DeleteCentralRealmUserAsync(iamUserId).ConfigureAwait(false); } @@ -495,6 +543,7 @@ private async Task CanResetPassword(Guid userId) await _provisioningDbAccess.SaveAsync().ConfigureAwait(false); return true; } + return false; } @@ -509,8 +558,10 @@ public async Task ExecuteOwnCompanyUserPasswordReset(Guid companyUserId) await _provisioningManager.ResetSharedUserPasswordAsync(alias, iamUserId).ConfigureAwait(false); return true; } + throw new ArgumentException($"cannot reset password more often than {_settings.PasswordReset.MaxNoOfReset} in {_settings.PasswordReset.NoOfHours} hours"); } + throw new NotFoundException($"Cannot identify companyId or shared idp : userId {companyUserId} is not associated with admin users company {_identityData.CompanyId}"); } @@ -530,7 +581,7 @@ public async Task DeleteOwnUserBusinessPartnerNumbersAsync(Guid userId, str { var userBusinessPartnerRepository = _portalRepositories.GetInstance(); - var (isValidUser, isAssignedBusinessPartner, isSameCompany) = await userBusinessPartnerRepository.GetOwnCompanyUserWithAssignedBusinessPartnerNumbersAsync(userId, _identityData.CompanyId, businessPartnerNumber).ConfigureAwait(false); + var (isValidUser, isAssignedBusinessPartner, isSameCompany) = await userBusinessPartnerRepository.GetOwnCompanyUserWithAssignedBusinessPartnerNumbersAsync(userId, _identityData.CompanyId, businessPartnerNumber.ToUpper()).ConfigureAwait(false); if (!isValidUser) { @@ -549,9 +600,9 @@ public async Task DeleteOwnUserBusinessPartnerNumbersAsync(Guid userId, str var iamUserId = await _provisioningManager.GetUserByUserName(userId.ToString()).ConfigureAwait(false) ?? throw new ConflictException($"user {userId} is not associated with a user in keycloak"); - userBusinessPartnerRepository.DeleteCompanyUserAssignedBusinessPartner(userId, businessPartnerNumber); + userBusinessPartnerRepository.DeleteCompanyUserAssignedBusinessPartner(userId, businessPartnerNumber.ToUpper()); - await _provisioningManager.DeleteCentralUserBusinessPartnerNumberAsync(iamUserId, businessPartnerNumber).ConfigureAwait(false); + await _provisioningManager.DeleteCentralUserBusinessPartnerNumberAsync(iamUserId, businessPartnerNumber.ToUpper()).ConfigureAwait(false); return await _portalRepositories.SaveAsync().ConfigureAwait(false); } diff --git a/src/administration/Administration.Service/Controllers/RegistrationController.cs b/src/administration/Administration.Service/Controllers/RegistrationController.cs index 0a5ba9d0e2..c207cbc016 100644 --- a/src/administration/Administration.Service/Controllers/RegistrationController.cs +++ b/src/administration/Administration.Service/Controllers/RegistrationController.cs @@ -154,7 +154,7 @@ public async Task ApproveApplication([FromRoute] Guid applicati /// Comment to explain why the application got declined /// cancellation token /// - /// Example: POST: api/administration/registration/application/4f0146c6-32aa-4bb1-b844-df7e8babdcb4/decline + /// Example: POST: api/administration/registration/application/{applicationId}/decline /// /// Successfully declined the application /// Either the CompanyApplication is not in status SUBMITTED, or there is no checklist entry of type Registration_Verification. diff --git a/src/administration/Administration.Service/Controllers/UserController.cs b/src/administration/Administration.Service/Controllers/UserController.cs index 3bfdbec02f..97fd04db41 100644 --- a/src/administration/Administration.Service/Controllers/UserController.cs +++ b/src/administration/Administration.Service/Controllers/UserController.cs @@ -196,9 +196,9 @@ public ValueTask UploadOwnCompanyUsersIdentityProviderFileAsy [Authorize(Roles = "view_user_management")] [Authorize(Policy = PolicyTypes.ValidCompany)] [Route("owncompany/users/{companyUserId}", Name = nameof(GetOwnCompanyUserDetails))] - [ProducesResponseType(typeof(CompanyUserDetails), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(CompanyUserDetailData), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] - public Task GetOwnCompanyUserDetails([FromRoute] Guid companyUserId) => + public Task GetOwnCompanyUserDetails([FromRoute] Guid companyUserId) => _logic.GetOwnCompanyUserDetailsAsync(companyUserId); /// @@ -249,7 +249,7 @@ public Task> ModifyAppUserRolesAsync([FromRoute] Gui /// Id of the user to add the business partner numbers to. /// the business partner numbers that should be added. /// - /// Example: POST: api/administration/user/owncompany/users/ac1cf001-7fbc-1f2f-817f-bce0575a0011/businessPartnerNumbers + /// Example: POST: api/administration/user/owncompany/users/{companyUserId}/businessPartnerNumbers /// The business partner numbers have been added successfully. /// Business Partner Numbers must not exceed 20 characters. /// User not found. @@ -269,7 +269,7 @@ public Task AddOwnCompanyUserBusinessPartnerNumbers(Guid companyUserId, IEn /// Id of the user to add the business partner numbers to. /// the business partner number that should be added. /// - /// Example: PUT: api/administration/user/owncompany/users/ac1cf001-7fbc-1f2f-817f-bce0575a0011/businessPartnerNumbers/CAXSDUMMYCATENAZZ + /// Example: PUT: api/administration/user/owncompany/users/{companyUserId}/businessPartnerNumbers/{businessPartnerNumber} /// The business partner number have been added successfully. /// Business Partner Numbers must not exceed 20 characters. /// User is not existing. diff --git a/src/framework/Framework.Async/ToImmutableDictionaryAsyncExtension.cs b/src/framework/Framework.Async/ToImmutableDictionaryAsyncExtension.cs new file mode 100644 index 0000000000..29918c8809 --- /dev/null +++ b/src/framework/Framework.Async/ToImmutableDictionaryAsyncExtension.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (c) 2021, 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.Collections.Immutable; + +namespace Org.Eclipse.TractusX.Portal.Backend.Framework.Async; + +public static class ToImmutableDictionaryAsyncExtension +{ + public static async Task> ToImmutableDictionaryAsync(this IEnumerable keys, Func> selector) where K : notnull + { + var builder = ImmutableDictionary.CreateBuilder(); + builder.AddRange( + await Task.WhenAll( + keys.Select(async key => new KeyValuePair( + key, + await selector(key).ConfigureAwait(false)))).ConfigureAwait(false)); + return builder.ToImmutableDictionary(); + } +} diff --git a/src/framework/Framework.Models/PortalClaimTypes.cs b/src/framework/Framework.Models/PortalClaimTypes.cs index a27cc9bcd0..4e59b296c1 100644 --- a/src/framework/Framework.Models/PortalClaimTypes.cs +++ b/src/framework/Framework.Models/PortalClaimTypes.cs @@ -22,7 +22,7 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.Models; public static class PortalClaimTypes { public const string Sub = "sub"; - public const string ClientId = "clientId"; + public const string ClientId = "client_id"; public const string PreferredUserName = "preferred_username"; public const string ResourceAccess = "resource_access"; } diff --git a/src/framework/Framework.Models/ValidationExpressions.cs b/src/framework/Framework.Models/ValidationExpressions.cs index 94d446ded3..3efc662680 100644 --- a/src/framework/Framework.Models/ValidationExpressions.cs +++ b/src/framework/Framework.Models/ValidationExpressions.cs @@ -24,5 +24,5 @@ public static class ValidationExpressions { public const string Name = @"^.+$"; // TODO: should be @"^(([A-Za-zÀ-ÿ]{1,40}?([-,.'\s]?[A-Za-zÀ-ÿ]{1,40}?)){1,8})$"; public const string Bpn = @"^(BPNL|bpnl)[\w|\d]{12}$"; - public const string Company = @"^\d*?[A-Za-zÀ-ÿ]\d?([A-Za-z0-9À-ÿ-_+=.,:;!?'\x22&#@()]\s?){3,80}$"; + public const string Company = @"^\d*?[A-Za-zÀ-ÿ]\d?([A-Za-z0-9À-ÿ-_+=.,:;!?'\x22&#@()]\s?){2,40}$"; } diff --git a/src/marketplace/Apps.Service/BusinessLogic/AppReleaseBusinessLogic.cs b/src/marketplace/Apps.Service/BusinessLogic/AppReleaseBusinessLogic.cs index d09eb66fe9..75907ed8e1 100644 --- a/src/marketplace/Apps.Service/BusinessLogic/AppReleaseBusinessLogic.cs +++ b/src/marketplace/Apps.Service/BusinessLogic/AppReleaseBusinessLogic.cs @@ -32,6 +32,7 @@ using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Identities; +using System.Text.RegularExpressions; namespace Org.Eclipse.TractusX.Portal.Backend.Apps.Service.BusinessLogic; @@ -40,6 +41,7 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Apps.Service.BusinessLogic; /// public class AppReleaseBusinessLogic : IAppReleaseBusinessLogic { + private static readonly Regex Company = new(ValidationExpressions.Company, RegexOptions.Compiled, TimeSpan.FromSeconds(1)); private readonly IPortalRepositories _portalRepositories; private readonly AppsSettings _settings; private readonly IOfferService _offerService; @@ -188,6 +190,11 @@ public Task AddAppAsync(AppRequestModel appRequestModel) throw new ControllerArgumentException("Use Case Ids must not be null or empty", nameof(appRequestModel.UseCaseIds)); } + if (!string.IsNullOrEmpty(appRequestModel.Provider) && !Company.IsMatch(appRequestModel.Provider)) + { + throw new ControllerArgumentException("Provider length must be 3-40 characters and *+=#%\\s not used as one of the first three characters in the Organisation name", nameof(appRequestModel.Provider)); + } + return this.CreateAppAsync(appRequestModel); } @@ -263,6 +270,11 @@ public async Task UpdateAppReleaseAsync(Guid appId, AppRequestModel appRequestMo throw new ForbiddenException($"Company {companyId} is not the app provider."); } + if (!string.IsNullOrEmpty(appRequestModel.Provider) && !Company.IsMatch(appRequestModel.Provider)) + { + throw new ControllerArgumentException("Provider length must be 3-40 characters and *+=#%\\s not used as one of the first three characters in the Organisation name", nameof(appRequestModel.Provider)); + } + if (appRequestModel.SalesManagerId.HasValue) { await _offerService.ValidateSalesManager(appRequestModel.SalesManagerId.Value, _settings.SalesManagerRoles).ConfigureAwait(false); diff --git a/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserData.cs b/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserData.cs index d45b5e2241..683d65c9d3 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserData.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserData.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -23,30 +22,38 @@ namespace Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; -public class CompanyUserData -{ - public CompanyUserData(Guid companyUserId, UserStatusId userStatusId, IEnumerable roles) - { - CompanyUserId = companyUserId; - UserStatusId = userStatusId; - Roles = roles; - } - - [JsonPropertyName("companyUserId")] - public Guid CompanyUserId { get; set; } - - [JsonPropertyName("status")] - public UserStatusId UserStatusId { get; set; } - - [JsonPropertyName("firstName")] - public string? FirstName { get; set; } - - [JsonPropertyName("lastName")] - public string? LastName { get; set; } - - [JsonPropertyName("email")] - public string? Email { get; set; } - - [JsonPropertyName("roles")] - public IEnumerable Roles { get; set; } -} +public record CompanyUserData( + [property: JsonPropertyName("companyUserId")] + Guid CompanyUserId, + [property: JsonPropertyName("status")] + UserStatusId UserStatusId, + [property: JsonPropertyName("firstName")] + string? FirstName, + [property: JsonPropertyName("lastName")] + string? LastName, + [property: JsonPropertyName("email")] + string? Email, + [property: JsonPropertyName("roles")] + IEnumerable Roles, + [property: JsonPropertyName("idpUserIds")] + IEnumerable IdpUserIds +); + +public record CompanyUserTransferData( + [property: JsonPropertyName("companyUserId")] + Guid CompanyUserId, + [property: JsonPropertyName("status")] + UserStatusId UserStatusId, + [property: JsonPropertyName("dateCreated")] + DateTimeOffset DateCreated, + [property: JsonPropertyName("firstName")] + string? FirstName, + [property: JsonPropertyName("lastName")] + string? LastName, + [property: JsonPropertyName("email")] + string? Email, + [property: JsonPropertyName("roles")] + IEnumerable Roles, + [property: JsonPropertyName("idpUserIds")] + IEnumerable IdpUserIds +); diff --git a/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserDetails.cs b/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserDetails.cs index cbd1cea74e..8266746cea 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserDetails.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserDetails.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -41,6 +40,25 @@ public record CompanyUserDetails( public string? Email { get; set; } } +public record CompanyUserDetailData( + [property: JsonPropertyName("companyUserId")] Guid CompanyUserId, + [property: JsonPropertyName("created")] DateTimeOffset CreatedAt, + [property: JsonPropertyName("bpn")] IEnumerable BusinessPartnerNumbers, + [property: JsonPropertyName("company")] string CompanyName, + [property: JsonPropertyName("status")] UserStatusId UserStatusId, + [property: JsonPropertyName("assignedRoles")] IEnumerable AssignedRoles, + [property: JsonPropertyName("idpUserIds")] IEnumerable IdpUserIds) +{ + [JsonPropertyName("firstName")] + public string? FirstName { get; set; } + + [JsonPropertyName("lastName")] + public string? LastName { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } +} + public record CompanyOwnUserDetails( [property: JsonPropertyName("companyUserId")] Guid CompanyUserId, [property: JsonPropertyName("created")] DateTimeOffset CreatedAt, @@ -48,7 +66,8 @@ public record CompanyOwnUserDetails( [property: JsonPropertyName("company")] string CompanyName, [property: JsonPropertyName("status")] UserStatusId UserStatusId, [property: JsonPropertyName("assignedRoles")] IEnumerable AssignedRoles, - [property: JsonPropertyName("admin")] IEnumerable AdminDetails) + [property: JsonPropertyName("admin")] IEnumerable AdminDetails, + [property: JsonPropertyName("idpUserIds")] IEnumerable IdpUserIds) { [JsonPropertyName("firstName")] public string? FirstName { get; set; } @@ -67,3 +86,51 @@ public record CompanyUserAdminDetails( public record CompanyUserAssignedRoleDetails( [property: JsonPropertyName("appId")] Guid OfferId, [property: JsonPropertyName("roles")] IEnumerable UserRoles); + +public record IdpUserId( + [property: JsonPropertyName("idpDisplayName")] string IdpDisplayName, + [property: JsonPropertyName("idpAlias")] string IdpAlias, + [property: JsonPropertyName("userId")] string UserId); + +public record CompanyOwnUserTransferDetails( + [property: JsonPropertyName("companyUserId")] Guid CompanyUserId, + [property: JsonPropertyName("created")] DateTimeOffset CreatedAt, + [property: JsonPropertyName("bpn")] IEnumerable BusinessPartnerNumbers, + [property: JsonPropertyName("company")] string CompanyName, + [property: JsonPropertyName("status")] UserStatusId UserStatusId, + [property: JsonPropertyName("assignedRoles")] IEnumerable AssignedRoles, + [property: JsonPropertyName("admin")] IEnumerable AdminDetails, + [property: JsonPropertyName("idpUserIds")] IEnumerable IdpUserIds) +{ + [JsonPropertyName("firstName")] + public string? FirstName { get; set; } + + [JsonPropertyName("lastName")] + public string? LastName { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } +} + +public record CompanyUserDetailTransferData( + [property: JsonPropertyName("companyUserId")] Guid CompanyUserId, + [property: JsonPropertyName("created")] DateTimeOffset CreatedAt, + [property: JsonPropertyName("bpn")] IEnumerable BusinessPartnerNumbers, + [property: JsonPropertyName("company")] string CompanyName, + [property: JsonPropertyName("status")] UserStatusId UserStatusId, + [property: JsonPropertyName("assignedRoles")] IEnumerable AssignedRoles, + [property: JsonPropertyName("idpUserIds")] IEnumerable IdpUserIds) +{ + [JsonPropertyName("firstName")] + public string? FirstName { get; set; } + + [JsonPropertyName("lastName")] + public string? LastName { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } +} + +public record IdpUserTransferId( + [property: JsonPropertyName("alias")] string? Alias, + [property: JsonPropertyName("userId")] string UserId); diff --git a/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserIdentityProviderProcessData.cs b/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserIdentityProviderProcessData.cs index 0ac8b20fdf..c9df539bfc 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserIdentityProviderProcessData.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Models/CompanyUserIdentityProviderProcessData.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -20,17 +19,17 @@ namespace Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; -public record CompanyUserIdentityProviderProcessData( +public record CompanyUserIdentityProviderProcessTransferData( Guid CompanyUserId, string? FirstName, string? LastName, string? Email, string CompanyName, string? Bpn, - IEnumerable ProviderLinkData + IEnumerable ProviderLinkData ); -public record ProviderLinkData( +public record ProviderLinkTransferData( string UserName, string? Alias, string ProviderUserId diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/CompanyRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/CompanyRepository.cs index d6db105873..01c98504eb 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/CompanyRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/CompanyRepository.cs @@ -112,7 +112,7 @@ public void CreateUpdateDeleteIdentifiers(Guid companyId, IEnumerable<(UniqueIde _context.Companies .AsNoTracking() .Where(company => company.CompanyStatusId == CompanyStatusId.ACTIVE && - (bpnIds == null || bpnIds.Contains(company.BusinessPartnerNumber) && + (bpnIds == null || bpnIds.Select(x => x.ToUpper()).Contains(company.BusinessPartnerNumber) && company.BusinessPartnerNumber != null)) .Select(company => company.BusinessPartnerNumber) .AsAsyncEnumerable(); diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/IUserRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/IUserRepository.cs index 29190381e5..bfd69dded3 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/IUserRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/IUserRepository.cs @@ -34,12 +34,14 @@ public interface IUserRepository Identity CreateIdentity(Guid companyId, UserStatusId userStatusId, IdentityTypeId identityTypeId, Action? setOptionalFields); void AttachAndModifyCompanyUser(Guid companyUserId, Action? initialize, Action setOptionalParameters); IQueryable GetOwnCompanyUserQuery(Guid companyId, Guid? companyUserId = null, string? firstName = null, string? lastName = null, string? email = null, IEnumerable? statusIds = null); + Func?>> GetOwnCompanyUserData(Guid companyId, Guid? companyUserId = null, string? firstName = null, string? lastName = null, string? email = null, IEnumerable? statusIds = null); + Task<(string? FirstName, string? LastName, string? Email)> GetUserEntityDataAsync(Guid companyUserId, Guid companyId); IAsyncEnumerable<(Guid CompanyUserId, bool IsFullMatch)> GetMatchingCompanyIamUsersByNameEmail(string firstName, string lastName, string email, Guid companyId, IEnumerable companyUserStatusIds); Task IsOwnCompanyUserWithEmailExisting(string email, Guid companyId); - Task GetOwnCompanyUserDetailsUntrackedAsync(Guid companyUserId, Guid companyId); + Task GetOwnCompanyUserDetailsUntrackedAsync(Guid companyUserId, Guid companyId); Task<(IEnumerable AssignedBusinessPartnerNumbers, bool IsValidUser)> GetOwnCompanyUserWithAssignedBusinessPartnerNumbersUntrackedAsync(Guid companyUserId, Guid companyId); - Task GetUserDetailsUntrackedAsync(Guid companyUserId, IEnumerable userRoleIds); + Task GetUserDetailsUntrackedAsync(Guid companyUserId, IEnumerable userRoleIds); Task GetUserWithCompanyIdpAsync(Guid companyUserId); /// @@ -123,6 +125,8 @@ public interface IUserRepository void AttachAndModifyIdentities(IEnumerable<(Guid IdentityId, Action? Initialize, Action Modify)> identities); CompanyUserAssignedIdentityProvider AddCompanyUserAssignedIdentityProvider(Guid companyUserId, Guid identityProviderId, string providerId, string userName); void RemoveCompanyUserAssignedIdentityProviders(IEnumerable<(Guid CompanyUserId, Guid IdentityProviderId)> companyUserIdentityProviderIds); - IAsyncEnumerable GetUserAssignedIdentityProviderForNetworkRegistration(Guid networkRegistrationId); + IAsyncEnumerable GetUserAssignedIdentityProviderForNetworkRegistration(Guid networkRegistrationId); IAsyncEnumerable GetNextIdentitiesForNetworkRegistration(Guid networkRegistrationId, IEnumerable validUserStates); + Task<(bool Exists, string ProviderId, string Username)> GetCompanyUserAssignedIdentityProvider(Guid companyUserId, Guid identityProviderId); + void AttachAndModifyUserAssignedIdentityProvider(Guid companyUserId, Guid identityProviderId, Action? initialize, Action modify); } diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/UserRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/UserRepository.cs index fb815f6983..e5ca10f0d3 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/UserRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/UserRepository.cs @@ -99,6 +99,42 @@ public IQueryable GetOwnCompanyUserQuery( (statusIds == null || statusIds.Contains(companyUser.Identity!.UserStatusId))); } + public Func?>> GetOwnCompanyUserData( + Guid companyId, + Guid? companyUserId = null, + string? firstName = null, + string? lastName = null, + string? email = null, + IEnumerable? statusIds = null) => + (skip, take) => Pagination.CreateSourceQueryAsync( + skip, + take, + _dbContext.CompanyUsers + .AsSplitQuery() + .Where(companyUser => + companyUser.Identity!.CompanyId == companyId && + (!companyUserId.HasValue || companyUser.Id == companyUserId.Value) && + (firstName == null || companyUser.Firstname == firstName) && + (lastName == null || companyUser.Lastname == lastName) && + (email == null || EF.Functions.ILike(companyUser.Email!, $"%{email.EscapeForILike()}%")) && + (statusIds == null || statusIds.Contains(companyUser.Identity!.UserStatusId))) + .GroupBy(x => x.Identity!.CompanyId), + x => x.OrderBy(cu => cu.Identity!.DateCreated), + cu => new CompanyUserTransferData( + cu.Id, + cu.Identity!.UserStatusId, + cu.Identity.DateCreated, + cu.Firstname, + cu.Lastname, + cu.Email, + cu.Identity!.IdentityAssignedRoles.Select(x => x.UserRole!).Select(userRole => new UserRoleData( + userRole.Id, + userRole.Offer!.AppInstances.First().IamClient!.ClientClientId, + userRole.UserRoleText)), + cu.CompanyUserAssignedIdentityProviders.Select(x => new IdpUserTransferId(x.IdentityProvider!.IamIdentityProvider!.IamIdpAlias, x.ProviderId)) + )) + .SingleOrDefaultAsync(); + public Task<(string? FirstName, string? LastName, string? Email)> GetUserEntityDataAsync(Guid companyUserId, Guid companyId) => _dbContext.CompanyUsers .AsNoTracking() @@ -129,14 +165,14 @@ public Task IsOwnCompanyUserWithEmailExisting(string email, Guid companyId _dbContext.CompanyUsers .AnyAsync(companyUser => companyUser.Identity!.CompanyId == companyId && companyUser.Email == email); - public Task GetOwnCompanyUserDetailsUntrackedAsync(Guid companyUserId, Guid companyId) => + public Task GetOwnCompanyUserDetailsUntrackedAsync(Guid companyUserId, Guid companyId) => _dbContext.CompanyUsers .AsNoTracking() .Where(companyUser => companyUser.Id == companyUserId && companyUser.Identity!.UserStatusId == UserStatusId.ACTIVE && companyUser.Identity!.CompanyId == companyId) - .Select(companyUser => new CompanyUserDetails( + .Select(companyUser => new CompanyUserDetailTransferData( companyUser.Id, companyUser.Identity!.DateCreated, companyUser.CompanyUserAssignedBusinessPartners.Select(assignedPartner => @@ -148,7 +184,8 @@ public Task IsOwnCompanyUserWithEmailExisting(string email, Guid companyId .Select(offer => new CompanyUserAssignedRoleDetails( offer.Id, offer.UserRoles.Where(role => companyUser.Identity!.IdentityAssignedRoles.Select(x => x.UserRole).Contains(role)).Select(x => x.UserRoleText) - ))) + )), + companyUser.CompanyUserAssignedIdentityProviders.Select(cuidp => new IdpUserTransferId(cuidp.IdentityProvider!.IamIdentityProvider!.IamIdpAlias, cuidp.ProviderId))) { FirstName = companyUser.Firstname, LastName = companyUser.Lastname, @@ -163,18 +200,16 @@ public Task IsOwnCompanyUserWithEmailExisting(string email, Guid companyId companyUser.Id == companyUserId && companyUser.Identity!.CompanyId == companyId) .Select(companyUser => new ValueTuple, bool>( - companyUser.CompanyUserAssignedBusinessPartners.Select(assignedPartner => - assignedPartner.BusinessPartnerNumber), - true) - ) + companyUser.CompanyUserAssignedBusinessPartners.Select(assignedPartner => assignedPartner.BusinessPartnerNumber), + true)) .SingleOrDefaultAsync(); - public Task GetUserDetailsUntrackedAsync(Guid companyUserId, IEnumerable userRoleIds) => + public Task GetUserDetailsUntrackedAsync(Guid companyUserId, IEnumerable userRoleIds) => _dbContext.CompanyUsers .AsNoTracking() .AsSplitQuery() .Where(companyUser => companyUser.Id == companyUserId) - .Select(companyUser => new CompanyOwnUserDetails( + .Select(companyUser => new CompanyOwnUserTransferDetails( companyUser.Id, companyUser.Identity!.DateCreated, companyUser.CompanyUserAssignedBusinessPartners.Select(assignedPartner => @@ -192,7 +227,8 @@ public Task IsOwnCompanyUserWithEmailExisting(string email, Guid companyId companyUser.Identity!.Company.Identities.Where(i => i.IdentityTypeId == IdentityTypeId.COMPANY_USER && i.IdentityAssignedRoles.Any(role => userRoleIds.Contains(role.UserRoleId))).Select(i => i.CompanyUser!) .Select(admin => new CompanyUserAdminDetails( admin.Id, - admin.Email))) + admin.Email)), + companyUser.CompanyUserAssignedIdentityProviders.Select(cuidp => new IdpUserTransferId(cuidp.IdentityProvider!.IamIdentityProvider!.IamIdpAlias, cuidp.ProviderId))) { FirstName = companyUser.Firstname, LastName = companyUser.Lastname, @@ -445,23 +481,45 @@ public CompanyUserAssignedIdentityProvider AddCompanyUserAssignedIdentityProvide public void RemoveCompanyUserAssignedIdentityProviders(IEnumerable<(Guid CompanyUserId, Guid IdentityProviderId)> companyUserIdentityProviderIds) => _dbContext.CompanyUserAssignedIdentityProviders.RemoveRange(companyUserIdentityProviderIds.Select(x => new CompanyUserAssignedIdentityProvider(x.CompanyUserId, x.IdentityProviderId, null!, null!))); - public IAsyncEnumerable GetUserAssignedIdentityProviderForNetworkRegistration(Guid networkRegistrationId) => + public IAsyncEnumerable GetUserAssignedIdentityProviderForNetworkRegistration(Guid networkRegistrationId) => _dbContext.CompanyUsers .Where(cu => cu.Identity!.UserStatusId == UserStatusId.PENDING && cu.Identity.Company!.NetworkRegistration!.Id == networkRegistrationId) .Select(cu => - new CompanyUserIdentityProviderProcessData( + new CompanyUserIdentityProviderProcessTransferData( cu.Id, cu.Firstname, cu.Lastname, cu.Email, cu.Identity!.Company!.Name, cu.Identity.Company.BusinessPartnerNumber, - cu.CompanyUserAssignedIdentityProviders.Select(assigned => new ProviderLinkData(assigned.UserName, assigned.IdentityProvider!.IamIdentityProvider!.IamIdpAlias, assigned.ProviderId)) + cu.CompanyUserAssignedIdentityProviders.Select(assigned => + new ProviderLinkTransferData( + assigned.UserName, + assigned.IdentityProvider!.IamIdentityProvider!.IamIdpAlias, + assigned.ProviderId)) )) .ToAsyncEnumerable(); + public Task<(bool Exists, string ProviderId, string Username)> GetCompanyUserAssignedIdentityProvider(Guid companyUserId, Guid identityProviderId) => + _dbContext.CompanyUserAssignedIdentityProviders + .Where(x => x.IdentityProviderId == identityProviderId && x.CompanyUserId == companyUserId) + .Select(x => new ValueTuple( + true, + x.ProviderId, + x.UserName + )) + .SingleOrDefaultAsync(); + + public void AttachAndModifyUserAssignedIdentityProvider(Guid companyUserId, Guid identityProviderId, Action? initialize, Action modify) + { + var companyUser = new CompanyUserAssignedIdentityProvider(companyUserId, identityProviderId, null!, null!); + initialize?.Invoke(companyUser); + var updatedEntity = _dbContext.Attach(companyUser).Entity; + modify.Invoke(updatedEntity); + } + public IAsyncEnumerable GetNextIdentitiesForNetworkRegistration(Guid networkRegistrationId, IEnumerable validUserStates) => _dbContext.CompanyUsers .Where(cu => diff --git a/src/portalbackend/PortalBackend.PortalEntities/Enums/ProcessTypeid.cs b/src/portalbackend/PortalBackend.PortalEntities/Enums/ProcessTypeId.cs similarity index 100% rename from src/portalbackend/PortalBackend.PortalEntities/Enums/ProcessTypeid.cs rename to src/portalbackend/PortalBackend.PortalEntities/Enums/ProcessTypeId.cs diff --git a/src/processes/NetworkRegistration.Library/NetworkRegistrationHandler.cs b/src/processes/NetworkRegistration.Library/NetworkRegistrationHandler.cs index b9e7284f16..88bfd4e084 100644 --- a/src/processes/NetworkRegistration.Library/NetworkRegistrationHandler.cs +++ b/src/processes/NetworkRegistration.Library/NetworkRegistrationHandler.cs @@ -18,11 +18,11 @@ ********************************************************************************/ using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Async; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Mailing.SendMail; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess; -using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; using Org.Eclipse.TractusX.Portal.Backend.Processes.NetworkRegistration.Library.DependencyInjection; @@ -104,7 +104,15 @@ public NetworkRegistrationHandler( continue; } - await _userProvisioningService.HandleCentralKeycloakCreation(new UserCreationRoleDataIdpInfo(cu.FirstName!, cu.LastName!, cu.Email!, roleData, string.Empty, string.Empty, UserStatusId.ACTIVE, true), cu.CompanyUserId, cu.CompanyName, cu.Bpn, null, cu.ProviderLinkData.Select(x => new IdentityProviderLink(x.Alias!, x.ProviderUserId, x.UserName)), userRepository, userRoleRepository).ConfigureAwait(false); + await _userProvisioningService.HandleCentralKeycloakCreation( + new UserCreationRoleDataIdpInfo(cu.FirstName!, cu.LastName!, cu.Email!, roleData, string.Empty, string.Empty, UserStatusId.ACTIVE, true), + cu.CompanyUserId, + cu.CompanyName, + cu.Bpn, + null, + cu.ProviderLinkData.Select(x => new IdentityProviderLink(x.Alias!, x.ProviderUserId, x.UserName)), + userRepository, + userRoleRepository).ConfigureAwait(false); } catch (Exception e) { @@ -112,7 +120,21 @@ public NetworkRegistrationHandler( } } - await SendMails(GetUserMailInformation(companyAssignedIdentityProviders), ospName).ConfigureAwait(false); + var displayNames = await companyAssignedIdentityProviders + .SelectMany(assigned => assigned.ProviderLinkData) + .Select(data => data.Alias!) + .Distinct() + .ToImmutableDictionaryAsync(async alias => + await _provisioningManager.GetIdentityProviderDisplayName(alias).ConfigureAwait(false) ?? throw new ConflictException($"Display Name should not be null for alias: {alias}")).ConfigureAwait(false); + + var userData = companyAssignedIdentityProviders.Select(userData => new UserMailInformation( + userData.Email ?? throw new UnexpectedConditionException("userData.Email should never be null here"), + userData.FirstName, + userData.LastName, + userData.ProviderLinkData.Select(data => displayNames[data.Alias!]))); + + await SendMails(userData, ospName).ConfigureAwait(false); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( null, ProcessStepStatusId.DONE, @@ -120,41 +142,9 @@ public NetworkRegistrationHandler( null); } - private async IAsyncEnumerable GetUserMailInformation(IEnumerable companyUserIdentityProviderProcessData) - { - var mapping = new Dictionary(); - - async Task GetDisplayName(string idpAlias) - { - if (!mapping.TryGetValue(idpAlias, out var displayName)) - { - displayName = await _provisioningManager.GetIdentityProviderDisplayName(idpAlias).ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(displayName)) - { - throw new ConflictException($"DisplayName for idpAlias {idpAlias} couldn't be determined"); - } - mapping.Add(idpAlias, displayName); - } - return displayName; - } - - foreach (var userData in companyUserIdentityProviderProcessData) - { - yield return new UserMailInformation( - userData.Email ?? throw new UnexpectedConditionException("userData.Email should never be null here"), - userData.FirstName, - userData.LastName, - await Task.WhenAll( - userData.ProviderLinkData.Select(pld => - GetDisplayName(pld.Alias ?? throw new UnexpectedConditionException("providerLinkData.Alias should never be null here")))).ConfigureAwait(false)); - } - } - - private static readonly IEnumerable MailTemplates = Enumerable.Repeat("OspWelcomeMail", 1); - - private async Task SendMails(IAsyncEnumerable companyUserWithRoleIdForCompany, string ospName) + private async Task SendMails(IEnumerable companyUserWithRoleIdForCompany, string ospName) { - await foreach (var (receiver, firstName, lastName, displayNames) in companyUserWithRoleIdForCompany) + foreach (var (receiver, firstName, lastName, displayNames) in companyUserWithRoleIdForCompany) { var userName = string.Join(" ", firstName, lastName); var mailParameters = new Dictionary @@ -167,7 +157,7 @@ private async Task SendMails(IAsyncEnumerable companyUserWi { "url", _settings.BasePortalAddress }, { "idpAlias", string.Join(",", displayNames) } }; - await _mailingService.SendMails(receiver, mailParameters, MailTemplates).ConfigureAwait(false); + await _mailingService.SendMails(receiver, mailParameters, Enumerable.Repeat("OspWelcomeMail", 1)).ConfigureAwait(false); } } diff --git a/src/processes/Processes.Worker/Processes.Worker.csproj b/src/processes/Processes.Worker/Processes.Worker.csproj index 595b6509d0..c50d9b584c 100644 --- a/src/processes/Processes.Worker/Processes.Worker.csproj +++ b/src/processes/Processes.Worker/Processes.Worker.csproj @@ -1,5 +1,4 @@ + + + + Org.Eclipse.TractusX.Portal.Backend.Framework.Synchronization.Executor.Tests + Org.Eclipse.TractusX.Portal.Backend.Framework.Synchronization.Executor.Tests + net7.0 + enable + enable + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/processes/Synchronization.Executor.Tests/Usings.cs b/tests/processes/Synchronization.Executor.Tests/Usings.cs new file mode 100644 index 0000000000..65016aec51 --- /dev/null +++ b/tests/processes/Synchronization.Executor.Tests/Usings.cs @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 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 + ********************************************************************************/ + +global using AutoFixture; +global using AutoFixture.AutoFakeItEasy; +global using FakeItEasy; +global using FluentAssertions; +global using Xunit;