diff --git a/.tractusx b/.tractusx
index 3949d4cdf2..4717e14410 100644
--- a/.tractusx
+++ b/.tractusx
@@ -19,8 +19,8 @@
leadingRepository: "https://github.com/eclipse-tractusx/portal"
openApiSpecs:
-- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC2/docs/api/administration-service.yaml"
-- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC2/docs/api/apps-service.yaml"
-- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC2/docs/api/notifications-service.yaml"
-- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC2/docs/api/registration-service.yaml"
-- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC2/docs/api/services-service.yaml"
+- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC3/docs/api/administration-service.yaml"
+- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC3/docs/api/apps-service.yaml"
+- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC3/docs/api/notifications-service.yaml"
+- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC3/docs/api/registration-service.yaml"
+- "https://raw.githubusercontent.com/eclipse-tractusx/portal-backend/refs/tags/v2.3.0-RC3/docs/api/services-service.yaml"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e454d0f2b0..8238223992 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,17 @@ New features, fixed bugs, known defects and other noteworthy changes to each rel
## Unreleased
+## 2.3.0-RC3
+
+### Technical Support
+
+* upgraded System.Text.Encodings.Web and System.Net.Http packages [#1156](https://github.com/eclipse-tractusx/portal-backend/pull/1156)
+
+### Bugfixes
+
+* **Keycloak realm seeding job**: added user profile and localization texts to seeding [#1154](https://github.com/eclipse-tractusx/portal-backend/pull/1154)
+* **Own Identity Provider**: fixed setting URLs for IdP update [#1153](https://github.com/eclipse-tractusx/portal-backend/pull/1153)
+
## 2.3.0-RC2
### Change
diff --git a/docs/api/administration-service.yaml b/docs/api/administration-service.yaml
index 1f42b23719..7307356085 100644
--- a/docs/api/administration-service.yaml
+++ b/docs/api/administration-service.yaml
@@ -1,7 +1,7 @@
openapi: 3.0.1
info:
title: Org.Eclipse.TractusX.Portal.Backend.Administration.Service
- version: v2.3.0-RC2
+ version: v2.3.0-RC3
paths:
/api/administration/companydata/ownCompanyDetails:
get:
diff --git a/docs/api/apps-service.yaml b/docs/api/apps-service.yaml
index feddd4e1c2..f3f08e4eab 100644
--- a/docs/api/apps-service.yaml
+++ b/docs/api/apps-service.yaml
@@ -1,7 +1,7 @@
openapi: 3.0.1
info:
title: Org.Eclipse.TractusX.Portal.Backend.Apps.Service
- version: v2.3.0-RC2
+ version: v2.3.0-RC3
paths:
'/api/apps/AppChange/{appId}/role/activeapp':
post:
diff --git a/docs/api/notifications-service.yaml b/docs/api/notifications-service.yaml
index eb3bd964f6..2bc85ce85d 100644
--- a/docs/api/notifications-service.yaml
+++ b/docs/api/notifications-service.yaml
@@ -1,7 +1,7 @@
openapi: 3.0.1
info:
title: Org.Eclipse.TractusX.Portal.Backend.Notifications.Service
- version: v2.3.0-RC2
+ version: v2.3.0-RC3
paths:
/api/notification/errormessage:
get:
diff --git a/docs/api/registration-service.yaml b/docs/api/registration-service.yaml
index a8c8c1e072..e8b3137513 100644
--- a/docs/api/registration-service.yaml
+++ b/docs/api/registration-service.yaml
@@ -1,7 +1,7 @@
openapi: 3.0.1
info:
title: Org.Eclipse.TractusX.Portal.Backend.Registration.Service
- version: v2.3.0-RC2
+ version: v2.3.0-RC3
paths:
/api/registration/errormessage:
get:
diff --git a/docs/api/services-service.yaml b/docs/api/services-service.yaml
index fc508d2bfa..23fb44d3df 100644
--- a/docs/api/services-service.yaml
+++ b/docs/api/services-service.yaml
@@ -1,7 +1,7 @@
openapi: 3.0.1
info:
title: Org.Eclipse.TractusX.Portal.Backend.Services.Service
- version: v2.3.0-RC2
+ version: v2.3.0-RC3
paths:
/api/services/errormessage:
get:
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index f736243330..7df3464fe3 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -20,6 +20,6 @@
2.3.0
- RC2
+ RC3
diff --git a/src/administration/Administration.Service/BusinessLogic/IdentityProviderBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/IdentityProviderBusinessLogic.cs
index 18a9f38bcf..a07f336036 100644
--- a/src/administration/Administration.Service/BusinessLogic/IdentityProviderBusinessLogic.cs
+++ b/src/administration/Administration.Service/BusinessLogic/IdentityProviderBusinessLogic.cs
@@ -304,6 +304,7 @@ await provisioningManager.UpdateCentralIdentityProviderDataOIDCAsync(
alias,
iamIdentityProvider => iamIdentityProvider.MetadataUrl = metadataUrl,
iamIdentityProvider => iamIdentityProvider.MetadataUrl = details.Oidc.MetadataUrl);
+ await portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);
}
private async ValueTask UpdateIdentityProviderSaml(string alias, IdentityProviderEditableDetails details)
@@ -323,6 +324,7 @@ await provisioningManager.UpdateCentralIdentityProviderDataSAMLAsync(
details.Saml.ServiceProviderEntityId,
details.Saml.SingleSignOnServiceUrl))
.ConfigureAwait(false);
+ await portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);
}
private async ValueTask UpdateIdentityProviderShared(string alias, IdentityProviderEditableDetails details)
diff --git a/src/framework/Framework.ErrorHandling.Controller/Framework.ErrorHandling.Controller.csproj b/src/framework/Framework.ErrorHandling.Controller/Framework.ErrorHandling.Controller.csproj
index fe0c41b956..9fbb00a8ef 100644
--- a/src/framework/Framework.ErrorHandling.Controller/Framework.ErrorHandling.Controller.csproj
+++ b/src/framework/Framework.ErrorHandling.Controller/Framework.ErrorHandling.Controller.csproj
@@ -62,7 +62,6 @@
-
diff --git a/src/framework/Framework.Web/Framework.Web.csproj b/src/framework/Framework.Web/Framework.Web.csproj
index 5f8a277b99..1d5437f9e9 100644
--- a/src/framework/Framework.Web/Framework.Web.csproj
+++ b/src/framework/Framework.Web/Framework.Web.csproj
@@ -75,6 +75,8 @@
+
+
diff --git a/src/keycloak/Keycloak.Library/Components/KeycloakClient.cs b/src/keycloak/Keycloak.Library/Components/KeycloakClient.cs
index d279fa2e98..a92a4c90f4 100644
--- a/src/keycloak/Keycloak.Library/Components/KeycloakClient.cs
+++ b/src/keycloak/Keycloak.Library/Components/KeycloakClient.cs
@@ -30,15 +30,15 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library;
public partial class KeycloakClient
{
- public async Task CreateComponentAsync(string realm, Component componentRepresentation) =>
- await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None))
+ public async Task CreateComponentAsync(string realm, Component componentRepresentation, CancellationToken cancellationToken) =>
+ await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None))
.AppendPathSegment("/admin/realms/")
.AppendPathSegment(realm, true)
.AppendPathSegment("/components")
- .PostJsonAsync(componentRepresentation)
+ .PostJsonAsync(componentRepresentation, cancellationToken: cancellationToken)
.ConfigureAwait(ConfigureAwaitOptions.None);
- public async Task> GetComponentsAsync(string realm, string? name = null, string? parent = null, string? type = null)
+ public async Task> GetComponentsAsync(string realm, string? name = null, string? parent = null, string? type = null, CancellationToken cancellationToken = default)
{
var queryParams = new Dictionary
{
@@ -47,58 +47,30 @@ public async Task> GetComponentsAsync(string realm, strin
[nameof(type)] = type
};
- return await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None))
+ return await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None))
.AppendPathSegment("/admin/realms/")
.AppendPathSegment(realm, true)
.AppendPathSegment("/components")
.SetQueryParams(queryParams)
- .GetJsonAsync>()
+ .GetJsonAsync>(cancellationToken: cancellationToken)
.ConfigureAwait(ConfigureAwaitOptions.None);
}
- public async Task GetComponentAsync(string realm, string componentId) =>
- await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None))
+ public async Task UpdateComponentAsync(string realm, string componentId, Component componentRepresentation, CancellationToken cancellationToken) =>
+ await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None))
.AppendPathSegment("/admin/realms/")
.AppendPathSegment(realm, true)
.AppendPathSegment("/components/")
.AppendPathSegment(componentId, true)
- .GetJsonAsync()
+ .PutJsonAsync(componentRepresentation, cancellationToken: cancellationToken)
.ConfigureAwait(ConfigureAwaitOptions.None);
- public async Task UpdateComponentAsync(string realm, string componentId, Component componentRepresentation) =>
- await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None))
+ public async Task DeleteComponentAsync(string realm, string componentId, CancellationToken cancellationToken) =>
+ await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None))
.AppendPathSegment("/admin/realms/")
.AppendPathSegment(realm, true)
.AppendPathSegment("/components/")
.AppendPathSegment(componentId, true)
- .PutJsonAsync(componentRepresentation)
+ .DeleteAsync(cancellationToken: cancellationToken)
.ConfigureAwait(ConfigureAwaitOptions.None);
-
- public async Task DeleteComponentAsync(string realm, string componentId) =>
- await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None))
- .AppendPathSegment("/admin/realms/")
- .AppendPathSegment(realm, true)
- .AppendPathSegment("/components/")
- .AppendPathSegment(componentId, true)
- .DeleteAsync()
- .ConfigureAwait(ConfigureAwaitOptions.None);
-
- public async Task> GetSubcomponentTypesAsync(string realm, string componentId, string? type = null)
- {
- var queryParams = new Dictionary
- {
- [nameof(type)] = type
- };
-
- var result = await (await GetBaseUrlAsync(realm).ConfigureAwait(ConfigureAwaitOptions.None))
- .AppendPathSegment("/admin/realms/")
- .AppendPathSegment(realm, true)
- .AppendPathSegment("/components/")
- .AppendPathSegment(componentId, true)
- .AppendPathSegment("/sub-component-types")
- .SetQueryParams(queryParams)
- .GetJsonAsync>()
- .ConfigureAwait(ConfigureAwaitOptions.None);
- return result;
- }
}
diff --git a/src/keycloak/Keycloak.Library/Keycloak.Library.csproj b/src/keycloak/Keycloak.Library/Keycloak.Library.csproj
index b0e7f5a261..de9d357dce 100644
--- a/src/keycloak/Keycloak.Library/Keycloak.Library.csproj
+++ b/src/keycloak/Keycloak.Library/Keycloak.Library.csproj
@@ -31,6 +31,7 @@
net8.0
enable
enable
+ ef300f2e-b1c3-4ce1-b028-92533b71aa73
diff --git a/src/keycloak/Keycloak.Library/Localization/KeycloakClient.cs b/src/keycloak/Keycloak.Library/Localization/KeycloakClient.cs
new file mode 100644
index 0000000000..414ac2ed42
--- /dev/null
+++ b/src/keycloak/Keycloak.Library/Localization/KeycloakClient.cs
@@ -0,0 +1,87 @@
+/********************************************************************************
+ * 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 Flurl.Http;
+using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq;
+using System.Net.Http.Headers;
+
+namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library;
+
+public partial class KeycloakClient
+{
+ private const string AdminUrlSegment = "/admin/realms/";
+ private const string LocalizationUrlSegment = "/localization/";
+
+ public async Task> GetLocaleAsync(string realm, CancellationToken cancellationToken = default)
+ {
+ return await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None))
+ .AppendPathSegment(AdminUrlSegment)
+ .AppendPathSegment(realm, true)
+ .AppendPathSegment("/localization")
+ .GetJsonAsync>(cancellationToken: cancellationToken)
+ .ConfigureAwait(ConfigureAwaitOptions.None);
+ }
+
+ public async Task>> GetLocaleAsync(string realm, string locale, CancellationToken cancellationToken = default)
+ {
+ var response = await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None))
+ .AppendPathSegment(AdminUrlSegment)
+ .AppendPathSegment(realm, true)
+ .AppendPathSegment(LocalizationUrlSegment)
+ .AppendPathSegment(locale, true)
+ .GetJsonAsync?>(cancellationToken: cancellationToken)
+ .ConfigureAwait(ConfigureAwaitOptions.None);
+
+ return response == null
+ ? Enumerable.Empty>()
+ : response.FilterNotNull();
+ }
+
+ public async Task UpdateLocaleAsync(string realm, string locale, string key, string translation, CancellationToken cancellationToken)
+ {
+ using var content = new StringContent(translation, MediaTypeHeaderValue.Parse("text/plain"));
+ await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None))
+ .AppendPathSegment(AdminUrlSegment)
+ .AppendPathSegment(realm, true)
+ .AppendPathSegment(LocalizationUrlSegment)
+ .AppendPathSegment(locale, true)
+ .AppendPathSegment(key, true)
+ .PutAsync(content, cancellationToken: cancellationToken)
+ .ConfigureAwait(ConfigureAwaitOptions.None);
+ }
+
+ public async Task DeleteLocaleAsync(string realm, string locale, string key, CancellationToken cancellationToken) =>
+ await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None))
+ .AppendPathSegment(AdminUrlSegment)
+ .AppendPathSegment(realm, true)
+ .AppendPathSegment(LocalizationUrlSegment)
+ .AppendPathSegment(locale, true)
+ .AppendPathSegment(key, true)
+ .DeleteAsync(cancellationToken: cancellationToken)
+ .ConfigureAwait(ConfigureAwaitOptions.None);
+
+ public async Task DeleteLocaleAsync(string realm, string locale, CancellationToken cancellationToken) =>
+ await (await GetBaseUrlAsync(realm, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None))
+ .AppendPathSegment(AdminUrlSegment)
+ .AppendPathSegment(realm, true)
+ .AppendPathSegment(LocalizationUrlSegment)
+ .AppendPathSegment(locale, true)
+ .DeleteAsync(cancellationToken: cancellationToken)
+ .ConfigureAwait(ConfigureAwaitOptions.None);
+}
diff --git a/src/keycloak/Keycloak.Library/Models/Components/Component.cs b/src/keycloak/Keycloak.Library/Models/Components/Component.cs
index 301767263a..be06b6c2cf 100644
--- a/src/keycloak/Keycloak.Library/Models/Components/Component.cs
+++ b/src/keycloak/Keycloak.Library/Models/Components/Component.cs
@@ -31,17 +31,23 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Components
public class Component
{
[JsonPropertyName("id")]
- public string Id { get; set; }
+ public string? Id { get; set; }
+
[JsonPropertyName("name")]
- public string Name { get; set; }
+ public string? Name { get; set; }
+
[JsonPropertyName("providerId")]
- public string ProviderId { get; set; }
+ public string? ProviderId { get; set; }
+
[JsonPropertyName("providerType")]
- public string ProviderType { get; set; }
+ public string? ProviderType { get; set; }
+
[JsonPropertyName("parentId")]
- public string ParentId { get; set; }
+ public string? ParentId { get; set; }
+
[JsonPropertyName("config")]
- public Config Config { get; set; }
+ public IReadOnlyDictionary?>? Config { get; set; }
+
[JsonPropertyName("subType")]
- public string SubType { get; set; }
+ public string? SubType { get; set; }
}
diff --git a/src/keycloak/Keycloak.Library/Models/Components/Config.cs b/src/keycloak/Keycloak.Library/Models/Components/Config.cs
deleted file mode 100644
index 3497e32b34..0000000000
--- a/src/keycloak/Keycloak.Library/Models/Components/Config.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-/********************************************************************************
- * MIT License
- *
- * Copyright (c) 2019 Luk Vermeulen
- * Copyright (c) 2022 BMW Group AG
- * Copyright (c) 2022 Contributors to the Eclipse Foundation
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- ********************************************************************************/
-
-using System.Text.Json.Serialization;
-
-namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Components;
-
-public class Config
-{
- [JsonPropertyName("priority")]
- public IEnumerable Priority { get; set; }
- [JsonPropertyName("allowdefaultscopes")]
- public IEnumerable AllowDefaultScopes { get; set; }
- [JsonPropertyName("maxclients")]
- public IEnumerable MaxClients { get; set; }
- [JsonPropertyName("allowedprotocolmappertypes")]
- public IEnumerable AllowedProtocolMapperTypes { get; set; }
- [JsonPropertyName("algorithm")]
- public IEnumerable Algorithm { get; set; }
- [JsonPropertyName("hostsendingregistrationrequestmustmatch")]
- public IEnumerable HostSendingRegistrationRequestMustMatch { get; set; }
- [JsonPropertyName("clienturismustmatch")]
- public IEnumerable ClientUrisMustMatch { get; set; }
-}
diff --git a/src/keycloak/Keycloak.Library/Models/RealmsAdmin/Realm.cs b/src/keycloak/Keycloak.Library/Models/RealmsAdmin/Realm.cs
index cd311b5ed0..1fb29f7cb8 100644
--- a/src/keycloak/Keycloak.Library/Models/RealmsAdmin/Realm.cs
+++ b/src/keycloak/Keycloak.Library/Models/RealmsAdmin/Realm.cs
@@ -176,4 +176,8 @@ public class Realm
public bool? UserManagedAccessAllowed { get; set; }
[JsonPropertyName("passwordPolicy")]
public string? PasswordPolicy { get; set; }
+ [JsonPropertyName("defaultLocale")]
+ public string? DefaultLocale { get; set; }
+ [JsonPropertyName("localizationTexts")]
+ public IDictionary?>? LocalizationTexts { get; set; }
}
diff --git a/src/keycloak/Keycloak.Library/Models/Root/Locale.cs b/src/keycloak/Keycloak.Library/Models/Root/Locale.cs
index f332520d7c..d474a7fba6 100644
--- a/src/keycloak/Keycloak.Library/Models/Root/Locale.cs
+++ b/src/keycloak/Keycloak.Library/Models/Root/Locale.cs
@@ -34,5 +34,8 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Root;
public enum Locale
{
[EnumMember(Value = "en")]
- En
+ En,
+
+ [EnumMember(Value = "de")]
+ De
}
diff --git a/src/keycloak/Keycloak.Library/Models/Users/UserProfileConfig.cs b/src/keycloak/Keycloak.Library/Models/Users/UserProfileConfig.cs
new file mode 100644
index 0000000000..b6e14dee85
--- /dev/null
+++ b/src/keycloak/Keycloak.Library/Models/Users/UserProfileConfig.cs
@@ -0,0 +1,155 @@
+/********************************************************************************
+ * 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
+ ********************************************************************************/
+
+namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Users;
+
+public sealed class UserProfileConfig : IEquatable
+{
+ public IEnumerable Attributes { get; set; } = null!;
+
+ public IEnumerable Groups { get; set; } = null!;
+
+ public bool Equals(UserProfileConfig? other) =>
+ other is not null &&
+ Attributes.OrderBy(x => x.Name).SequenceEqual(other.Attributes.OrderBy(x => x.Name)) &&
+ Groups.OrderBy(x => x.Name).SequenceEqual(other.Groups.OrderBy(x => x.Name));
+
+ public override bool Equals(object? obj) =>
+ obj is not null && obj.GetType() == this.GetType() && Equals((UserProfileConfig)obj);
+
+ public override int GetHashCode() =>
+ HashCode.Combine(Attributes, Groups);
+}
+
+public sealed class ProfileAttribute : IEquatable
+{
+ public string Name { get; set; } = null!;
+ public string DisplayName { get; set; } = null!;
+ public object? Validations { get; set; }
+ public object? Annotations { get; set; }
+ public ProfileAttributeRequired? Required { get; set; }
+ public ProfileAttributePermission Permissions { get; set; } = null!;
+ public ProfileAttributeSelector? Selector { get; set; }
+ public string Group { get; set; } = null!;
+ public bool Multivalued { get; set; }
+
+ public bool Equals(ProfileAttribute? other) =>
+ other is not null &&
+ Name == other.Name &&
+ DisplayName == other.DisplayName &&
+ Equals(Validations, other.Validations) &&
+ Equals(Annotations, other.Annotations) &&
+ Equals(Required, other.Required) &&
+ Permissions.Equals(other.Permissions) &&
+ Equals(Selector, other.Selector) &&
+ Group == other.Group &&
+ Multivalued == other.Multivalued;
+
+ public override bool Equals(object? obj) =>
+ obj is not null && obj.GetType() == this.GetType() && Equals((ProfileAttribute)obj);
+
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+ hashCode.Add(Name);
+ hashCode.Add(DisplayName);
+ hashCode.Add(Validations);
+ hashCode.Add(Annotations);
+ hashCode.Add(Required);
+ hashCode.Add(Permissions);
+ hashCode.Add(Selector);
+ hashCode.Add(Group);
+ hashCode.Add(Multivalued);
+ return hashCode.ToHashCode();
+ }
+}
+
+public sealed class ProfileAttributeRequired : IEquatable
+{
+ public IEnumerable? Roles { get; init; } = null!;
+ public IEnumerable? Scopes { get; init; } = null!;
+
+ public bool Equals(ProfileAttributeRequired? other) =>
+ other is not null &&
+ ((Roles == null && other.Roles == null) ||
+ Roles != null && other.Roles != null && Roles.Order().SequenceEqual(other.Roles.Order())) &&
+ ((Scopes == null && other.Scopes == null) || Scopes != null && other.Scopes != null && Scopes.Order().SequenceEqual(other.Scopes.Order()));
+
+ public override bool Equals(object? obj) =>
+ obj is not null && obj.GetType() == this.GetType() && Equals((ProfileAttributeRequired)obj);
+
+ public override int GetHashCode() => HashCode.Combine(Roles, Scopes);
+}
+
+public sealed class ProfileAttributePermission : IEquatable
+{
+ public IEnumerable View { get; init; } = null!;
+ public IEnumerable Edit { get; init; } = null!;
+
+ public bool Equals(ProfileAttributePermission? other) =>
+ other is not null &&
+ View.Order().SequenceEqual(other.View.Order()) &&
+ Edit.Order().SequenceEqual(other.Edit.Order());
+
+ public override bool Equals(object? obj) =>
+ obj is not null && obj.GetType() == this.GetType() && Equals((ProfileAttributePermission)obj);
+
+ public override int GetHashCode() =>
+ HashCode.Combine(View, Edit);
+}
+
+public sealed class ProfileAttributeSelector : IEquatable
+{
+ public IEnumerable Attributes { get; init; } = null!;
+ public IEnumerable Groups { get; init; } = null!;
+ public object? UnmanagedAttributePolicy { get; init; }
+
+ public bool Equals(ProfileAttributeSelector? other) =>
+ other is not null &&
+ Attributes.OrderBy(x => x.Name).SequenceEqual(other.Attributes.OrderBy(x => x.Name)) &&
+ Groups.OrderBy(x => x.Name).SequenceEqual(other.Groups.OrderBy(x => x.Name)) &&
+ Equals(UnmanagedAttributePolicy, other.UnmanagedAttributePolicy);
+
+ public override bool Equals(object? obj) =>
+ obj is not null && obj.GetType() == this.GetType() && Equals((ProfileAttributeSelector)obj);
+
+ public override int GetHashCode() =>
+ HashCode.Combine(Attributes, Groups, UnmanagedAttributePolicy);
+}
+
+public sealed class ProfileGroup : IEquatable
+{
+ public string Name { get; init; } = null!;
+ public string DisplayHeader { get; init; } = null!;
+ public string DisplayDescription { get; init; } = null!;
+ public IEnumerable