This is an automated email from the ASF dual-hosted git repository.

riemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new bc56f6a466 feat(#3717): Support external role assignments for OAuth 
providers (#3718)
bc56f6a466 is described below

commit bc56f6a466ff21236654737082a4ea24bdadbcc2
Author: Dominik Riemer <[email protected]>
AuthorDate: Thu Aug 7 08:23:21 2025 +0200

    feat(#3717): Support external role assignments for OAuth providers (#3718)
---
 .../environment/model/OAuthConfiguration.java      |   9 +
 .../parser/OAuthConfigurationParser.java           |   3 +-
 .../streampipes/model/client/user/UserAccount.java |   9 +
 .../service/core/oauth2/UserService.java           |  59 +++-
 .../src/lib/model/gen/streampipes-model-client.ts  |   5 +-
 .../edit-user-dialog.component.html                |  45 +--
 .../edit-user-dialog/edit-user-dialog.component.ts | 313 +++++++++++----------
 .../user-group-configuration.component.html        |   9 +
 .../user-group-configuration.component.ts          |   2 +-
 9 files changed, 276 insertions(+), 178 deletions(-)

diff --git 
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/model/OAuthConfiguration.java
 
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/model/OAuthConfiguration.java
index 7ab566f460..607bf468b5 100644
--- 
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/model/OAuthConfiguration.java
+++ 
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/model/OAuthConfiguration.java
@@ -34,6 +34,7 @@ public class OAuthConfiguration {
   private String userInfoUri;
   private String emailAttributeName;
   private String userIdAttributeName;
+  private String roleAttributeName;
 
 
   public String getRegistrationId() {
@@ -136,6 +137,14 @@ public class OAuthConfiguration {
     return userIdAttributeName;
   }
 
+  public String getRoleAttributeName() {
+    return roleAttributeName;
+  }
+
+  public void setRoleAttributeName(String roleAttributeName) {
+    this.roleAttributeName = roleAttributeName;
+  }
+
   public void setUserIdAttributeName(String userIdAttributeName) {
     this.userIdAttributeName = userIdAttributeName;
   }
diff --git 
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/parser/OAuthConfigurationParser.java
 
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/parser/OAuthConfigurationParser.java
index 48168566d2..3650a06104 100644
--- 
a/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/parser/OAuthConfigurationParser.java
+++ 
b/streampipes-commons/src/main/java/org/apache/streampipes/commons/environment/parser/OAuthConfigurationParser.java
@@ -75,7 +75,7 @@ public class OAuthConfigurationParser {
   ) {
     var parts = getParts(key);
     if (parts.length >= 5) {
-      // containst the identifier of the provider (e.g. azure, github, ...)
+      // contains the identifier of the provider (e.g. azure, github, ...)
       var registrationId = getRegistrationId(parts);
       var settingName = getSettingName(parts);
 
@@ -95,6 +95,7 @@ public class OAuthConfigurationParser {
         case "USER_INFO_URI" -> oAuthConfiguration.setUserInfoUri(value);
         case "EMAIL_ATTRIBUTE_NAME" -> 
oAuthConfiguration.setEmailAttributeName(value);
         case "USER_ID_ATTRIBUTE_NAME" -> 
oAuthConfiguration.setUserIdAttributeName(value);
+        case "ROLE_ATTRIBUTE_NAME" -> 
oAuthConfiguration.setRoleAttributeName(value);
         case "NAME" -> oAuthConfiguration.setRegistrationName(value);
         default -> LOG.warn(
             "Unknown setting {} for oauth configuration in environment 
variable {}",
diff --git 
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
 
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
index 98bcfbca5c..6df8e62555 100644
--- 
a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
+++ 
b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserAccount.java
@@ -45,6 +45,7 @@ public class UserAccount extends Principal {
    * The authentication provider (LOCAL or one of the configured OAuth 
providers
    */
   protected String provider;
+  protected boolean externallyManagedRoles = false;
 
   public UserAccount() {
     super(PrincipalType.USER_ACCOUNT);
@@ -172,4 +173,12 @@ public class UserAccount extends Principal {
   public void setProvider(String provider) {
     this.provider = provider;
   }
+
+  public boolean isExternallyManagedRoles() {
+    return externallyManagedRoles;
+  }
+
+  public void setExternallyManagedRoles(boolean externallyManagedRoles) {
+    this.externallyManagedRoles = externallyManagedRoles;
+  }
 }
diff --git 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/UserService.java
 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/UserService.java
index c861c278a6..956a99778d 100755
--- 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/UserService.java
+++ 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/oauth2/UserService.java
@@ -20,6 +20,7 @@ package org.apache.streampipes.service.core.oauth2;
 
 import org.apache.streampipes.commons.environment.Environment;
 import org.apache.streampipes.commons.environment.Environments;
+import org.apache.streampipes.commons.environment.model.OAuthConfiguration;
 import org.apache.streampipes.model.client.user.DefaultRole;
 import org.apache.streampipes.model.client.user.UserAccount;
 import org.apache.streampipes.resource.management.UserResourceManager;
@@ -31,8 +32,11 @@ import 
org.springframework.security.oauth2.core.oidc.OidcIdToken;
 import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
 
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 public class UserService {
@@ -74,10 +78,15 @@ public class UserService {
               String.format("Already signed up with another provider %s", 
user.getProvider())
           );
         }
+        applyRoles(user, oAuthConfig, attributes);
+        userStorage.updateUser(user);
       } else {
-        new 
UserResourceManager().registerOauthUser(toUserAccount(registrationId, 
principalId, email, fullName));
-        user = (UserAccount) userStorage.getUserById(principalId);
+        user = toUserAccount(registrationId, principalId, email, fullName);
+        applyRoles(user, oAuthConfig, attributes);
+        new UserResourceManager().registerOauthUser(user);
       }
+
+      user = (UserAccount) userStorage.getUserById(principalId);
       return OidcUserAccountDetails.create(user, attributes, idToken, 
userInfo);
     } else {
       throw new OAuth2AuthenticationProcessingException(
@@ -86,12 +95,37 @@ public class UserService {
     }
   }
 
+  private void applyRoles(UserAccount user,
+                          OAuthConfiguration oAuthConfig,
+                          Map<String, Object> attributes) {
+    if (oAuthConfig.getRoleAttributeName() != null) {
+      Object rolesObject = attributes.get(oAuthConfig.getRoleAttributeName());
+
+      if (rolesObject instanceof List<?> rolesList) {
+        Set<String> roles = extractRoleOrGroup("ROLE", rolesList);
+        Set<String> groups = convertGroup(extractRoleOrGroup("GROUP", 
rolesList));
+
+        user.setRoles(roles);
+        user.setGroups(groups);
+        user.setExternallyManagedRoles(true);
+      } else {
+        applyDefaultRole(user);
+      }
+    } else {
+      applyDefaultRole(user);
+    }
+  }
+
+  private void applyDefaultRole(UserAccount user) {
+    
user.setRoles(Stream.of(DefaultRole.ROLE_ADMIN.toString()).collect(Collectors.toSet()));
+    user.setExternallyManagedRoles(false);
+  }
+
   private UserAccount toUserAccount(String registrationId,
                                     String principalId,
                                     String email,
                                     Object fullName) {
-    var roles = Stream.of(DefaultRole.ROLE_ADMIN.toString()).toList();
-    var user = UserAccount.from(email, null, new HashSet<>(roles));
+    var user = UserAccount.from(email, null, new HashSet<>());
     user.setPrincipalId(principalId);
     if (Objects.nonNull(fullName)) {
       user.setFullName(fullName.toString());
@@ -100,4 +134,21 @@ public class UserService {
     user.setProvider(registrationId);
     return user;
   }
+
+  private Set<String> extractRoleOrGroup(String type,
+                                         List<?> roles) {
+    return roles.stream()
+        .filter(role -> role instanceof String)
+        .filter(role -> ((String) role).startsWith(type))
+        .map(role -> (String) role)
+        .collect(Collectors.toSet());
+  }
+
+  private Set<String> convertGroup(Set<String> groups) {
+    return groups.stream()
+        .map(group -> group.split("_"))
+        .filter(parts -> parts.length == 2)
+        .map(parts -> parts[1])
+        .collect(Collectors.toSet());
+  }
 }
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
index a865b8ea10..f6d00886bf 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model-client.ts
@@ -16,10 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2025-02-24 
11:25:02.
+// Generated using typescript-generator version 3.2.1263 on 2025-08-05 
18:37:35.
 
 import { Storable } from './streampipes-model';
 
@@ -231,6 +232,7 @@ export class ServiceAccount extends Principal {
 
 export class UserAccount extends Principal {
     darkMode: boolean;
+    externallyManagedRoles: boolean;
     fullName: string;
     hideTutorial: boolean;
     password: string;
@@ -247,6 +249,7 @@ export class UserAccount extends Principal {
         const instance = target || new UserAccount();
         super.fromData(data, instance);
         instance.darkMode = data.darkMode;
+        instance.externallyManagedRoles = data.externallyManagedRoles;
         instance.fullName = data.fullName;
         instance.hideTutorial = data.hideTutorial;
         instance.password = data.password;
diff --git 
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
 
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
index 72687566c2..d46527b426 100644
--- 
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
+++ 
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
@@ -20,7 +20,7 @@
     <div class="sp-dialog-content">
         <div fxFlex="100" fxLayout="column" class="p-15">
             <sp-warning-box *ngIf="isUserAccount && isExternalProvider">
-                Some settings of externally-managed users cannot be changed.
+                Settings of externally-managed users cannot be changed.
             </sp-warning-box>
             <form [formGroup]="parentForm" fxFlex="100" fxLayout="column">
                 <div class="general-options-panel" fxLayout="column">
@@ -123,6 +123,9 @@
                     <span class="general-options-header">Groups</span>
                     <mat-checkbox
                         *ngFor="let group of availableGroups"
+                        [disabled]="
+                            isExternalProvider && isExternallyManagedRoles
+                        "
                         [value]="group.groupId"
                         [checked]="user.groups.indexOf(group.groupId) > -1"
                         (change)="changeGroupAssignment($event)"
@@ -135,6 +138,9 @@
                     <mat-checkbox
                         *ngFor="let role of availableRoles$ | async"
                         [value]="role.elementId"
+                        [disabled]="
+                            isExternalProvider && isExternallyManagedRoles
+                        "
                         [checked]="user.roles.indexOf(role.elementId) > -1"
                         (change)="changeRoleAssignment($event)"
                         [attr.data-cy]="'role-' + role.elementId"
@@ -142,21 +148,23 @@
                         {{ role.label }}
                     </mat-checkbox>
                 </div>
-                <div fxLayout="column" class="general-options-panel">
-                    <span class="general-options-header">Account</span>
-                    <mat-checkbox
-                        formControlName="accountEnabled"
-                        data-cy="new-user-enabled"
-                    >
-                        Enabled
-                    </mat-checkbox>
-                    <mat-checkbox
-                        formControlName="accountLocked"
-                        data-cy="new-user-locked"
-                    >
-                        Locked
-                    </mat-checkbox>
-                </div>
+                @if (!isExternalProvider) {
+                    <div fxLayout="column" class="general-options-panel">
+                        <span class="general-options-header">Account</span>
+                        <mat-checkbox
+                            formControlName="accountEnabled"
+                            data-cy="new-user-enabled"
+                        >
+                            Enabled
+                        </mat-checkbox>
+                        <mat-checkbox
+                            formControlName="accountLocked"
+                            data-cy="new-user-locked"
+                        >
+                            Locked
+                        </mat-checkbox>
+                    </div>
+                }
             </form>
         </div>
     </div>
@@ -170,7 +178,10 @@
                     color="accent"
                     (click)="save()"
                     style="margin-right: 10px"
-                    [disabled]="!parentForm.valid"
+                    [disabled]="
+                        parentForm.invalid ||
+                        (isExternalProvider && isExternallyManagedRoles)
+                    "
                     data-cy="sp-element-edit-user-save"
                 >
                     <i class="material-icons">save</i><span>&nbsp;Save</span>
diff --git 
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
 
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
index 885a89b8d0..a6ff998ed0 100644
--- 
a/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
+++ 
b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
@@ -59,7 +59,8 @@ export class EditUserDialogComponent implements OnInit {
     editMode: boolean;
 
     isUserAccount: boolean;
-    isExternalProvider: boolean = false;
+    isExternalProvider = false;
+    isExternallyManagedRoles = false;
     parentForm: UntypedFormGroup;
     clonedUser: UserAccount | ServiceAccount;
 
@@ -85,138 +86,163 @@ export class EditUserDialogComponent implements OnInit {
     ) {}
 
     ngOnInit(): void {
-        const filterObject =
+        this.initRoleFilter();
+        this.loadInitialData();
+        this.cloneUser();
+        this.initForm();
+        this.handleFormChanges();
+    }
+
+    save() {
+        this.registrationError = undefined;
+
+        const saveCallback = () => this.close(true);
+        const errorCallback = (error: any) => {
+            this.registrationError = error.error.notifications
+                ? error.error.notifications[0].title
+                : 'Unknown error';
+        };
+
+        if (this.editMode) {
+            const update$ = this.isUserAccount
+                ? this.userService.updateUser(this.clonedUser as UserAccount)
+                : this.userService.updateService(
+                      this.clonedUser as ServiceAccount,
+                  );
+
+            update$.subscribe(() => {
+                if (this.emailChanged) {
+                    this.authService.logout();
+                    this.close(false);
+                    this.router.navigate(['login']);
+                } else {
+                    saveCallback();
+                }
+            });
+        } else {
+            const create$ = this.isUserAccount
+                ? this.userService.createUser(this.clonedUser as UserAccount)
+                : this.userService.createServiceAccount(
+                      this.clonedUser as ServiceAccount,
+                  );
+
+            create$.subscribe(saveCallback, errorCallback);
+        }
+    }
+
+    private initRoleFilter(): void {
+        const filterRole =
             this.user instanceof UserAccount
                 ? UserRole.ROLE_SERVICE_ADMIN
                 : UserRole.ROLE_ADMIN;
+
         this.availableRoles$ = this.availableRolesService
             .getAvailableRoles()
             .pipe(
                 map(roles =>
                     roles
-                        .filter(role => role.elementId !== filterObject)
+                        .filter(role => role.elementId !== filterRole)
                         .sort((a, b) => a.label.localeCompare(b.label)),
                 ),
             );
-        this.mailConfigService
-            .getMailConfig()
-            .subscribe(
-                config => (this.emailConfigured = config.emailConfigured),
-            );
-        this.userGroupService.getAllUserGroups().subscribe(response => {
-            this.availableGroups = response;
+    }
+
+    private loadInitialData(): void {
+        this.mailConfigService.getMailConfig().subscribe(config => {
+            this.emailConfigured = config.emailConfigured;
         });
-        this.clonedUser =
-            this.user instanceof UserAccount
-                ? UserAccount.fromData(this.user, new UserAccount())
-                : ServiceAccount.fromData(this.user, new ServiceAccount());
-        this.isUserAccount = this.user instanceof UserAccount;
+
+        this.userGroupService.getAllUserGroups().subscribe(groups => {
+            this.availableGroups = groups;
+        });
+    }
+
+    private cloneUser(): void {
+        const isUserAccount = this.user instanceof UserAccount;
+
+        this.isUserAccount = isUserAccount;
         this.isExternalProvider =
-            this.user instanceof UserAccount && this.user.provider !== 'local';
-        this.parentForm = this.fb.group({});
-        let usernameValidators = [];
-        if (this.isUserAccount) {
-            if ((this.clonedUser as UserAccount).provider === 'local') {
-                usernameValidators = [Validators.required, Validators.email];
-            } else {
-                usernameValidators = [Validators.email];
-            }
-        } else {
-            usernameValidators = [Validators.required];
+            isUserAccount && this.user.provider !== 'local';
+        this.isExternallyManagedRoles =
+            isUserAccount && this.user.externallyManagedRoles;
+
+        this.clonedUser = isUserAccount
+            ? UserAccount.fromData(this.user, new UserAccount())
+            : ServiceAccount.fromData(this.user, new ServiceAccount());
+    }
+
+    private initForm(): void {
+        const form: Record<string, any> = {};
+
+        form['username'] = [
+            this.clonedUser.username,
+            this.getUsernameValidators(),
+        ];
+
+        if (!this.isExternalProvider) {
+            form['accountEnabled'] = [this.clonedUser.accountEnabled];
+            form['accountLocked'] = [this.clonedUser.accountLocked];
         }
-        this.parentForm.addControl(
-            'username',
-            new UntypedFormControl(this.clonedUser.username),
-        );
-        this.parentForm.controls['username'].setValidators(usernameValidators);
-        this.parentForm.addControl(
-            'accountEnabled',
-            new UntypedFormControl(this.clonedUser.accountEnabled),
-        );
-        this.parentForm.addControl(
-            'accountLocked',
-            new UntypedFormControl(this.clonedUser.accountLocked),
-        );
+
         if (this.clonedUser instanceof UserAccount) {
-            this.parentForm.addControl(
-                'fullName',
-                new UntypedFormControl(this.clonedUser.fullName),
-            );
+            form['fullName'] = [this.clonedUser.fullName];
         } else {
-            this.parentForm.addControl(
-                'clientSecret',
-                new UntypedFormControl(this.clonedUser.clientSecret, [
-                    Validators.required,
-                    Validators.minLength(35),
-                ]),
-            );
+            form['clientSecret'] = [
+                this.clonedUser.clientSecret,
+                [Validators.required, Validators.minLength(35)],
+            ];
         }
 
         if (!this.editMode && this.clonedUser instanceof UserAccount) {
-            this.parentForm.addControl(
-                'password',
-                new UntypedFormControl(
-                    this.clonedUser.password,
-                    Validators.required,
-                ),
-            );
-            this.parentForm.addControl(
-                'repeatPassword',
-                new UntypedFormControl(),
-            );
-            this.parentForm.addControl(
-                'sendPasswordToUser',
-                new UntypedFormControl(this.sendPasswordToUser),
-            );
-            this.parentForm.setValidators(this.checkPasswords);
+            form['password'] = [this.clonedUser.password, Validators.required];
+            form['repeatPassword'] = [''];
+            form['sendPasswordToUser'] = [this.sendPasswordToUser];
         }
 
+        this.parentForm = this.fb.group(form, {
+            validators:
+                this.editMode || !this.isUserAccount
+                    ? null
+                    : this.checkPasswords,
+        });
+
         if (this.isExternalProvider) {
-            this.parentForm.controls['username'].disable();
-            this.parentForm.controls['fullName'].disable();
+            this.parentForm.get('username')?.disable();
+            this.parentForm.get('fullName')?.disable();
         }
+    }
 
+    private handleFormChanges(): void {
         this.parentForm.valueChanges.subscribe(v => {
-            this.clonedUser.username = v.username;
-            this.clonedUser.accountLocked = v.accountLocked;
-            this.clonedUser.accountEnabled = v.accountEnabled;
+            const raw = this.parentForm.getRawValue();
+            if (!this.isUserAccount || !this.isExternalProvider) {
+                this.clonedUser.username = v.username;
+            }
+
+            if (!this.isExternalProvider) {
+                this.clonedUser.accountLocked = raw.accountLocked;
+                this.clonedUser.accountEnabled = raw.accountEnabled;
+            }
+
             if (this.clonedUser instanceof UserAccount) {
                 this.emailChanged =
                     this.clonedUser.username !== this.user.username &&
                     this.user.username ===
                         this.currentUserService.getCurrentUser().username &&
                     this.editMode;
-                this.clonedUser.fullName = v.fullName;
+
+                if (!this.isExternalProvider) {
+                    this.clonedUser.fullName = v.fullName;
+                }
+
                 if (!this.editMode) {
                     this.sendPasswordToUser = v.sendPasswordToUser;
+
                     if (this.sendPasswordToUser) {
-                        if (this.parentForm.controls['password']) {
-                            this.parentForm.removeControl('password');
-                            this.parentForm.removeControl('repeatPassword');
-                            this.parentForm.removeValidators(
-                                this.checkPasswords,
-                            );
-                            this.clonedUser.password = undefined;
-                        }
+                        this.removePasswordControls();
                     } else {
-                        if (!this.parentForm.controls['password']) {
-                            this.parentForm.addControl(
-                                'password',
-                                new UntypedFormControl(
-                                    this.clonedUser.password,
-                                    Validators.required,
-                                ),
-                            );
-                            this.parentForm.addControl(
-                                'repeatPassword',
-                                new UntypedFormControl(),
-                            );
-                            this.parentForm.setValidators(this.checkPasswords);
-                        }
+                        this.addPasswordControlsIfMissing();
                         this.clonedUser.password = v.password;
-                        this.parentForm.controls.password.setValidators(
-                            Validators.required,
-                        );
                     }
                 }
             } else {
@@ -228,6 +254,42 @@ export class EditUserDialogComponent implements OnInit {
         });
     }
 
+    private removePasswordControls(): void {
+        this.parentForm.removeControl('password');
+        this.parentForm.removeControl('repeatPassword');
+        this.parentForm.clearValidators();
+        if (this.clonedUser instanceof UserAccount) {
+            this.clonedUser.password = undefined;
+        }
+    }
+
+    private addPasswordControlsIfMissing(): void {
+        if (!this.parentForm.get('password')) {
+            this.parentForm.addControl(
+                'password',
+                new UntypedFormControl('', Validators.required),
+            );
+            this.parentForm.addControl(
+                'repeatPassword',
+                new UntypedFormControl(),
+            );
+            this.parentForm.setValidators(this.checkPasswords);
+        }
+    }
+
+    private getUsernameValidators(): ValidatorFn[] {
+        if (this.isUserAccount) {
+            return this.user.provider === 'local'
+                ? [Validators.required, Validators.email]
+                : [Validators.email];
+        }
+        return [Validators.required];
+    }
+
+    close(refresh: boolean) {
+        this.dialogRef.close(refresh);
+    }
+
     checkPasswords: ValidatorFn = (
         group: AbstractControl,
     ): ValidationErrors | null => {
@@ -240,63 +302,6 @@ export class EditUserDialogComponent implements OnInit {
         return pass.value === confirmPass.value ? null : { notMatching: true };
     };
 
-    save() {
-        this.registrationError = undefined;
-        if (this.editMode) {
-            if (this.isUserAccount) {
-                this.userService
-                    .updateUser(this.clonedUser as UserAccount)
-                    .subscribe(() => {
-                        if (this.emailChanged) {
-                            this.authService.logout();
-                            this.close(false);
-                            this.router.navigate(['login']);
-                        } else {
-                            this.close(true);
-                        }
-                    });
-            } else {
-                this.userService
-                    .updateService(this.clonedUser as ServiceAccount)
-                    .subscribe(() => {
-                        this.close(true);
-                    });
-            }
-        } else {
-            if (this.isUserAccount) {
-                this.userService
-                    .createUser(this.clonedUser as UserAccount)
-                    .subscribe(
-                        () => {
-                            this.close(true);
-                        },
-                        error => {
-                            this.registrationError = error.error.notifications
-                                ? error.error.notifications[0].title
-                                : 'Unknown error';
-                        },
-                    );
-            } else {
-                this.userService
-                    .createServiceAccount(this.clonedUser as ServiceAccount)
-                    .subscribe(
-                        () => {
-                            this.close(true);
-                        },
-                        error => {
-                            this.registrationError = error.error.notifications
-                                ? error.error.notifications[0].title
-                                : 'Unknown error';
-                        },
-                    );
-            }
-        }
-    }
-
-    close(refresh: boolean) {
-        this.dialogRef.close(refresh);
-    }
-
     changeGroupAssignment(event: MatCheckboxChange) {
         if (this.clonedUser.groups.indexOf(event.source.value) > -1) {
             this.clonedUser.groups.splice(
diff --git 
a/ui/src/app/configuration/security-configuration/user-group-configuration/user-group-configuration.component.html
 
b/ui/src/app/configuration/security-configuration/user-group-configuration/user-group-configuration.component.html
index d3d3f60b7e..97f186e049 100644
--- 
a/ui/src/app/configuration/security-configuration/user-group-configuration/user-group-configuration.component.html
+++ 
b/ui/src/app/configuration/security-configuration/user-group-configuration/user-group-configuration.component.html
@@ -45,6 +45,15 @@
                 </td>
             </ng-container>
 
+            <ng-container matColumnDef="groupId">
+                <th mat-header-cell mat-sort-header *matHeaderCellDef>
+                    Group ID
+                </th>
+                <td mat-cell *matCellDef="let group">
+                    {{ group.elementId }}
+                </td>
+            </ng-container>
+
             <ng-container matColumnDef="edit">
                 <th mat-header-cell *matHeaderCellDef class="text-right">
                     Actions
diff --git 
a/ui/src/app/configuration/security-configuration/user-group-configuration/user-group-configuration.component.ts
 
b/ui/src/app/configuration/security-configuration/user-group-configuration/user-group-configuration.component.ts
index e8bc605135..b00799cc6f 100644
--- 
a/ui/src/app/configuration/security-configuration/user-group-configuration/user-group-configuration.component.ts
+++ 
b/ui/src/app/configuration/security-configuration/user-group-configuration/user-group-configuration.component.ts
@@ -36,7 +36,7 @@ import { MatDialog } from '@angular/material/dialog';
 export class SecurityUserGroupConfigComponent implements OnInit {
     dataSource: MatTableDataSource<Group>;
 
-    displayedColumns: string[] = ['groupName', 'edit'];
+    displayedColumns: string[] = ['groupName', 'groupId', 'edit'];
 
     constructor(
         private userGroupService: UserGroupService,

Reply via email to