Skip to content

Commit

Permalink
implement optimistic locking for serviceaccounts
Browse files Browse the repository at this point in the history
  • Loading branch information
ntruchsess committed Jul 19, 2024
1 parent 6907769 commit d211709
Show file tree
Hide file tree
Showing 49 changed files with 455 additions and 305 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
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;
Expand All @@ -45,13 +44,11 @@ public class ServiceAccountBusinessLogic(
IPortalRepositories portalRepositories,
IOptions<ServiceAccountSettings> options,
IServiceAccountCreation serviceAccountCreation,
IIdentityService identityService,
IDateTimeProvider dateTimeProvider)
IIdentityService identityService)
: 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 @@ -115,16 +112,23 @@ public async Task<int> DeleteOwnCompanyServiceAccountAsync(Guid serviceAccountId
throw ConflictException.Create(AdministrationServiceAccountErrors.SERVICE_USERID_ACTIVATION_ACTIVE_CONFLICT);
}

if (!result.ServiceAccount.TryLock(dateTimeProvider.OffsetNow.Add(_lockExpiryTime)))
// serviceAccount
if (result.IsDimServiceAccount)
{
throw ConflictException.Create(AdministrationServiceAccountErrors.SERVICE_ACCOUNT_LOCKED, [new("serviceAccountId", serviceAccountId.ToString())]);
}
var processId = result.ProcessId ?? throw ConflictException.Create(AdministrationServiceAccountErrors.SERVICE_ACCOUNT_NOT_LINKED_TO_PROCESS, [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);
var processData = await portalRepositories.GetInstance<IProcessStepRepository>()
.GetProcessDataForServiceAccountDeletionCallback(processId, [ProcessStepTypeId.BEFORE_DELETE_DIM_TECHNICAL_USER])
.ConfigureAwait(ConfigureAwaitOptions.None);

// serviceAccount
if (!string.IsNullOrWhiteSpace(result.ClientClientId) && !result.IsDimServiceAccount)
var context = processData.ProcessData.CreateManualProcessData(ProcessStepTypeId.BEFORE_DELETE_DIM_TECHNICAL_USER,
portalRepositories, () => $"externalId {processId}");

context.ScheduleProcessSteps([ProcessStepTypeId.DELETE_DIM_TECHNICAL_USER]);

context.FinalizeProcessStep();
}
else if (!string.IsNullOrWhiteSpace(result.ClientClientId))
{
await provisioningManager.DeleteCentralClientAsync(result.ClientClientId).ConfigureAwait(ConfigureAwaitOptions.None);
portalRepositories.GetInstance<IUserRepository>().AttachAndModifyIdentity(serviceAccountId, null, i =>
Expand All @@ -133,17 +137,6 @@ public async Task<int> DeleteOwnCompanyServiceAccountAsync(Guid serviceAccountId
});
}

if (result.IsDimServiceAccount)
{
if (result.ProcessId == null)
{
throw ConflictException.Create(AdministrationServiceAccountErrors.SERVICE_ACCOUNT_NOT_LINKED_TO_PROCESS, [new("serviceAccountId", serviceAccountId.ToString())]);
}

var processStepRepository = portalRepositories.GetInstance<ProcessStepRepository>();
processStepRepository.CreateProcessStep(ProcessStepTypeId.DELETE_DIM_TECHNICAL_USER, ProcessStepStatusId.TODO, result.ProcessId.Value);
}

portalRepositories.GetInstance<IUserRolesRepository>().DeleteCompanyUserAssignedRoles(result.UserRoleIds.Select(userRoleId => (serviceAccountId, userRoleId)));

if (result.ConnectorId != null)
Expand All @@ -159,7 +152,6 @@ public async Task<int> DeleteOwnCompanyServiceAccountAsync(Guid serviceAccountId
});
}

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

Expand Down Expand Up @@ -267,24 +259,16 @@ public async Task<ServiceAccountDetails> UpdateOwnCompanyServiceAccountDetailsAs
throw ConflictException.Create(AdministrationServiceAccountErrors.SERVICE_INACTIVE_CONFLICT, [new("serviceAccountId", serviceAccountId.ToString())]);
}

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

if (!result.ServiceAccount.TryLock(dateTimeProvider.OffsetNow.Add(_lockExpiryTime)))
{
throw ConflictException.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);

ClientAuthData? authData;
if (result.ServiceAccount.CompanyServiceAccountKindId == CompanyServiceAccountKindId.INTERNAL)
if (result.CompanyServiceAccountKindId == CompanyServiceAccountKindId.INTERNAL)
{
var internalClientId = await provisioningManager.UpdateCentralClientAsync(
result.ServiceAccount.ClientClientId,
result.ClientClientId,
new ClientConfigData(
serviceAccountDetails.Name,
serviceAccountDetails.Description,
Expand All @@ -297,23 +281,33 @@ public async Task<ServiceAccountDetails> UpdateOwnCompanyServiceAccountDetailsAs
authData = null;
}

result.ServiceAccount.Name = serviceAccountDetails.Name;
result.ServiceAccount.Description = serviceAccountDetails.Description;
serviceAccountRepository.AttachAndModifyCompanyServiceAccount(
serviceAccountId,
result.ServiceAccountVersion,
sa =>
{
sa.Name = result.Name;
sa.Description = result.Description;
},
sa =>
{
sa.Name = serviceAccountDetails.Name;
sa.Description = serviceAccountDetails.Description;
});

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

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

public Task<Pagination.Response<CompanyServiceAccountData>> GetOwnCompanyServiceAccountsDataAsync(int page, int size, string? clientId, bool? isOwner, bool filterForInactive, IEnumerable<UserStatusId>? userStatusIds)
Expand Down Expand Up @@ -349,37 +343,31 @@ public async Task HandleServiceAccountCreationCallback(Guid processId, Authentic
{
throw new ConflictException($"ServiceAccountId must be set for process {processId}");
}

switch (processData.ProcessTypeId)
if (processData.ServiceAccountVersion is null)
{
case ProcessTypeId.OFFER_SUBSCRIPTION:
HandleOfferSubscriptionTechnicalUserCallback(processData.ServiceAccountId.Value, callbackData, context);
break;
case ProcessTypeId.DIM_TECHNICAL_USER:
CreateDimServiceAccount(callbackData, processData.ServiceAccountId.Value);
break;
default:
throw new ControllerArgumentException($"process {processId} has invalid processType {processData.ProcessTypeId}");
throw new UnexpectedConditionException("ServiceAccountVersion or IdentityVersion should never be null here");
}

CreateDimServiceAccount(callbackData, processData.ServiceAccountId.Value, processData.ServiceAccountVersion.Value);

context.ScheduleProcessSteps(processData.ProcessTypeId switch
{
ProcessTypeId.OFFER_SUBSCRIPTION => [ProcessStepTypeId.TRIGGER_ACTIVATE_SUBSCRIPTION, ProcessStepTypeId.BEFORE_DELETE_DIM_TECHNICAL_USER],
ProcessTypeId.DIM_TECHNICAL_USER => [ProcessStepTypeId.BEFORE_DELETE_DIM_TECHNICAL_USER],
_ => throw new ControllerArgumentException($"process {processId} has invalid processType {processData.ProcessTypeId}")
});
context.FinalizeProcessStep();
await portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);
}

private void HandleOfferSubscriptionTechnicalUserCallback(Guid serviceAccountId, AuthenticationDetail callbackData, ManualProcessStepData context)
{
CreateDimServiceAccount(callbackData, serviceAccountId);
context.ScheduleProcessSteps([ProcessStepTypeId.TRIGGER_ACTIVATE_SUBSCRIPTION]);
}

private void CreateDimServiceAccount(AuthenticationDetail callbackData, Guid serviceAccountId)
private void CreateDimServiceAccount(AuthenticationDetail callbackData, Guid serviceAccountId, Guid serviceAccountVersion)
{
var serviceAccountRepository = portalRepositories.GetInstance<IServiceAccountRepository>();
portalRepositories.GetInstance<IUserRepository>().AttachAndModifyIdentity(serviceAccountId,
i => { i.UserStatusId = UserStatusId.PENDING; },
i => { i.UserStatusId = UserStatusId.ACTIVE; });

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

Expand All @@ -399,25 +387,14 @@ public async Task HandleServiceAccountDeletionCallback(Guid processId)
var context = processData.ProcessData.CreateManualProcessData(ProcessStepTypeId.AWAIT_DELETE_DIM_TECHNICAL_USER,
portalRepositories, () => $"externalId {processId}");

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

if (!processData.ServiceAccount.TryLock(dateTimeProvider.OffsetNow.Add(_lockExpiryTime)))
{
throw ConflictException.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.GetInstance<IUserRepository>().AttachAndModifyIdentity(processData.ServiceAccount.Id, null, i =>
{
i.UserStatusId = UserStatusId.INACTIVE;
});
portalRepositories.GetInstance<IUserRepository>().AttachAndModifyIdentity(
processData.ServiceAccountId ?? throw new ConflictException($"ServiceAccountId must be set for process {processId}"),
null,
i =>
{
i.UserStatusId = UserStatusId.INACTIVE;
});

processData.ServiceAccount.ReleaseLock();
context.FinalizeProcessStep();
await portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);
}
Expand Down
2 changes: 1 addition & 1 deletion src/framework/Framework.Async/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<Project>
<PropertyGroup>
<VersionPrefix>2.3.0</VersionPrefix>
<VersionPrefix>2.6.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
</PropertyGroup>
</Project>
2 changes: 1 addition & 1 deletion src/framework/Framework.Cors/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<Project>
<PropertyGroup>
<VersionPrefix>2.3.0</VersionPrefix>
<VersionPrefix>2.6.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
</PropertyGroup>
</Project>
2 changes: 1 addition & 1 deletion src/framework/Framework.DBAccess/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<Project>
<PropertyGroup>
<VersionPrefix>2.3.0</VersionPrefix>
<VersionPrefix>2.6.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
</PropertyGroup>
</Project>
27 changes: 27 additions & 0 deletions src/framework/Framework.DBAccess/IVersionedEntityHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/********************************************************************************
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess;

public interface IVersionedEntityHandler
{
void HandleVersionForChangedEntries(ChangeTracker changeTracker);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/********************************************************************************
* Copyright (c) 2022 BMW Group AG
* Copyright (c) 2022 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
Expand All @@ -22,7 +21,7 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess;

public static class VersionedEntityExtensions
{
public static void UpdateVersion(this ILockableEntity entity)
public static void UpdateVersion(this IVersionedEntity entity)
{
entity.Version = Guid.NewGuid();
}
Expand Down
34 changes: 34 additions & 0 deletions src/framework/Framework.DBAccess/VersionedEntityHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/********************************************************************************
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess;

public class VersionedEntityHandler : IVersionedEntityHandler
{
public void HandleVersionForChangedEntries(ChangeTracker changeTracker)
{
foreach (var item in changeTracker.Entries().Where(entry => entry.State is EntityState.Modified or EntityState.Deleted).OfType<IVersionedEntity>())
{
item.UpdateVersion();
}
}
}
30 changes: 30 additions & 0 deletions src/framework/Framework.DBAccess/VersioningDependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/********************************************************************************
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

using Microsoft.Extensions.DependencyInjection;

namespace Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess;

public static class VersioningDependencyInjection
{
public static IServiceCollection AddDbVersioning(this IServiceCollection services)
{
return services.AddTransient<IVersionedEntityHandler, VersionedEntityHandler>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<Project>
<PropertyGroup>
<VersionPrefix>2.3.0</VersionPrefix>
<VersionPrefix>2.6.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<Project>
<PropertyGroup>
<VersionPrefix>2.3.0</VersionPrefix>
<VersionPrefix>2.6.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<Project>
<PropertyGroup>
<VersionPrefix>2.3.0</VersionPrefix>
<VersionPrefix>2.6.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
</PropertyGroup>
</Project>
Loading

0 comments on commit d211709

Please sign in to comment.