Skip to content

Commit

Permalink
feat(technicalUser): add locking for service accounts
Browse files Browse the repository at this point in the history
Refs: #809
  • Loading branch information
Phil91 authored and ntruchsess committed Aug 2, 2024
1 parent 0820055 commit 61ecbc8
Show file tree
Hide file tree
Showing 16 changed files with 201 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
using Org.Eclipse.TractusX.Portal.Backend.Administration.Service.ErrorHandling;
using Org.Eclipse.TractusX.Portal.Backend.Administration.Service.Models;
using Org.Eclipse.TractusX.Portal.Backend.Dim.Library.Models;
using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider;
using Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess;
using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling;
using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq;
using Org.Eclipse.TractusX.Portal.Backend.Framework.Models;
using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration;
using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Encryption;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models;
Expand All @@ -43,11 +46,13 @@ public class ServiceAccountBusinessLogic(
IPortalRepositories portalRepositories,
IOptions<ServiceAccountSettings> options,
IServiceAccountCreation serviceAccountCreation,
IIdentityService identityService)
IIdentityService identityService,
IDateTimeProvider dateTimeProvider)
: IServiceAccountBusinessLogic
{
private readonly IIdentityData _identityData = identityService.IdentityData;
private readonly ServiceAccountSettings _settings = options.Value;
private readonly TimeSpan lockExpiryTime = new(options.Value.LockExpirySeconds * 10000000L);

private const string CompanyId = "companyId";

Expand Down Expand Up @@ -114,15 +119,23 @@ public async Task<int> DeleteOwnCompanyServiceAccountAsync(Guid serviceAccountId
throw ConflictException.Create(AdministrationServiceAccountErrors.SERVICE_USERID_ACTIVATION_ACTIVE_CONFLICT);
}

portalRepositories.GetInstance<IUserRepository>().AttachAndModifyIdentity(serviceAccountId, null, i =>
if (!result.ServiceAccount.TryLock(dateTimeProvider.OffsetNow.Add(lockExpiryTime)))
{
i.UserStatusId = UserStatusId.INACTIVE;
});
throw UnexpectedConditionException.Create(AdministrationServiceAccountErrors.SERVICE_ACCOUNT_LOCKED, [new("serviceAccountId", serviceAccountId.ToString())]);
}

// save the lock of the service account here to make sure no process overwrites it
await portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);
portalRepositories.Clear();

// serviceAccount
if (!string.IsNullOrWhiteSpace(result.ClientClientId) && !result.IsDimServiceAccount)
{
await provisioningManager.DeleteCentralClientAsync(result.ClientClientId).ConfigureAwait(ConfigureAwaitOptions.None);
portalRepositories.GetInstance<IUserRepository>().AttachAndModifyIdentity(serviceAccountId, null, i =>
{
i.UserStatusId = UserStatusId.INACTIVE;
});
}

if (result.IsDimServiceAccount)
Expand Down Expand Up @@ -151,6 +164,7 @@ public async Task<int> DeleteOwnCompanyServiceAccountAsync(Guid serviceAccountId
});
}

result.ServiceAccount.ReleaseLock();
return await portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);
}

Expand All @@ -170,10 +184,10 @@ public async Task<ServiceAccountConnectorOfferData> GetOwnCompanyServiceAccountD
if (result.DimServiceAccountData != null)
{
iamClientAuthMethod = IamClientAuthMethod.SECRET;
secret = Decrypt(
var cryptoHelper = _settings.EncryptionConfigs.GetCryptoHelper(_settings.EncryptionConfigIndex);
secret = cryptoHelper.Decrypt(
result.DimServiceAccountData.ClientSecret,
result.DimServiceAccountData.InitializationVector,
result.DimServiceAccountData.EncryptionMode);
result.DimServiceAccountData.InitializationVector);
}
else if (result.ClientClientId != null)
{
Expand Down Expand Up @@ -205,23 +219,6 @@ public async Task<ServiceAccountConnectorOfferData> GetOwnCompanyServiceAccountD
result.SubscriptionId);
}

private string Decrypt(byte[]? clientSecret, byte[]? initializationVector, int? encryptionMode)
{
if (clientSecret == null)
{
throw new ConflictException("ClientSecret must not be null");
}

if (encryptionMode == null)
{
throw new ConflictException("EncryptionMode must not be null");
}

var cryptoConfig = _settings.EncryptionConfigs.SingleOrDefault(x => x.Index == encryptionMode) ?? throw new ConfigurationException($"EncryptionModeIndex {encryptionMode} is not configured");

return CryptoHelper.Decrypt(clientSecret, initializationVector, Convert.FromHexString(cryptoConfig.EncryptionKey), cryptoConfig.CipherMode, cryptoConfig.PaddingMode);
}

public async Task<ServiceAccountDetails> ResetOwnCompanyServiceAccountSecretAsync(Guid serviceAccountId)
{
var companyId = _identityData.CompanyId;
Expand Down Expand Up @@ -275,16 +272,25 @@ public async Task<ServiceAccountDetails> UpdateOwnCompanyServiceAccountDetailsAs
throw ConflictException.Create(AdministrationServiceAccountErrors.SERVICE_INACTIVE_CONFLICT, [new("serviceAccountId", serviceAccountId.ToString())]);
}

if (result.ClientClientId == null)
if (result.ServiceAccount.ClientClientId == null)
{
throw ConflictException.Create(AdministrationServiceAccountErrors.SERVICE_CLIENTID_NOT_NULL_CONFLICT, [new("serviceAccountId", serviceAccountId.ToString())]);
}

if (!result.ServiceAccount.TryLock(dateTimeProvider.OffsetNow.Add(lockExpiryTime)))
{
throw UnexpectedConditionException.Create(AdministrationServiceAccountErrors.SERVICE_ACCOUNT_LOCKED, [new("serviceAccountId", serviceAccountId.ToString())]);
}

// save the lock of the service account here to make sure no process overwrites it
await portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);
portalRepositories.Clear();

ClientAuthData? authData;
if (result.CompanyServiceAccountKindId == CompanyServiceAccountKindId.INTERNAL)
if (result.ServiceAccount.CompanyServiceAccountKindId == CompanyServiceAccountKindId.INTERNAL)
{
var internalClientId = await provisioningManager.UpdateCentralClientAsync(
result.ClientClientId,
result.ServiceAccount.ClientClientId,
new ClientConfigData(
serviceAccountDetails.Name,
serviceAccountDetails.Description,
Expand All @@ -301,28 +307,29 @@ public async Task<ServiceAccountDetails> UpdateOwnCompanyServiceAccountDetailsAs
serviceAccountId,
sa =>
{
sa.Name = result.Name;
sa.Description = result.Description;
sa.Name = result.ServiceAccount.Name;
sa.Description = result.ServiceAccount.Description;
},
sa =>
{
sa.Name = serviceAccountDetails.Name;
sa.Description = serviceAccountDetails.Description;
});

result.ServiceAccount.ReleaseLock();
await portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);

return new ServiceAccountDetails(
result.ServiceAccountId,
result.ClientClientId,
result.ServiceAccount.Id,
result.ServiceAccount.ClientClientId,
serviceAccountDetails.Name,
serviceAccountDetails.Description,
result.UserStatusId,
authData?.IamClientAuthMethod,
result.UserRoleDatas,
result.CompanyServiceAccountTypeId,
result.ServiceAccount.CompanyServiceAccountTypeId,
authData?.Secret,
result.OfferSubscriptionId);
result.ServiceAccount.OfferSubscriptionId);
}

public Task<Pagination.Response<CompanyServiceAccountData>> GetOwnCompanyServiceAccountsDataAsync(int page, int size, string? clientId, bool? isOwner, bool filterForInactive, IEnumerable<UserStatusId>? userStatusIds)
Expand Down Expand Up @@ -387,28 +394,46 @@ private void CreateDimServiceAccount(AuthenticationDetail callbackData, Guid ser
portalRepositories.GetInstance<IUserRepository>().AttachAndModifyIdentity(serviceAccountId,
i => { i.UserStatusId = UserStatusId.PENDING; },
i => { i.UserStatusId = UserStatusId.ACTIVE; });

serviceAccountRepository.AttachAndModifyCompanyServiceAccount(serviceAccountId,
sa => { sa.ClientClientId = null; },
sa => { sa.ClientClientId = callbackData.ClientId; });

var cryptoConfig = _settings.EncryptionConfigs.SingleOrDefault(x => x.Index == _settings.EncryptionConfigIndex) ?? throw new ConfigurationException($"EncryptionModeIndex {_settings.EncryptionConfigIndex} is not configured");
var (secret, initializationVector) = CryptoHelper.Encrypt(callbackData.ClientSecret, Convert.FromHexString(cryptoConfig.EncryptionKey), cryptoConfig.CipherMode, cryptoConfig.PaddingMode);
var cryptoHelper = _settings.EncryptionConfigs.GetCryptoHelper(_settings.EncryptionConfigIndex);
var (secret, initializationVector) = cryptoHelper.Encrypt(callbackData.ClientSecret);

serviceAccountRepository.CreateDimCompanyServiceAccount(serviceAccountId, callbackData.AuthenticationServiceUrl, secret, initializationVector, _settings.EncryptionConfigIndex);
}

public async Task HandleServiceAccountDeletionCallback(Guid processId)
{
var processData = await portalRepositories.GetInstance<IProcessStepRepository>().GetProcessDataForServiceAccountDeletionCallback(processId, [ProcessStepTypeId.AWAIT_CREATE_DIM_TECHNICAL_USER_RESPONSE])
var processData = await portalRepositories.GetInstance<IProcessStepRepository>()
.GetProcessDataForServiceAccountDeletionCallback(processId,
[ProcessStepTypeId.AWAIT_CREATE_DIM_TECHNICAL_USER_RESPONSE])
.ConfigureAwait(ConfigureAwaitOptions.None);

var context = processData.ProcessData.CreateManualProcessData(ProcessStepTypeId.AWAIT_DELETE_DIM_TECHNICAL_USER, portalRepositories, () => $"externalId {processId}");
var context = processData.ProcessData.CreateManualProcessData(ProcessStepTypeId.AWAIT_DELETE_DIM_TECHNICAL_USER,
portalRepositories, () => $"externalId {processId}");

if (processData.ServiceAccountId is null)
if (processData.ServiceAccount is null)
{
throw new ConflictException($"ServiceAccountId must be set for process {processId}");
}

if (!processData.ServiceAccount.TryLock(dateTimeProvider.OffsetNow.Add(lockExpiryTime)))
{
throw UnexpectedConditionException.Create(AdministrationServiceAccountErrors.SERVICE_ACCOUNT_LOCKED, [new("serviceAccountId", processData.ServiceAccount.Id.ToString())]);
}

// save the lock of the service account here to make sure no process overwrites it
await portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);
portalRepositories.Clear();

portalRepositories.GetInstance<IUserRepository>().AttachAndModifyIdentity(processData.ServiceAccount.Id, null, i =>
{
i.UserStatusId = UserStatusId.INACTIVE;
});

context.FinalizeProcessStep();
await portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class ServiceAccountSettings
[Required]
[DistinctValues("x => x.Index")]
public IEnumerable<EncryptionModeConfig> EncryptionConfigs { get; set; } = null!;

public int LockExpirySeconds { get; set; }
}

public static class ServiceAccountSettingsExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public class AdministrationServiceAccountErrorMessageContainer : IErrorMessageCo
{ AdministrationServiceAccountErrors.SERVICE_ID_PATH_NOT_MATCH_ARGUMENT, "serviceAccountId {serviceAccountId} from path does not match the one in body {serviceAccountDetailsServiceAccountId}"},
{ AdministrationServiceAccountErrors.SERVICE_INACTIVE_CONFLICT, "serviceAccount {serviceAccountId} is already INACTIVE"},
{ AdministrationServiceAccountErrors.SERVICE_CLIENTID_NOT_NULL_CONFLICT, "clientClientId of serviceAccount {serviceAccountId} should not be null"},
{ AdministrationServiceAccountErrors.SERVICE_ACCOUNT_NOT_LINKED_TO_PROCESS, "Service Account {serviceAccountId} is not linked to a process" }
{ AdministrationServiceAccountErrors.SERVICE_ACCOUNT_NOT_LINKED_TO_PROCESS, "Service Account {serviceAccountId} is not linked to a process" },
{ AdministrationServiceAccountErrors.SERVICE_ACCOUNT_LOCKED, "Service Account {serviceAccountId} is locked by another process" }
}.ToImmutableDictionary(x => (int)x.Key, x => x.Value);

public Type Type { get => typeof(AdministrationServiceAccountErrors); }
Expand All @@ -58,5 +59,6 @@ public enum AdministrationServiceAccountErrors
SERVICE_ID_PATH_NOT_MATCH_ARGUMENT,
SERVICE_INACTIVE_CONFLICT,
SERVICE_CLIENTID_NOT_NULL_CONFLICT,
SERVICE_ACCOUNT_NOT_LINKED_TO_PROCESS
SERVICE_ACCOUNT_NOT_LINKED_TO_PROCESS,
SERVICE_ACCOUNT_LOCKED
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,12 @@
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Entities;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums;

namespace Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models;

public record CompanyServiceAccountWithRoleDataClientId(
Guid ServiceAccountId,
CompanyServiceAccount ServiceAccount,
UserStatusId UserStatusId,
string Name,
string Description,
CompanyServiceAccountTypeId CompanyServiceAccountTypeId,
CompanyServiceAccountKindId CompanyServiceAccountKindId,
Guid? OfferSubscriptionId,
string? ClientClientId,
IEnumerable<UserRoleData> UserRoleDatas);
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Entities;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums;

namespace Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models;

public record OwnServiceAccountData(
IEnumerable<Guid> UserRoleIds,
CompanyServiceAccount ServiceAccount,
Guid? ConnectorId,
string? ClientClientId,
ConnectorStatusId? StatusId,
OfferSubscriptionStatusId? OfferStatusId,
bool IsDimServiceAccount,
string? Bpn,
string ServiceAccountName,
Guid? ProcessId
);
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ public interface IProcessStepRepository
IAsyncEnumerable<(Guid ProcessStepId, ProcessStepTypeId ProcessStepTypeId)> GetProcessStepData(Guid processId);
public Task<(bool ProcessExists, VerifyProcessData ProcessData)> IsValidProcess(Guid processId, ProcessTypeId processTypeId, IEnumerable<ProcessStepTypeId> processStepTypeIds);
Task<(ProcessTypeId ProcessTypeId, VerifyProcessData ProcessData, Guid? ServiceAccountId)> GetProcessDataForServiceAccountCallback(Guid processId, IEnumerable<ProcessStepTypeId> processStepTypeIds);
Task<(ProcessTypeId ProcessTypeId, VerifyProcessData ProcessData, Guid? ServiceAccountId)> GetProcessDataForServiceAccountDeletionCallback(Guid processId, IEnumerable<ProcessStepTypeId> processStepTypeIds);
Task<(ProcessTypeId ProcessTypeId, VerifyProcessData ProcessData, CompanyServiceAccount? ServiceAccount)> GetProcessDataForServiceAccountDeletionCallback(Guid processId, IEnumerable<ProcessStepTypeId> processStepTypeIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,19 +137,19 @@ public IAsyncEnumerable<Process> GetActiveProcesses(IEnumerable<ProcessTypeId> p
)
.SingleOrDefaultAsync();

public Task<(ProcessTypeId ProcessTypeId, VerifyProcessData ProcessData, Guid? ServiceAccountId)> GetProcessDataForServiceAccountDeletionCallback(Guid processId, IEnumerable<ProcessStepTypeId> processStepTypeIds) =>
public Task<(ProcessTypeId ProcessTypeId, VerifyProcessData ProcessData, CompanyServiceAccount? ServiceAccount)> GetProcessDataForServiceAccountDeletionCallback(Guid processId, IEnumerable<ProcessStepTypeId> processStepTypeIds) =>
_context.Processes
.AsNoTracking()
.Where(x => x.Id == processId && x.ProcessTypeId == ProcessTypeId.DIM_TECHNICAL_USER)
.Select(x => new ValueTuple<ProcessTypeId, VerifyProcessData, Guid?>(
.Select(x => new ValueTuple<ProcessTypeId, VerifyProcessData, CompanyServiceAccount?>(
x.ProcessTypeId,
new VerifyProcessData(
x,
x.ProcessSteps
.Where(step =>
processStepTypeIds.Contains(step.ProcessStepTypeId) &&
step.ProcessStepStatusId == ProcessStepStatusId.TODO)),
x.DimUserCreationData!.ServiceAccountId)
x.DimUserCreationData!.ServiceAccount)
)
.SingleOrDefaultAsync();
}
Loading

0 comments on commit 61ecbc8

Please sign in to comment.