You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2021/10/04 20:03:03 UTC

[incubator-streampipes] branch STREAMPIPES-426 updated: [STREAMPIPES-439] Add initial UI to manage users and roles

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

riemer pushed a commit to branch STREAMPIPES-426
in repository https://gitbox.apache.org/repos/asf/incubator-streampipes.git


The following commit(s) were added to refs/heads/STREAMPIPES-426 by this push:
     new 4eb1de9  [STREAMPIPES-439] Add initial UI to manage users and roles
4eb1de9 is described below

commit 4eb1de9d2dfcda13ae86e00018318fd42f17351d
Author: Dominik Riemer <ri...@fzi.de>
AuthorDate: Mon Oct 4 22:02:52 2021 +0200

    [STREAMPIPES-439] Add initial UI to manage users and roles
---
 .../backend/StreamPipesResourceConfig.java         |   2 +
 .../streampipes/model/client/user/Group.java       |  51 +++--
 .../streampipes/model/client/user/Principal.java   |  12 +-
 .../streampipes/model/client/user/Privilege.java   |  77 +++++++
 .../model/client/user/ServiceAccount.java          |  10 +-
 .../streampipes/model/client/user/UserAccount.java |   5 +-
 .../streampipes/model/client/user/UserInfo.java    |  11 +-
 .../manager/setup/CouchDbInstallationStep.java     |   4 +-
 .../setup/PipelineElementInstallationStep.java     |   8 +-
 .../manager/storage/UserManagementService.java     |   4 +-
 .../streampipes/manager/storage/UserService.java   |  30 +--
 .../streampipes/rest/impl/UserGroupResource.java   |  67 ++++++
 .../apache/streampipes/rest/impl/UserProfile.java  |  76 +------
 .../apache/streampipes/rest/impl/UserResource.java | 224 +++++++++++++++++++++
 .../streampipes/storage/api/INoSqlStorage.java     |   2 +
 .../streampipes/storage/api/IUserGroupStorage.java |   5 +-
 .../streampipes/storage/api/IUserStorage.java      |  10 +-
 .../storage/couchdb/CouchDbStorageManager.java     |   5 +
 .../storage/couchdb/impl/UserGroupStorageImpl.java |  58 ++++++
 .../storage/couchdb/impl/UserStorage.java          | 191 ++++++++++--------
 .../streampipes/storage/couchdb/utils/Utils.java   |   4 +
 .../user/management/jwt/JwtTokenProvider.java      |   4 +-
 .../user/management/jwt/SpKeyResolver.java         |   4 +-
 ui/src/app/configuration/configuration.module.ts   |  13 +-
 .../messaging-configuration.component.html         | 141 +++++++------
 .../abstract-security-principal-config.ts          |  92 +++++++++
 .../edit-user-dialog.component.html                |  89 ++++++++
 .../edit-user-dialog.component.scss                |  10 +-
 .../edit-user-dialog/edit-user-dialog.component.ts | 141 +++++++++++++
 .../security-configuration.component.html          |  18 ++
 .../security-configuration.component.ts            |   3 +
 .../security-service-config.component.html         |  77 +++++++
 .../security-service-config.component.scss         |   5 +-
 .../security-service-config.component.ts           |  47 +++++
 .../security-user-config.component.html            |  94 +++++++++
 .../security-user-config.component.scss            |   5 +-
 .../security-user-config.component.ts}             |  31 ++-
 .../app/core-model/gen/streampipes-model-client.ts |  30 ++-
 .../split-section/split-section.component.scss     |   2 +
 .../core/components/toolbar/toolbar.component.ts   |   2 +-
 ui/src/app/platform-services/apis/user.service.ts  |  70 +++++++
 ui/src/app/platform-services/platform.module.ts    |   4 +-
 .../profile/components/basic-profile-settings.ts   |   6 +-
 .../general/general-profile-settings.component.ts  |   6 +-
 .../token/token-management-settings.component.ts   |  12 +-
 ui/src/app/profile/profile.service.ts              |  17 +-
 ui/src/app/services/auth.service.ts                |   5 -
 ui/src/scss/sp/widgets.scss                        |  14 ++
 48 files changed, 1443 insertions(+), 355 deletions(-)

diff --git a/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java b/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
index 14d92a1..8448996 100644
--- a/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
+++ b/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
@@ -93,6 +93,7 @@ public class StreamPipesResourceConfig extends ResourceConfig {
         register(Setup.class);
         register(ResetResource.class);
         register(UserProfile.class);
+        register(UserResource.class);
         register(Version.class);
         register(PipelineElementAsset.class);
         register(DataLakeDashboardResource.class);
@@ -104,6 +105,7 @@ public class StreamPipesResourceConfig extends ResourceConfig {
         register(DashboardWidget.class);
         register(Dashboard.class);
         register(VisualizablePipelineResource.class);
+        register(UserGroupResource.class);
 
         // Serializers
         register(GsonWithIdProvider.class);
diff --git a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IUserStorage.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Group.java
similarity index 51%
copy from streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IUserStorage.java
copy to streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Group.java
index 4f1c7f8..2a2db99 100644
--- a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IUserStorage.java
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Group.java
@@ -15,32 +15,53 @@
  * limitations under the License.
  *
  */
-package org.apache.streampipes.storage.api;
+package org.apache.streampipes.model.client.user;
 
-import org.apache.streampipes.model.client.user.Principal;
-import org.apache.streampipes.model.client.user.ServiceAccount;
-import org.apache.streampipes.model.client.user.UserAccount;
+import com.google.gson.annotations.SerializedName;
 
 import java.util.List;
 
-public interface IUserStorage {
-  List<Principal> getAllUsers();
+public class Group {
 
-  List<UserAccount> getAllUserAccounts();
+  protected @SerializedName("_id") String groupId;
+  protected @SerializedName("_rev") String rev;
 
-  List<ServiceAccount> getAllServiceAccounts();
+  private String groupName;
 
-  Principal getUser(String principalName);
+  private List<Role> roles;
 
-  UserAccount getUserAccount(String principalName);
+  public Group() {
+  }
 
-  ServiceAccount getServiceAccount(String principalName);
+  public String getGroupId() {
+    return groupId;
+  }
 
-  void storeUser(Principal user);
+  public void setGroupId(String groupId) {
+    this.groupId = groupId;
+  }
 
-  void updateUser(Principal user);
+  public String getRev() {
+    return rev;
+  }
 
-  boolean emailExists(String email);
+  public void setRev(String rev) {
+    this.rev = rev;
+  }
 
-  boolean checkUser(String username);
+  public String getGroupName() {
+    return groupName;
+  }
+
+  public void setGroupName(String groupName) {
+    this.groupName = groupName;
+  }
+
+  public List<Role> getRoles() {
+    return roles;
+  }
+
+  public void setRoles(List<Role> roles) {
+    this.roles = roles;
+  }
 }
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Principal.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Principal.java
index 29d20e0..a9b9b04 100644
--- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Principal.java
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Principal.java
@@ -35,7 +35,7 @@ public abstract class Principal implements UserDetails {
 	private boolean accountLocked;
 	private boolean accountExpired;
 
-	private String principalName;
+	protected String username;
 
 	protected List<Element> ownSources;
 	protected List<Element> ownSepas;
@@ -135,12 +135,8 @@ public abstract class Principal implements UserDetails {
 		this.accountExpired = accountExpired;
 	}
 
-	public String getPrincipalName() {
-		return principalName;
-	}
-
-	public void setPrincipalName(String principalName) {
-		this.principalName = principalName;
+	public void setUsername(String username) {
+		this.username = username;
 	}
 
 	public void setOwnSources(List<Element> ownSources) {
@@ -193,7 +189,7 @@ public abstract class Principal implements UserDetails {
 
 	@Override
 	public String getUsername() {
-		return principalName;
+		return username;
 	}
 
 
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Privilege.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Privilege.java
new file mode 100644
index 0000000..3bf9649
--- /dev/null
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/Privilege.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://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.
+ *
+ */
+package org.apache.streampipes.model.client.user;
+
+public enum Privilege {
+  // Pipelines
+  PRIVILEGE_CREATE_PIPELINE,
+  PRIVILEGE_READ_PIPELINE,
+  PRIVILEGE_UPDATE_PIPELINE,
+  PRIVILEGE_DELETE_PIPELINE,
+
+  // Adapters
+  PRIVILEGE_CREATE_ADAPTER,
+  PRIVILEGE_READ_ADAPTER,
+  PRIVILEGE_UPDATE_ADAPTER,
+  PRIVILEGE_DELETE_ADAPTER,
+
+  // Pipeline Elements
+  PRIVILEGE_CREATE_PIPELINE_ELEMENT,
+  PRIVILEGE_READ_PIPELINE_ELEMENT,
+  PRIVILEGE_UPDATE_PIPELINE_ELEMENT,
+  PRIVILEGE_DELETE_PIPELINE_ELEMENT,
+
+  // Dashboard
+  PRIVILEGE_CREATE_DASHBOARD,
+  PRIVILEGE_READ_DASHBOARD,
+  PRIVILEGE_UPDATE_DASHBOARD,
+  PRIVILEGE_DELETE_DASHBOARD,
+
+  // Dashboard widget
+  PRIVILEGE_CREATE_DASHBOARD_WIDGET,
+  PRIVILEGE_READ_DASHBOARD_WIDGET,
+  PRIVILEGE_UPDATE_DASHBOARD_WIDGET,
+  PRIVILEGE_DELETE_DASHBOARD_WIDGET,
+
+  // Data Explorer view
+  PRIVILEGE_CREATE_DATA_EXPLORER_VIEW,
+  PRIVILEGE_READ_DATA_EXPLORER_VIEW,
+  PRIVILEGE_UPDATE_DATA_EXPLORER_VIEW,
+  PRIVILEGE_DELETE_DATA_EXPLORER_VIEW,
+
+  // Data Explorer widget
+  PRIVILEGE_CREATE_DATA_EXPLORER_WIDGET,
+  PRIVILEGE_READ_DATA_EXPLORER_WIDGET,
+  PRIVILEGE_UPDATE_DATA_EXPLORER_WIDGET,
+  PRIVILEGE_DELETE_DATA_EXPLORER_WIDGET,
+
+  // Apps
+  PRIVILEGE_READ_APPS,
+
+  // NOTIFICATIONS
+  PRIVILEGE_READ_NOTIFICATIONS,
+
+  // FILES
+  PRIVILEGE_READ_FILES,
+  PRIVILEGE_CREATE_FILES,
+  PRIVILEGE_UPDATE_FILES,
+  PRIVILEGE_DELETE_FILES,
+
+  // Admin
+  PRIVILEGE_ADMIN
+}
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/ServiceAccount.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/ServiceAccount.java
index 7fafdb5..3134d34 100644
--- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/ServiceAccount.java
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/ServiceAccount.java
@@ -17,11 +17,14 @@
  */
 package org.apache.streampipes.model.client.user;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import org.apache.streampipes.model.shared.annotation.TsModel;
 import org.springframework.security.core.GrantedAuthority;
 
 import java.util.Collection;
 import java.util.Set;
 
+@TsModel
 public class ServiceAccount extends Principal {
 
   private String clientSecret;
@@ -30,7 +33,7 @@ public class ServiceAccount extends Principal {
                                  String clientSecret,
                                  Set<Role> roles) {
     ServiceAccount account = new ServiceAccount();
-    account.setPrincipalName(serviceAccountName);
+    account.setUsername(serviceAccountName);
     account.setClientSecret(clientSecret);
     account.setRoles(roles);
     account.setAccountEnabled(true);
@@ -51,6 +54,7 @@ public class ServiceAccount extends Principal {
     this.clientSecret = clientSecret;
   }
 
+  @JsonIgnore
   @Override
   public Collection<? extends GrantedAuthority> getAuthorities() {
     return null;
@@ -61,8 +65,4 @@ public class ServiceAccount extends Principal {
     return null;
   }
 
-  @Override
-  public String getUsername() {
-    return null;
-  }
 }
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 c6bfd34..c8a6396 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
@@ -18,6 +18,7 @@
 
 package org.apache.streampipes.model.client.user;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import org.apache.streampipes.model.shared.annotation.TsModel;
 import org.springframework.security.core.GrantedAuthority;
 
@@ -32,7 +33,6 @@ public class UserAccount extends Principal {
 
 	protected String email;
 	protected String fullName;
-	protected String username;
 	protected String password;
 
 	protected List<String> preferredDataStreams;
@@ -48,7 +48,7 @@ public class UserAccount extends Principal {
 																 String encryptedPassword,
 																 Set<Role> roles) {
 		UserAccount account = new UserAccount();
-		account.setPrincipalName(username);
+		account.setUsername(username);
 		account.setPassword(encryptedPassword);
 		account.setRoles(roles);
 		account.setAccountEnabled(true);
@@ -171,6 +171,7 @@ public class UserAccount extends Principal {
 		this.darkMode = darkMode;
 	}
 
+	@JsonIgnore
 	@Override
 	public Collection<? extends GrantedAuthority> getAuthorities() {
 		return roles.stream().map(Enum::toString).map(r -> (GrantedAuthority) () -> r).collect(Collectors.toList());
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserInfo.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserInfo.java
index d12a1ec..dd12381 100644
--- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserInfo.java
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserInfo.java
@@ -20,13 +20,12 @@ package org.apache.streampipes.model.client.user;
 
 import org.apache.streampipes.model.shared.annotation.TsModel;
 
-import java.util.List;
 import java.util.Set;
 
 @TsModel
 public class UserInfo {
 
-  private String userId;
+  private String username;
   private String displayName;
   private String email;
   private Set<String> roles;
@@ -36,12 +35,12 @@ public class UserInfo {
   public UserInfo() {
   }
 
-  public String getUserId() {
-    return userId;
+  public String getUsername() {
+    return username;
   }
 
-  public void setUserId(String userId) {
-    this.userId = userId;
+  public void setUsername(String username) {
+    this.username = username;
   }
 
   public String getDisplayName() {
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/CouchDbInstallationStep.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/CouchDbInstallationStep.java
index 7dbe871..3b6b803 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/CouchDbInstallationStep.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/CouchDbInstallationStep.java
@@ -146,10 +146,10 @@ public class CouchDbInstallationStep extends InstallationStep {
       Map<String, MapReduce> views = new HashMap<>();
 
       MapReduce passwordFunction = new MapReduce();
-      passwordFunction.setMap("function(doc) { if(doc.principalName && doc.principalType === 'USER_ACCOUNT' && doc.password) { emit(doc.principalName, doc.password); } }");
+      passwordFunction.setMap("function(doc) { if(doc.username && doc.principalType === 'USER_ACCOUNT' && doc.password) { emit(doc.username, doc.password); } }");
 
       MapReduce usernameFunction = new MapReduce();
-      usernameFunction.setMap("function(doc) { if(doc.principalName) { emit(doc.principalName, doc); } }");
+      usernameFunction.setMap("function(doc) { if(doc.username) { emit(doc.username, doc); } }");
 
       MapReduce tokenFunction = new MapReduce();
       tokenFunction.setMap("function(doc) { if (doc.userApiTokens) { doc.userApiTokens.forEach(function(token) { emit(token.hashedToken, doc.email); });}}");
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/PipelineElementInstallationStep.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/PipelineElementInstallationStep.java
index 67fa355..5d6fca8 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/PipelineElementInstallationStep.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/setup/PipelineElementInstallationStep.java
@@ -30,11 +30,11 @@ import java.util.List;
 public class PipelineElementInstallationStep extends InstallationStep {
 
   private ExtensionsServiceEndpoint endpoint;
-  private String principalName;
+  private String username;
 
-  public PipelineElementInstallationStep(ExtensionsServiceEndpoint endpoint, String principalName) {
+  public PipelineElementInstallationStep(ExtensionsServiceEndpoint endpoint, String username) {
     this.endpoint = endpoint;
-    this.principalName = principalName;
+    this.username = username;
   }
 
   @Override
@@ -43,7 +43,7 @@ public class PipelineElementInstallationStep extends InstallationStep {
     List<ExtensionsServiceEndpointItem> items = Operations.getEndpointUriContents(Collections.singletonList(endpoint));
     for(ExtensionsServiceEndpointItem item : items) {
       statusMessages.add(new EndpointItemParser().parseAndAddEndpointItem(item.getUri(),
-              principalName, true, false));
+              username, true, false));
     }
 
     if (statusMessages.stream().allMatch(Message::isSuccess)) {
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/UserManagementService.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/UserManagementService.java
index 26cbe88..2ea3715 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/UserManagementService.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/UserManagementService.java
@@ -45,9 +45,9 @@ public class UserManagementService {
     return true;
   }
 
-  public static void setHideTutorial(String principalName, boolean hideTutorial) {
+  public static void setHideTutorial(String username, boolean hideTutorial) {
     IUserStorage userService = getUserStorage();
-    UserAccount user = userService.getUserAccount(principalName);
+    UserAccount user = userService.getUserAccount(username);
     user.setHideTutorial(hideTutorial);
     userService.updateUser(user);
   }
diff --git a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/UserService.java b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/UserService.java
index fbb51c2..90ff3e2 100644
--- a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/UserService.java
+++ b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/storage/UserService.java
@@ -202,12 +202,12 @@ public class UserService {
     return getUserAccount(username).getPreferredDataSinks();
   }
 
-  public List<String> getAvailableActionUris(String principalName) {
-    List<String> actions = new ArrayList<>(getOwnActionUris(principalName));
+  public List<String> getAvailableActionUris(String username) {
+    List<String> actions = new ArrayList<>(getOwnActionUris(username));
     userStorage
             .getAllUsers()
             .stream()
-            .filter(u -> !(u.getPrincipalName().equals(principalName)))
+            .filter(u -> !(u.getUsername().equals(username)))
             .map(u -> u.getOwnActions().stream().filter(p -> p.isPublicElement()).map(p -> p.getElementId()).collect(Collectors.toList())).forEach(actions::addAll);
     return actions;
   }
@@ -216,18 +216,18 @@ public class UserService {
     return userStorage.getUser(username).getOwnSepas().stream().map(r -> r.getElementId()).collect(Collectors.toList());
   }
 
-  public List<String> getAvailableSepaUris(String principalName) {
-    List<String> sepas = new ArrayList<>(getOwnSepaUris(principalName));
+  public List<String> getAvailableSepaUris(String username) {
+    List<String> sepas = new ArrayList<>(getOwnSepaUris(username));
     userStorage
             .getAllUsers()
             .stream()
-            .filter(u -> !(u.getPrincipalName().equals(principalName)))
+            .filter(u -> !(u.getUsername().equals(username)))
             .map(u -> u.getOwnSepas().stream().filter(p -> p.isPublicElement()).map(p -> p.getElementId()).collect(Collectors.toList())).forEach(sepas::addAll);
     return sepas;
   }
 
-  public List<String> getFavoriteSepaUris(String principalName) {
-    return getUserAccount(principalName).getPreferredDataProcessors();
+  public List<String> getFavoriteSepaUris(String username) {
+    return getUserAccount(username).getPreferredDataProcessors();
   }
 
   public List<String> getOwnSourceUris(String email) {
@@ -239,12 +239,12 @@ public class UserService {
             .collect(Collectors.toList());
   }
 
-  public List<String> getAvailableSourceUris(String principalName) {
-    List<String> sources = new ArrayList<>(getOwnSepaUris(principalName));
+  public List<String> getAvailableSourceUris(String username) {
+    List<String> sources = new ArrayList<>(getOwnSepaUris(username));
     userStorage
             .getAllUsers()
             .stream()
-            .filter(u -> !(u.getPrincipalName().equals(principalName)))
+            .filter(u -> !(u.getUsername().equals(username)))
             .map(u -> u.getOwnSources()
                     .stream()
                     .filter(p -> p.isPublicElement())
@@ -258,12 +258,12 @@ public class UserService {
     return getUserAccount(username).getPreferredDataStreams();
   }
 
-  public UserAccount getUserAccount(String principalName) {
-    return (UserAccount) getPrincipal(principalName);
+  public UserAccount getUserAccount(String username) {
+    return (UserAccount) getPrincipal(username);
   }
 
-  private Principal getPrincipal(String principalName) {
-    return userStorage.getUser(principalName);
+  private Principal getPrincipal(String username) {
+    return userStorage.getUser(username);
   }
 
   private IUserStorage userStorage() {
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserGroupResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserGroupResource.java
new file mode 100644
index 0000000..f02627c
--- /dev/null
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserGroupResource.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://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.
+ *
+ */
+package org.apache.streampipes.rest.impl;
+
+import org.apache.streampipes.model.client.user.Group;
+import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource;
+import org.apache.streampipes.storage.api.IUserGroupStorage;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.Response;
+
+@Path("/v2/users/groups")
+public class UserGroupResource extends AbstractAuthGuardedRestResource {
+
+  @GET
+  public Response getAllUserGroups() {
+    return ok(getUserGroupStorage().getAll());
+  }
+
+  @POST
+  public Response addUserGroup(Group group) {
+    getUserGroupStorage().createElement(group);
+    return ok();
+  }
+
+  @PUT
+  @Path("{groupId}")
+  public Response updateUserGroup(@PathParam("groupId") String groupId,
+                                  Group group) {
+    if (!groupId.equals(group.getGroupId())) {
+      return badRequest();
+    } else {
+      return ok(getUserGroupStorage().updateElement(group));
+    }
+  }
+
+  @DELETE
+  @Path("{groupId}")
+  public Response deleteUserGroup(@PathParam("groupId") String groupId) {
+    Group group = getUserGroupStorage().getElementById(groupId);
+    if (group != null) {
+      getUserGroupStorage().deleteElement(group);
+      return ok();
+    } else {
+      return badRequest();
+    }
+  }
+
+  private IUserGroupStorage getUserGroupStorage() {
+    return getNoSqlStorage().getUserGroupStorage();
+  }
+}
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserProfile.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserProfile.java
index cb50c1d..d3f2de2 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserProfile.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserProfile.java
@@ -18,86 +18,12 @@
 
 package org.apache.streampipes.rest.impl;
 
-import org.apache.streampipes.model.client.user.RawUserApiToken;
-import org.apache.streampipes.model.client.user.UserAccount;
-import org.apache.streampipes.model.message.Notifications;
 import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource;
-import org.apache.streampipes.rest.shared.annotation.JacksonSerialized;
-import org.apache.streampipes.user.management.service.TokenService;
 
-import javax.ws.rs.*;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import java.util.stream.Collectors;
+import javax.ws.rs.Path;
 
 @Path("/v2/users/profile")
 public class UserProfile extends AbstractAuthGuardedRestResource {
 
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    public Response getUserDetails() {
-        UserAccount user = getUser(getAuthenticatedUsername());
-        user.setPassword("");
 
-        if (user != null) {
-            return ok(user);
-        } else {
-            return statusMessage(Notifications.error("User not found"));
-        }
-    }
-
-    @Path("/appearance/mode/{darkMode}")
-    @PUT
-    @Produces(MediaType.APPLICATION_JSON)
-    public Response updateAppearanceMode(@PathParam("darkMode") boolean darkMode) {
-        String authenticatedUserId = getAuthenticatedUsername();
-        if (authenticatedUserId != null) {
-            UserAccount user = getUser(authenticatedUserId);
-            user.setDarkMode(darkMode);
-            getUserStorage().updateUser(user);
-
-            return ok(Notifications.success("Appearance updated"));
-        } else {
-            return statusMessage(Notifications.error("User not found"));
-        }
-    }
-
-    @PUT
-    @Consumes(MediaType.APPLICATION_JSON)
-    @Produces(MediaType.APPLICATION_JSON)
-    public Response updateUserDetails(UserAccount user) {
-        String authenticatedUserId = getAuthenticatedUsername();
-        if (user != null && authenticatedUserId.equals(user.getEmail())) {
-            UserAccount existingUser = getUser(user.getEmail());
-            user.setPassword(existingUser.getPassword());
-            user.setUserApiTokens(existingUser
-                    .getUserApiTokens()
-                    .stream()
-                    .filter(existingToken -> user.getUserApiTokens()
-                            .stream()
-                            .anyMatch(updatedToken -> existingToken
-                                    .getTokenId()
-                                    .equals(updatedToken.getTokenId())))
-                    .collect(Collectors.toList()));
-            user.setRev(existingUser.getRev());
-            getUserStorage().updateUser(user);
-            return ok(Notifications.success("User updated"));
-        } else {
-            return statusMessage(Notifications.error("User not found"));
-        }
-    }
-
-    private UserAccount getUser(String email) {
-        return getUserStorage().getUserAccount(email);
-    }
-
-    @POST
-    @Path("tokens")
-    @Consumes(MediaType.APPLICATION_JSON)
-    @Produces(MediaType.APPLICATION_JSON)
-    @JacksonSerialized
-    public Response createNewApiToken(RawUserApiToken rawToken) {
-        RawUserApiToken generatedToken = new TokenService().createAndStoreNewToken(getAuthenticatedUsername(), rawToken);
-        return ok(generatedToken);
-    }
 }
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
new file mode 100644
index 0000000..95a452e
--- /dev/null
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/UserResource.java
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://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.
+ *
+ */
+package org.apache.streampipes.rest.impl;
+
+import org.apache.streampipes.model.client.user.*;
+import org.apache.streampipes.model.message.Notifications;
+import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource;
+import org.apache.streampipes.rest.shared.annotation.JacksonSerialized;
+import org.apache.streampipes.user.management.service.TokenService;
+import org.apache.streampipes.user.management.util.PasswordUtil;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Path("/v2/users")
+public class UserResource extends AbstractAuthGuardedRestResource {
+
+  @GET
+  @JacksonSerialized
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response getAllUsers(@QueryParam("type") String principalType) {
+    List<Principal> allPrincipals = new ArrayList<>();
+    if (principalType != null && principalType.equals(PrincipalType.USER_ACCOUNT.name())) {
+      allPrincipals.addAll(getUserStorage().getAllUserAccounts());
+    } else if (principalType != null && principalType.equals(PrincipalType.SERVICE_ACCOUNT.name())) {
+      allPrincipals.addAll(getUserStorage().getAllServiceAccounts());
+    } else {
+      allPrincipals.addAll(getUserStorage().getAllUsers());
+    }
+    removeCredentials(allPrincipals);
+    return ok(allPrincipals);
+  }
+
+
+  @GET
+  @JacksonSerialized
+  @Path("{username}")
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response getUserDetails(@PathParam("username") String username) {
+    Principal principal = getPrincipal(username);
+    removeCredentials(principal);
+
+    if (principal != null) {
+      return ok(principal);
+    } else {
+      return statusMessage(Notifications.error("User not found"));
+    }
+  }
+
+  @DELETE
+  @JacksonSerialized
+  @Path("{principalId}")
+  public Response deleteUser(@PathParam("principalId") String principalId) {
+    Principal principal = getPrincipalById(principalId);
+
+    if (principal != null) {
+      getUserStorage().deleteUser(principalId);
+      return ok();
+    } else {
+      return statusMessage(Notifications.error("User not found"));
+    }
+  }
+
+  @Path("{userId}/appearance/mode/{darkMode}")
+  @PUT
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response updateAppearanceMode(@PathParam("userId") String userId,
+                                       @PathParam("darkMode") boolean darkMode) {
+    String authenticatedUserId = getAuthenticatedUsername();
+    if (authenticatedUserId != null) {
+      UserAccount user = getUser(authenticatedUserId);
+      user.setDarkMode(darkMode);
+      getUserStorage().updateUser(user);
+
+      return ok(Notifications.success("Appearance updated"));
+    } else {
+      return statusMessage(Notifications.error("User not found"));
+    }
+  }
+
+  @POST
+  @Path("/user")
+  @JacksonSerialized
+  @Produces(MediaType.APPLICATION_JSON)
+  @Consumes(MediaType.APPLICATION_JSON)
+  public Response registerUser(UserAccount userAccount) {
+    // TODO check if userId is already taken
+    try {
+      if (getUserStorage().getUser(userAccount.getUsername()) == null) {
+        String property = userAccount.getPassword();
+        String encryptedProperty = PasswordUtil.encryptPassword(property);
+        userAccount.setPassword(encryptedProperty);
+        getUserStorage().storeUser(userAccount);
+        return ok();
+      } else {
+        return badRequest(Notifications.error("This user ID already exists. Please choose another address."));
+      }
+    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+      return badRequest();
+    }
+  }
+
+  @POST
+  @Path("/service")
+  @JacksonSerialized
+  @Produces(MediaType.APPLICATION_JSON)
+  @Consumes(MediaType.APPLICATION_JSON)
+  public Response registerService(ServiceAccount userAccount) {
+    // TODO check if userId is already taken
+    if (getUserStorage().getUser(userAccount.getUsername()) == null) {
+      getUserStorage().storeUser(userAccount);
+      return ok();
+    } else {
+      return badRequest(Notifications.error("This user ID already exists. Please choose another address."));
+    }
+  }
+
+  @POST
+  @Path("{userId}/tokens")
+  @Consumes(MediaType.APPLICATION_JSON)
+  @Produces(MediaType.APPLICATION_JSON)
+  @JacksonSerialized
+  public Response createNewApiToken(@PathParam("userId") String userId,
+                                    RawUserApiToken rawToken) {
+    RawUserApiToken generatedToken = new TokenService().createAndStoreNewToken(userId, rawToken);
+    return ok(generatedToken);
+  }
+
+  @PUT
+  @Path("user/{principalId}")
+  @Consumes(MediaType.APPLICATION_JSON)
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response updateUserAccountDetails(@PathParam("principalId") String principalId,
+                                    UserAccount user) {
+    String authenticatedUserId = getAuthenticatedUsername();
+    if (user != null && (authenticatedUserId.equals(principalId) || isAdmin())) {
+      Principal existingUser = getPrincipalById(principalId);
+      updateUser((UserAccount) existingUser, user);
+      user.setRev(existingUser.getRev());
+      getUserStorage().updateUser(user);
+      return ok(Notifications.success("User updated"));
+    } else {
+      return statusMessage(Notifications.error("User not found"));
+    }
+  }
+
+  @PUT
+  @Path("service/{principalId}")
+  @Consumes(MediaType.APPLICATION_JSON)
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response updateServiceAccountDetails(@PathParam("principalId") String principalId,
+                                           ServiceAccount user) {
+    String authenticatedUserId = getAuthenticatedUsername();
+    if (user != null && (authenticatedUserId.equals(principalId) || isAdmin())) {
+      Principal existingUser = getPrincipalById(principalId);
+      user.setRev(existingUser.getRev());
+      getUserStorage().updateUser(user);
+      return ok(Notifications.success("User updated"));
+    } else {
+      return statusMessage(Notifications.error("User not found"));
+    }
+  }
+
+  private boolean isAdmin() {
+    return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(r -> r.getAuthority().equals(Role.ADMIN.name()));
+  }
+
+  private void updateUser(UserAccount existingUser, UserAccount user) {
+    user.setPassword(existingUser.getPassword());
+    user.setUserApiTokens(existingUser
+            .getUserApiTokens()
+            .stream()
+            .filter(existingToken -> user.getUserApiTokens()
+                    .stream()
+                    .anyMatch(updatedToken -> existingToken
+                            .getTokenId()
+                            .equals(updatedToken.getTokenId())))
+            .collect(Collectors.toList()));
+  }
+
+  private UserAccount getUser(String username) {
+    return getUserStorage().getUserAccount(username);
+  }
+
+  private Principal getPrincipal(String username) {
+    return getUserStorage().getUser(username);
+  }
+
+  private Principal getPrincipalById(String principalId) {
+    return getUserStorage().getUserById(principalId);
+  }
+
+  private void removeCredentials(List<Principal> principals) {
+    principals.forEach(this::removeCredentials);
+  }
+
+  private void removeCredentials(Principal principal) {
+    if (principal instanceof UserAccount) {
+      ((UserAccount) principal).setPassword("");
+    }
+  }
+}
diff --git a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java
index 54f8f27..52a5b80 100644
--- a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java
+++ b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java
@@ -25,6 +25,8 @@ public interface INoSqlStorage {
 
   ICategoryStorage getCategoryStorageAPI();
 
+  IUserGroupStorage getUserGroupStorage();
+
   ILabelStorage getLabelStorageAPI();
 
   IPipelineStorage getPipelineStorageAPI();
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/Permission.java b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IUserGroupStorage.java
similarity index 82%
rename from streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/Permission.java
rename to streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IUserGroupStorage.java
index 00340e2..accd125 100644
--- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/Permission.java
+++ b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IUserGroupStorage.java
@@ -15,8 +15,9 @@
  * limitations under the License.
  *
  */
+package org.apache.streampipes.storage.api;
 
-package org.apache.streampipes.model.client.security;
+import org.apache.streampipes.model.client.user.Group;
 
-public class Permission {
+public interface IUserGroupStorage extends CRUDStorage<String, Group> {
 }
diff --git a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IUserStorage.java b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IUserStorage.java
index 4f1c7f8..d7f996a 100644
--- a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IUserStorage.java
+++ b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IUserStorage.java
@@ -30,11 +30,11 @@ public interface IUserStorage {
 
   List<ServiceAccount> getAllServiceAccounts();
 
-  Principal getUser(String principalName);
+  Principal getUser(String username);
 
-  UserAccount getUserAccount(String principalName);
+  UserAccount getUserAccount(String username);
 
-  ServiceAccount getServiceAccount(String principalName);
+  ServiceAccount getServiceAccount(String username);
 
   void storeUser(Principal user);
 
@@ -43,4 +43,8 @@ public interface IUserStorage {
   boolean emailExists(String email);
 
   boolean checkUser(String username);
+
+  void deleteUser(String principalId);
+
+  Principal getUserById(String principalId);
 }
diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java
index 288ae0f..b26d056 100644
--- a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java
+++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java
@@ -38,6 +38,11 @@ public enum CouchDbStorageManager implements INoSqlStorage {
   public ICategoryStorage getCategoryStorageAPI() { return new CategoryStorageImpl(); }
 
   @Override
+  public IUserGroupStorage getUserGroupStorage() {
+    return new UserGroupStorageImpl();
+  }
+
+  @Override
   public ILabelStorage getLabelStorageAPI() { return new LabelStorageImpl(); }
 
   @Override
diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserGroupStorageImpl.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserGroupStorageImpl.java
new file mode 100644
index 0000000..ef1db56
--- /dev/null
+++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserGroupStorageImpl.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://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.
+ *
+ */
+package org.apache.streampipes.storage.couchdb.impl;
+
+import org.apache.streampipes.model.client.user.Group;
+import org.apache.streampipes.storage.api.IUserGroupStorage;
+import org.apache.streampipes.storage.couchdb.dao.AbstractDao;
+import org.apache.streampipes.storage.couchdb.utils.Utils;
+
+import java.util.List;
+
+public class UserGroupStorageImpl extends AbstractDao<Group> implements IUserGroupStorage {
+
+  public UserGroupStorageImpl() {
+    super(Utils::getCouchDbUserGroupStorage, Group.class);
+  }
+
+  @Override
+  public List<Group> getAll() {
+    return findAll();
+  }
+
+  @Override
+  public void createElement(Group element) {
+    persist(element);
+  }
+
+  @Override
+  public Group getElementById(String s) {
+   return findWithNullIfEmpty(s);
+  }
+
+  @Override
+  public Group updateElement(Group element) {
+    update(element);
+    return getElementById(element.getGroupId());
+  }
+
+  @Override
+  public void deleteElement(Group element) {
+    delete(element.getGroupId());
+  }
+}
diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java
index c4b6d7f..5e1b157 100644
--- a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java
+++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java
@@ -35,98 +35,111 @@ import java.util.stream.Collectors;
 /**
  * User Storage.
  * Handles operations on user including user-specified pipelines.
- *
- *
  */
 public class UserStorage extends AbstractDao<Principal> implements IUserStorage {
 
-    Logger LOG = LoggerFactory.getLogger(UserStorage.class);
-
-    public UserStorage() {
-        super(Utils::getCouchDbUserClient, Principal.class);
-    }
-
-    @Override
-    public List<Principal> getAllUsers()
-    {
-      List<Principal> users = findAll();
-    	return new ArrayList<>(users);
-    }
-
-    @Override
-    public List<UserAccount> getAllUserAccounts() {
-        return findAll()
-                .stream()
-                .filter(u -> u instanceof UserAccount)
-                .map(u -> (UserAccount) u)
-                .collect(Collectors.toList());
-    }
-
-    @Override
-    public List<ServiceAccount> getAllServiceAccounts() {
-        return findAll()
-                .stream()
-                .filter(u -> u instanceof ServiceAccount)
-                .map(u -> (ServiceAccount) u)
-                .collect(Collectors.toList());
-    }
-
-    @Override
-    public Principal getUser(String principalName) {
-        // TODO improve
-        CouchDbClient couchDbClient = couchDbClientSupplier.get();
-        List<Principal> users = couchDbClient.view("users/username").key(principalName).includeDocs(true).query(Principal.class);
-        if (users.size() != 1) {
-            LOG.error("None or to many users with matching username");
-        }
-        return users.get(0);
+  Logger LOG = LoggerFactory.getLogger(UserStorage.class);
+
+  public UserStorage() {
+    super(Utils::getCouchDbUserClient, Principal.class);
+  }
+
+  @Override
+  public List<Principal> getAllUsers() {
+    List<Principal> users = couchDbClientSupplier
+            .get()
+            .view("users/username")
+            .includeDocs(true)
+            .query(Principal.class);
+    return new ArrayList<>(users);
+  }
+
+  @Override
+  public List<UserAccount> getAllUserAccounts() {
+    return getAllUsers()
+            .stream()
+            .filter(u -> u instanceof UserAccount)
+            .map(u -> (UserAccount) u)
+            .collect(Collectors.toList());
+  }
+
+  @Override
+  public List<ServiceAccount> getAllServiceAccounts() {
+    return getAllUsers()
+            .stream()
+            .filter(u -> u instanceof ServiceAccount)
+            .map(u -> (ServiceAccount) u)
+            .collect(Collectors.toList());
+  }
+
+  @Override
+  public Principal getUser(String username) {
+    // TODO improve
+    CouchDbClient couchDbClient = couchDbClientSupplier.get();
+    List<Principal> users = couchDbClient
+            .view("users/username")
+            .key(username)
+            .includeDocs(true)
+            .query(Principal.class);
+    if (users.size() != 1) {
+      LOG.error("None or to many users with matching username");
     }
-
-    @Override
-    public UserAccount getUserAccount(String principalName) {
-        return (UserAccount) getUser(principalName);
-    }
-
-    @Override
-    public ServiceAccount getServiceAccount(String principalName) {
-        return (ServiceAccount) getUser(principalName);
-    }
-
-    @Override
-    public void storeUser(Principal user) {
-        persist(user);
-    }
-
-    @Override
-    public void updateUser(Principal user) {
-        update(user);
-    }
-
-    @Override
-    public boolean emailExists(String email)
-    {
-    	List<UserAccount> users = getAllUserAccounts();
-    	return users
-                .stream()
-                .filter(u -> u.getEmail() != null)
-                .anyMatch(u -> u.getEmail().equals(email));
-    }
-
-    /**
-    *
-    * @param username
-    * @return True if user exists exactly once, false otherwise
-    */
-   @Override
-   public boolean checkUser(String username) {
-       List<Principal> users = couchDbClientSupplier
-               .get()
-               .view("users/username")
-               .key(username)
-               .includeDocs(true)
-               .query(Principal.class);
-
-       return users.size() == 1;
-   }
+    return users.size() > 0 ? users.get(0) : null;
+  }
+
+  @Override
+  public UserAccount getUserAccount(String username) {
+    return (UserAccount) getUser(username);
+  }
+
+  @Override
+  public ServiceAccount getServiceAccount(String username) {
+    return (ServiceAccount) getUser(username);
+  }
+
+  @Override
+  public void storeUser(Principal user) {
+    persist(user);
+  }
+
+  @Override
+  public void updateUser(Principal user) {
+    update(user);
+  }
+
+  @Override
+  public boolean emailExists(String email) {
+    List<UserAccount> users = getAllUserAccounts();
+    return users
+            .stream()
+            .filter(u -> u.getEmail() != null)
+            .anyMatch(u -> u.getEmail().equals(email));
+  }
+
+  /**
+   * @param username
+   * @return True if user exists exactly once, false otherwise
+   */
+  @Override
+  public boolean checkUser(String username) {
+    List<Principal> users = couchDbClientSupplier
+            .get()
+            .view("users/username")
+            .key(username)
+            .includeDocs(true)
+            .query(Principal.class);
+
+    return users.size() == 1;
+  }
+
+  @Override
+  public void deleteUser(String principalId) {
+    delete(principalId);
+  }
+
+  @Override
+  public Principal getUserById(String principalId) {
+    return findWithNullIfEmpty(principalId);
+  }
 
 }
diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java
index ba2aba5..aa4eeb5 100644
--- a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java
+++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java
@@ -76,6 +76,10 @@ public class Utils {
     return getCouchDbGsonClient("pipeline");
   }
 
+  public static CouchDbClient getCouchDbUserGroupStorage() {
+    return getCouchDbGsonClient("usergroup");
+  }
+
   public static CouchDbClient getCouchDbSepaInvocationClient() {
     return getCouchDbGsonClient("invocation");
   }
diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/jwt/JwtTokenProvider.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/jwt/JwtTokenProvider.java
index 3dc0857..e877f5f 100644
--- a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/jwt/JwtTokenProvider.java
+++ b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/jwt/JwtTokenProvider.java
@@ -36,7 +36,7 @@ public class JwtTokenProvider {
 		Date tokenExpirationDate = makeExpirationDate();
 		Map<String, Object> claims = makeClaims(userPrincipal, roles);
 
-		return JwtTokenUtils.makeJwtToken(userPrincipal.getPrincipalName(), tokenSecret(), claims, tokenExpirationDate);
+		return JwtTokenUtils.makeJwtToken(userPrincipal.getUsername(), tokenSecret(), claims, tokenExpirationDate);
 	}
 
 	private Map<String, Object> makeClaims(Principal principal,
@@ -76,7 +76,7 @@ public class JwtTokenProvider {
 	private UserInfo toUserInfo(UserAccount localUser,
 															Set<String> roles) {
 		UserInfo userInfo = new UserInfo();
-		userInfo.setUserId("id");
+		userInfo.setUsername(localUser.getUsername());
 		userInfo.setEmail(localUser.getEmail());
 		userInfo.setDisplayName(localUser.getUsername());
 		userInfo.setShowTutorial(!localUser.isHideTutorial());
diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/jwt/SpKeyResolver.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/jwt/SpKeyResolver.java
index a8c8cf6..bcf3635 100644
--- a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/jwt/SpKeyResolver.java
+++ b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/jwt/SpKeyResolver.java
@@ -58,8 +58,8 @@ public class SpKeyResolver implements SigningKeyResolver {
     return null;
   }
 
-  private Principal getPrincipal(String principalName) {
-    return userStorage.getUser(principalName);
+  private Principal getPrincipal(String username) {
+    return userStorage.getUser(username);
   }
 
   private boolean isRealUser(Principal principal) {
diff --git a/ui/src/app/configuration/configuration.module.ts b/ui/src/app/configuration/configuration.module.ts
index 3005794..a9cbc59 100644
--- a/ui/src/app/configuration/configuration.module.ts
+++ b/ui/src/app/configuration/configuration.module.ts
@@ -25,7 +25,7 @@ import { MatInputModule } from '@angular/material/input';
 import { MatTooltipModule } from '@angular/material/tooltip';
 import { FlexLayoutModule } from '@angular/flex-layout';
 import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 
 import { ConfigurationComponent } from './configuration.component';
 import { ConfigurationService } from './shared/configuration.service';
@@ -46,6 +46,10 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
 import { SecurityConfigurationComponent } from './security-configuration/security-configuration.component';
 import { CoreUiModule } from '../core-ui/core-ui.module';
 import { MatDividerModule } from '@angular/material/divider';
+import { SecurityUserConfigComponent } from './security-configuration/security-user-configuration/security-user-config.component';
+import { SecurityServiceConfigComponent } from './security-configuration/security-service-configuration/security-service-config.component';
+import { EditUserDialogComponent } from './security-configuration/edit-user-dialog/edit-user-dialog.component';
+import { PlatformServicesModule } from '../platform-services/platform.module';
 
 @NgModule({
   imports: [
@@ -62,7 +66,9 @@ import { MatDividerModule } from '@angular/material/divider';
     MatTooltipModule,
     FormsModule,
     DragDropModule,
-    CoreUiModule
+    CoreUiModule,
+    ReactiveFormsModule,
+    PlatformServicesModule,
   ],
   declarations: [
     ConfigurationComponent,
@@ -73,8 +79,11 @@ import { MatDividerModule } from '@angular/material/divider';
     ConsulConfigsBooleanComponent,
     ConsulConfigsNumberComponent,
     DeleteDatalakeIndexComponent,
+    EditUserDialogComponent,
     PipelineElementConfigurationComponent,
     SecurityConfigurationComponent,
+    SecurityUserConfigComponent,
+    SecurityServiceConfigComponent,
     MessagingConfigurationComponent,
     DatalakeConfigurationComponent
   ],
diff --git a/ui/src/app/configuration/messaging-configuration/messaging-configuration.component.html b/ui/src/app/configuration/messaging-configuration/messaging-configuration.component.html
index b64ea3a..61d96e3 100644
--- a/ui/src/app/configuration/messaging-configuration/messaging-configuration.component.html
+++ b/ui/src/app/configuration/messaging-configuration/messaging-configuration.component.html
@@ -16,81 +16,78 @@
   ~
   -->
 
-<div fxLayout="row" class="page-container-padding">
-    <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start">
-        <sp-split-section title="Kafka Settings"
-                          subtitle="Manage Kafka settings for pipeline communication">
-            <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start" class="page-container-padding-inner">
-                <form (ngSubmit)="updateMessagingSettings()" class="form-width" fxFlex="100" fxLayout="column"
-                      *ngIf="loadingCompleted">
-                    <mat-form-field class="form-field" fxFlex="100">
-                        <input matInput [(ngModel)]="messagingSettings.batchSize"
-                               [placeholder]="'Batch Size'" type="text"
-                               [ngModelOptions]="{standalone: true}">
-                    </mat-form-field>
-                    <mat-form-field class="form-field" fxFlex="100">
-                        <input matInput [(ngModel)]="messagingSettings.messageMaxBytes"
-                               [placeholder]="'Message Max Bytes'" type="text"
-                               [ngModelOptions]="{standalone: true}">
-                    </mat-form-field>
-                    <mat-form-field class="form-field" fxFlex="100">
-                        <input matInput [(ngModel)]="messagingSettings.acks"
-                               [placeholder]="'Acks'" type="text"
-                               [ngModelOptions]="{standalone: true}">
-                    </mat-form-field>
-                    <mat-form-field class="form-field" fxFlex="100">
-                        <input matInput [(ngModel)]="messagingSettings.lingerMs"
-                               [placeholder]="'Linger MS'" type="text"
-                               [ngModelOptions]="{standalone: true}">
-                    </mat-form-field>
-                    <div fxLayoutAlign="start center" class="mt-10">
-                        <button mat-raised-button color="accent" type="submit"
-                                class="md-raised md-primary submit-button">Update
-                        </button>
-                    </div>
+<div fxLayout="column" class="page-container-padding">
+    <sp-split-section title="Kafka Settings"
+                      subtitle="Manage Kafka settings for pipeline communication">
+        <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start" class="page-container-padding-inner">
+            <form (ngSubmit)="updateMessagingSettings()" class="form-width" fxFlex="100" fxLayout="column"
+                  *ngIf="loadingCompleted">
+                <mat-form-field class="form-field" fxFlex="100">
+                    <input matInput [(ngModel)]="messagingSettings.batchSize"
+                           [placeholder]="'Batch Size'" type="text"
+                           [ngModelOptions]="{standalone: true}">
+                </mat-form-field>
+                <mat-form-field class="form-field" fxFlex="100">
+                    <input matInput [(ngModel)]="messagingSettings.messageMaxBytes"
+                           [placeholder]="'Message Max Bytes'" type="text"
+                           [ngModelOptions]="{standalone: true}">
+                </mat-form-field>
+                <mat-form-field class="form-field" fxFlex="100">
+                    <input matInput [(ngModel)]="messagingSettings.acks"
+                           [placeholder]="'Acks'" type="text"
+                           [ngModelOptions]="{standalone: true}">
+                </mat-form-field>
+                <mat-form-field class="form-field" fxFlex="100">
+                    <input matInput [(ngModel)]="messagingSettings.lingerMs"
+                           [placeholder]="'Linger MS'" type="text"
+                           [ngModelOptions]="{standalone: true}">
+                </mat-form-field>
+                <div fxLayoutAlign="start center" class="mt-10">
+                    <button mat-raised-button color="accent" type="submit"
+                            class="md-raised md-primary submit-button">Update
+                    </button>
+                </div>
 
-                </form>
-            </div>
-        </sp-split-section>
-        <mat-divider></mat-divider>
-        <div class="mt-30">
-            <sp-split-section title="Message Formats"
-                              subtitle="Manage the priority of message formats used">
-                <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start" class="page-container-padding-inner">
-                    <div cdkDropList class="data-format-list" (cdkDropListDropped)="drop($event)"
-                         *ngIf="loadingCompleted">
-                        <div class="data-format-box" *ngFor="let format of messagingSettings.prioritizedFormats"
-                             cdkDrag>
-                            {{format}}
-                        </div>
-                    </div>
-                    <div fxLayoutAlign="start center" class="mt-10">
-                        <button mat-raised-button color="accent" type="submit"
-                                class="md-raised md-primary submit-button" (click)="updateMessagingSettings()">Update
-                        </button>
-                    </div>
+            </form>
+        </div>
+    </sp-split-section>
+    <mat-divider></mat-divider>
+
+    <sp-split-section title="Message Formats"
+                      subtitle="Manage the priority of message formats used">
+        <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start" class="page-container-padding-inner">
+            <div cdkDropList class="data-format-list" (cdkDropListDropped)="drop($event)"
+                 *ngIf="loadingCompleted">
+                <div class="data-format-box" *ngFor="let format of messagingSettings.prioritizedFormats"
+                     cdkDrag>
+                    {{format}}
                 </div>
-            </sp-split-section>
+            </div>
+            <div fxLayoutAlign="start center" class="mt-10">
+                <button mat-raised-button color="accent" type="submit"
+                        class="md-raised md-primary submit-button" (click)="updateMessagingSettings()">Update
+                </button>
+            </div>
         </div>
-        <mat-divider></mat-divider>
-        <div class="mt-30">
-            <sp-split-section title="Protocols"
-                              subtitle="Manage the priority of protocols used">
-                <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start" class="page-container-padding-inner">
-                    <div cdkDropList class="data-format-list" (cdkDropListDropped)="dropProtocol($event)"
-                         *ngIf="loadingCompleted">
-                        <div class="data-format-box" *ngFor="let protocol of messagingSettings.prioritizedProtocols"
-                             cdkDrag>
-                            {{protocol}}
-                        </div>
-                    </div>
-                    <div fxLayoutAlign="start center" class="mt-10">
-                        <button mat-raised-button color="accent" type="submit"
-                                class="md-raised md-primary submit-button" (click)="updateMessagingSettings()">Update
-                        </button>
-                    </div>
+    </sp-split-section>
+
+    <mat-divider></mat-divider>
+
+    <sp-split-section title="Protocols"
+                      subtitle="Manage the priority of protocols used">
+        <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start" class="page-container-padding-inner">
+            <div cdkDropList class="data-format-list" (cdkDropListDropped)="dropProtocol($event)"
+                 *ngIf="loadingCompleted">
+                <div class="data-format-box" *ngFor="let protocol of messagingSettings.prioritizedProtocols"
+                     cdkDrag>
+                    {{protocol}}
                 </div>
-            </sp-split-section>
+            </div>
+            <div fxLayoutAlign="start center" class="mt-10">
+                <button mat-raised-button color="accent" type="submit"
+                        class="md-raised md-primary submit-button" (click)="updateMessagingSettings()">Update
+                </button>
+            </div>
         </div>
-    </div>
+    </sp-split-section>
 </div>
diff --git a/ui/src/app/configuration/security-configuration/abstract-security-principal-config.ts b/ui/src/app/configuration/security-configuration/abstract-security-principal-config.ts
new file mode 100644
index 0000000..be1b03d
--- /dev/null
+++ b/ui/src/app/configuration/security-configuration/abstract-security-principal-config.ts
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://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.
+ *
+ */
+
+import { Directive, OnInit, ViewChild } from '@angular/core';
+import { MatPaginator } from '@angular/material/paginator';
+import { MatSort } from '@angular/material/sort';
+import { MatTableDataSource } from '@angular/material/table';
+import { UserService } from '../../platform-services/apis/user.service';
+import { Observable } from 'rxjs';
+import { ServiceAccount, UserAccount } from '../../core-model/gen/streampipes-model-client';
+import { PanelType } from '../../core-ui/dialog/base-dialog/base-dialog.model';
+import { DialogService } from '../../core-ui/dialog/base-dialog/base-dialog.service';
+import { EditUserDialogComponent } from './edit-user-dialog/edit-user-dialog.component';
+
+@Directive()
+export abstract class AbstractSecurityPrincipalConfig<T extends (UserAccount | ServiceAccount)> implements OnInit {
+
+  users: T[] = [];
+
+  @ViewChild(MatPaginator) paginator: MatPaginator;
+  pageSize = 1;
+  @ViewChild(MatSort) sort: MatSort;
+
+  dataSource: MatTableDataSource<T>;
+
+  constructor(protected userService: UserService,
+              protected dialogService: DialogService) {
+  }
+
+  ngOnInit(): void {
+    this.load();
+  }
+
+  openEditDialog(user: (UserAccount | ServiceAccount),
+                 editMode: boolean) {
+    const dialogRef = this.dialogService.open(EditUserDialogComponent, {
+      panelType: PanelType.SLIDE_IN_PANEL,
+      title: editMode ? 'Edit user ' + user.username : 'Add user',
+      width: '50vw',
+      data: {
+        'user': user,
+        'editMode': editMode
+      }
+    });
+
+    dialogRef.afterClosed().subscribe(refresh => {
+      if (refresh) {
+        this.load();
+      }
+    });
+  }
+
+  createUser() {
+    const principal = this.getNewInstance();
+    principal.roles = [];
+    this.openEditDialog(principal, false);
+  }
+
+  load() {
+    this.getObservable().subscribe(response => {
+      this.users = response;
+      this.dataSource = new MatTableDataSource(this.users);
+    });
+  }
+
+  deleteUser(account: UserAccount | ServiceAccount) {
+    this.userService.deleteUser(account.principalId).subscribe(() => {
+      this.load();
+    });
+  }
+
+  abstract getObservable(): Observable<T[]>;
+
+  abstract getNewInstance(): T;
+
+
+}
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
new file mode 100644
index 0000000..4bdc2a3
--- /dev/null
+++ b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.html
@@ -0,0 +1,89 @@
+<div class="sp-dialog-container">
+    <div class="sp-dialog-content">
+        <div fxFlex="100" fxLayout="column" class="p-15">
+            <form [formGroup]="parentForm" fxFlex="100" fxLayout="column">
+                <div class="general-options-panel" fxLayout="column">
+                    <span class="general-options-header">Basics</span>
+                    <mat-form-field color="accent">
+                        <mat-label>Username</mat-label>
+                        <input formControlName="username" fxFlex
+                               matInput
+                               required>
+                    </mat-form-field>
+                    <mat-form-field color="accent" *ngIf="isUserAccount">
+                        <mat-label>Full Name</mat-label>
+                        <input formControlName="fullName" fxFlex
+                               matInput
+                               required>
+                    </mat-form-field>
+                    <mat-form-field color="accent" *ngIf="isUserAccount">
+                        <mat-label>Email</mat-label>
+                        <input formControlName="email" fxFlex
+                               matInput
+                               required>
+                    </mat-form-field>
+                </div>
+                <div fxLayout="column" class="general-options-panel" *ngIf="!editMode && isUserAccount">
+                    <span class="general-options-header">Password</span>
+                    <mat-form-field color="accent">
+                        <mat-label>Initial password</mat-label>
+                        <input formControlName="password" fxFlex
+                               matInput
+                               required>
+                    </mat-form-field>
+                    <mat-form-field color="accent">
+                        <mat-label>Repeat password</mat-label>
+                        <input formControlName="repeatPassword" fxFlex
+                               matInput
+                               required>
+                    </mat-form-field>
+                </div>
+                <div fxLayout="column" class="general-options-panel" *ngIf="!isUserAccount">
+                    <span class="general-options-header">Authentication</span>
+                    <mat-form-field color="accent">
+                        <mat-label>Client Secret</mat-label>
+                        <input formControlName="clientSecret" fxFlex
+                               matInput
+                               required>
+                    </mat-form-field>
+                </div>
+                <div fxLayout="column" class="general-options-panel">
+                    <span class="general-options-header">Groups</span>
+<!--                    <mat-checkbox *ngFor="let role of availableRoles" [value]="role"-->
+<!--                                  [checked]="user.roles.indexOf(role) > -1" (change)="changeRoleAssignment($event)">-->
+<!--                        {{role}}-->
+<!--                    </mat-checkbox>-->
+                </div>
+                <div fxLayout="column" class="general-options-panel">
+                    <span class="general-options-header">Roles</span>
+                <mat-checkbox *ngFor="let role of availableRoles" [value]="role"
+                              [checked]="user.roles.indexOf(role) > -1" (change)="changeRoleAssignment($event)">
+                    {{role}}
+                </mat-checkbox>
+                </div>
+                <div fxLayout="column" class="general-options-panel">
+                    <span class="general-options-header">Account</span>
+                    <mat-checkbox formControlName="accountEnabled">
+                        Enabled
+                    </mat-checkbox>
+                    <mat-checkbox formControlName="accountLocked">
+                        Locked
+                    </mat-checkbox>
+                </div>
+            </form>
+        </div>
+    </div>
+    <mat-divider></mat-divider>
+    <div class="sp-dialog-actions">
+        <div fxLayout="row">
+            <button mat-button mat-raised-button color="accent" (click)="save()" style="margin-right:10px;"
+                    [disabled]="!parentForm.valid || clonedUser.roles.length == 0"
+                    data-cy="sp-element-edit-user-save">
+                <i class="material-icons">save</i><span>&nbsp;Save</span>
+            </button>
+            <button mat-button mat-raised-button class="mat-basic" (click)="close(false)">
+                Cancel
+            </button>
+        </div>
+    </div>
+</div>
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/Subject.java b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.scss
similarity index 83%
rename from streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/Subject.java
rename to ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.scss
index 39caab6..aad1d42 100644
--- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/Subject.java
+++ b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.scss
@@ -16,11 +16,13 @@
  *
  */
 
-package org.apache.streampipes.model.client.security;
+@import '../../../../scss/sp/sp-dialog.scss';
 
-public abstract class Subject {
-
-    protected String subjectId;
+.form-field .mat-form-field-wrapper {
+  margin-bottom: -1.25em;
+}
 
 
+.form-field .mat-form-field-infix {
+  border-top: 0;
 }
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
new file mode 100644
index 0000000..c9cb3f9
--- /dev/null
+++ b/ui/src/app/configuration/security-configuration/edit-user-dialog/edit-user-dialog.component.ts
@@ -0,0 +1,141 @@
+import { AfterViewInit, Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
+import { DialogRef } from '../../../core-ui/dialog/base-dialog/dialog-ref';
+import {
+  Role,
+  ServiceAccount,
+  UserAccount
+} from '../../../core-model/gen/streampipes-model-client';
+import {
+  AbstractControl,
+  FormBuilder,
+  FormControl,
+  FormGroup,
+  ValidationErrors,
+  ValidatorFn,
+  Validators
+} from '@angular/forms';
+import { UserRole } from '../../../_enums/user-role.enum';
+import { MatCheckboxChange } from '@angular/material/checkbox';
+import { UserService } from '../../../platform-services/apis/user.service';
+
+@Component({
+  selector: 'sp-edit-user-dialog',
+  templateUrl: './edit-user-dialog.component.html',
+  styleUrls: ['./edit-user-dialog.component.scss'],
+  encapsulation: ViewEncapsulation.None
+})
+export class EditUserDialogComponent implements OnInit, AfterViewInit {
+
+  @Input()
+  user: any;
+
+  @Input()
+  editMode: boolean;
+
+  isUserAccount: boolean;
+  parentForm: FormGroup;
+  clonedUser: UserAccount | ServiceAccount;
+
+  availableRoles: string[];
+
+  constructor(private dialogRef: DialogRef<EditUserDialogComponent>,
+              private fb: FormBuilder,
+              private userService: UserService) {
+  }
+
+  ngAfterViewInit(): void {
+  }
+
+  ngOnInit(): void {
+    this.availableRoles = Object.values(UserRole).filter(value => typeof value === 'string') as string[];
+    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.parentForm = this.fb.group({});
+    this.parentForm.addControl('username', new FormControl(this.clonedUser.username, Validators.required));
+    this.parentForm.addControl('accountEnabled', new FormControl(this.clonedUser.accountEnabled));
+    this.parentForm.addControl('accountLocked', new FormControl(this.clonedUser.accountLocked));
+    if (this.clonedUser instanceof UserAccount) {
+      this.parentForm.addControl('email', new FormControl(this.clonedUser.email));
+      this.parentForm.addControl('fullName', new FormControl(this.clonedUser.fullName));
+    } else {
+      this.parentForm.addControl('clientSecret', new FormControl(this.clonedUser.clientSecret));
+    }
+
+    if (!this.editMode && this.isUserAccount) {
+      this.parentForm.addControl('password', new FormControl(this.clonedUser.password, Validators.required));
+      this.parentForm.addControl('repeatPassword', new FormControl());
+      this.parentForm.setValidators(this.checkPasswords);
+    }
+
+    this.parentForm.valueChanges.subscribe(v => {
+      this.clonedUser.username = v.username;
+      this.clonedUser.accountLocked = v.accountLocked;
+      this.clonedUser.accountEnabled = v.accountEnabled;
+      if (this.clonedUser instanceof UserAccount) {
+        this.clonedUser.email = v.email;
+        this.clonedUser.fullName = v.fullName;
+      } else {
+        this.clonedUser.clientSecret = v.clientSecret;
+      }
+      if (!this.editMode) {
+        this.clonedUser.password = v.password;
+      }
+    });
+
+  }
+
+  checkPasswords: ValidatorFn = (group: AbstractControl):  ValidationErrors | null => {
+    const pass = group.get('password');
+    const confirmPass = group.get('repeatPassword');
+
+    if (!pass || !confirmPass) {
+      return null;
+    }
+    return pass.value === confirmPass.value ? null : { notMatching: true };
+  }
+
+  save() {
+    if (this.editMode) {
+      if (this.isUserAccount) {
+        this.userService.updateUser(this.clonedUser as UserAccount).subscribe(() => {
+          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);
+        });
+      } else {
+        this.userService.createServiceAccount(this.clonedUser as ServiceAccount).subscribe(() => {
+          this.close(true);
+        });
+      }
+    }
+  }
+
+  close(refresh: boolean) {
+    this.dialogRef.close(refresh);
+  }
+
+  changeRoleAssignment(event: MatCheckboxChange) {
+    if (this.clonedUser.roles.indexOf(event.source.value as Role) > -1) {
+      this.removeRole(event.source.value);
+    } else {
+      this.addRole(event.source.value);
+    }
+  }
+
+  removeRole(role: string) {
+    this.clonedUser.roles.splice(this.clonedUser.roles.indexOf(role as Role), 1);
+  }
+
+  addRole(role: string) {
+    this.clonedUser.roles.push(role as Role);
+  }
+
+}
diff --git a/ui/src/app/configuration/security-configuration/security-configuration.component.html b/ui/src/app/configuration/security-configuration/security-configuration.component.html
index e40d375..a1c9218 100644
--- a/ui/src/app/configuration/security-configuration/security-configuration.component.html
+++ b/ui/src/app/configuration/security-configuration/security-configuration.component.html
@@ -16,3 +16,21 @@
   ~
   -->
 
+<div fxLayout="column" class="page-container-padding">
+    <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start">
+        <sp-split-section title="User Accounts"
+                          subtitle="Add and edit user accounts">
+            <div class="subsection-title">Existing user accounts</div>
+            <sp-security-user-config></sp-security-user-config>
+        </sp-split-section>
+    </div>
+    <mat-divider></mat-divider>
+    <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start">
+        <sp-split-section title="Service Accounts"
+                          subtitle="Add and edit service accounts">
+            <div class="subsection-title">Existing service accounts</div>
+            <sp-security-service-config></sp-security-service-config>
+        </sp-split-section>
+    </div>
+    <mat-divider></mat-divider>
+</div>
diff --git a/ui/src/app/configuration/security-configuration/security-configuration.component.ts b/ui/src/app/configuration/security-configuration/security-configuration.component.ts
index a4d9ed8..427c982 100644
--- a/ui/src/app/configuration/security-configuration/security-configuration.component.ts
+++ b/ui/src/app/configuration/security-configuration/security-configuration.component.ts
@@ -24,6 +24,9 @@ import { Component, OnInit } from '@angular/core';
   styleUrls: ['./security-configuration.component.scss']
 })
 export class SecurityConfigurationComponent implements OnInit {
+
+  constructor() {}
+
   ngOnInit(): void {
   }
 
diff --git a/ui/src/app/configuration/security-configuration/security-service-configuration/security-service-config.component.html b/ui/src/app/configuration/security-configuration/security-service-configuration/security-service-config.component.html
new file mode 100644
index 0000000..948ffc0
--- /dev/null
+++ b/ui/src/app/configuration/security-configuration/security-service-configuration/security-service-config.component.html
@@ -0,0 +1,77 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~    http://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.
+  ~
+  -->
+
+<div fxLayout="column">
+    <div>
+        <button mat-button mat-raised-button color="accent" (click)="createUser()"><i
+                class="material-icons">add</i><span>&nbsp;New Service Account</span></button>
+    </div>
+    <div fxFlex="100" fxLayout="column">
+        <table
+                fxFlex="100"
+                mat-table
+                data-cy="security-service-config"
+                [dataSource]="dataSource"
+                style="width: 100%;"
+                matSort>
+
+            <ng-container matColumnDef="username">
+                <th mat-header-cell mat-sort-header *matHeaderCellDef>Username</th>
+                <td mat-cell *matCellDef="let account">
+                    <h4 style="margin-bottom:0px;">{{account.username}}</h4>
+                </td>
+            </ng-container>
+
+            <ng-container matColumnDef="edit">
+                <th mat-header-cell *matHeaderCellDef class="text-right">Actions</th>
+                <td mat-cell *matCellDef="let account">
+                    <div fxLayout="row">
+                            <span fxFlex fxFlexOrder="3" fxLayout="row" fxLayoutAlign="end center">
+                                <div class="mr-15">
+                                <button color="accent"
+                                        mat-button
+                                        mat-raised-button
+                                        matTooltip="Edit user"
+                                        matTooltipPosition="above"
+                                        data-cy="service-edit-btn"
+                                        (click)="editService(account)">
+                                        <i class="material-icons">edit</i>
+                                        <span>&nbsp;Edit</span>
+                                    </button>
+                                    </div>
+                                <button color="warn"
+                                        mat-button
+                                        mat-raised-button
+                                        matTooltip="Delete service"
+                                        matTooltipPosition="above"
+                                        data-cy="service-delete-btn"
+                                        (click)="deleteUser(account)">
+                                        <i class="material-icons">delete</i>
+                                        <span>&nbsp;Delete</span>
+                                    </button>
+                                </span>
+                    </div>
+                </td>
+            </ng-container>
+
+            <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+            <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+
+        </table>
+    </div>
+</div>
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/User.java b/ui/src/app/configuration/security-configuration/security-service-configuration/security-service-config.component.scss
similarity index 91%
rename from streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/User.java
rename to ui/src/app/configuration/security-configuration/security-service-configuration/security-service-config.component.scss
index 5006702..4c18ca4 100644
--- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/User.java
+++ b/ui/src/app/configuration/security-configuration/security-service-configuration/security-service-config.component.scss
@@ -16,7 +16,6 @@
  *
  */
 
-package org.apache.streampipes.model.client.security;
-
-public class User {
+.text-right {
+  text-align: right;
 }
diff --git a/ui/src/app/configuration/security-configuration/security-service-configuration/security-service-config.component.ts b/ui/src/app/configuration/security-configuration/security-service-configuration/security-service-config.component.ts
new file mode 100644
index 0000000..e6eff68
--- /dev/null
+++ b/ui/src/app/configuration/security-configuration/security-service-configuration/security-service-config.component.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://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.
+ *
+ */
+
+import { Component } from '@angular/core';
+import { ServiceAccount } from '../../../core-model/gen/streampipes-model-client';
+import { AbstractSecurityPrincipalConfig } from '../abstract-security-principal-config';
+import { Observable } from 'rxjs';
+
+@Component({
+  selector: 'sp-security-service-config',
+  templateUrl: './security-service-config.component.html',
+  styleUrls: ['./security-service-config.component.scss']
+})
+export class SecurityServiceConfigComponent extends AbstractSecurityPrincipalConfig<ServiceAccount> {
+
+
+  displayedColumns: string[] = ['username', 'edit'];
+
+  getObservable(): Observable<ServiceAccount[]> {
+    return this.userService.getAllServiceAccounts();
+  }
+
+  editService(account: ServiceAccount) {
+    this.openEditDialog(account, true);
+  }
+
+  getNewInstance(): ServiceAccount {
+    return new ServiceAccount();
+  }
+
+
+}
diff --git a/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.html b/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.html
new file mode 100644
index 0000000..1120c9d
--- /dev/null
+++ b/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.html
@@ -0,0 +1,94 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~    http://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.
+  ~
+  -->
+
+<div fxLayout="column">
+    <div>
+        <button mat-button mat-raised-button color="accent" (click)="createUser()"><i
+                class="material-icons">add</i><span>&nbsp;New User</span></button>
+    </div>
+    <div fxFlex="100" fxLayout="column">
+        <table
+                fxFlex="100"
+                mat-table
+                data-cy="security-user-config"
+                [dataSource]="dataSource"
+                style="width: 100%;"
+                matSort>
+
+            <ng-container matColumnDef="username">
+                <th mat-header-cell mat-sort-header *matHeaderCellDef>Username</th>
+                <td mat-cell *matCellDef="let account">
+                    <h4 style="margin-bottom:0px;">{{account.username}}</h4>
+                </td>
+            </ng-container>
+
+            <ng-container matColumnDef="fullName">
+                <th mat-header-cell mat-sort-header *matHeaderCellDef>Full Name</th>
+                <td mat-cell *matCellDef="let account">
+                    {{account.fullName}}
+                </td>
+            </ng-container>
+
+            <ng-container matColumnDef="email">
+                <th mat-header-cell mat-sort-header *matHeaderCellDef>Email</th>
+                <td
+                        mat-cell
+                        data-cy="security-email"
+                        *matCellDef="let account">
+                    {{account.email}}
+                </td>
+            </ng-container>
+
+            <ng-container matColumnDef="edit">
+                <th mat-header-cell *matHeaderCellDef class="text-right">Actions</th>
+                <td mat-cell *matCellDef="let account">
+                    <div fxLayout="row">
+                            <span fxFlex fxFlexOrder="3" fxLayout="row" fxLayoutAlign="end center">
+                                <div class="mr-15">
+                                <button color="accent"
+                                        mat-button
+                                        mat-raised-button
+                                        matTooltip="Edit user"
+                                        matTooltipPosition="above"
+                                        data-cy="user-edit-btn"
+                                        (click)="editUser(account)">
+                                        <i class="material-icons">edit</i>
+                                        <span>&nbsp;Edit</span>
+                                    </button>
+                                    </div>
+                                <button color="warn"
+                                        mat-button
+                                        mat-raised-button
+                                        matTooltip="Delete user"
+                                        matTooltipPosition="above"
+                                        data-cy="user-delete-btn"
+                                        (click)="deleteUser(account)">
+                                        <i class="material-icons">delete</i>
+                                        <span>&nbsp;Delete</span>
+                                    </button>
+                                </span>
+                    </div>
+                </td>
+            </ng-container>
+
+            <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+            <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+
+        </table>
+    </div>
+</div>
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/Role.java b/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.scss
similarity index 91%
rename from streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/Role.java
rename to ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.scss
index 99d223f..4c18ca4 100644
--- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/security/Role.java
+++ b/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.scss
@@ -16,7 +16,6 @@
  *
  */
 
-package org.apache.streampipes.model.client.security;
-
-public class Role {
+.text-right {
+  text-align: right;
 }
diff --git a/ui/src/app/configuration/security-configuration/security-configuration.component.ts b/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.ts
similarity index 50%
copy from ui/src/app/configuration/security-configuration/security-configuration.component.ts
copy to ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.ts
index a4d9ed8..39d34f1 100644
--- a/ui/src/app/configuration/security-configuration/security-configuration.component.ts
+++ b/ui/src/app/configuration/security-configuration/security-user-configuration/security-user-config.component.ts
@@ -16,15 +16,34 @@
  *
  */
 
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
+import { UserAccount } from '../../../core-model/gen/streampipes-model-client';
+import { AbstractSecurityPrincipalConfig } from '../abstract-security-principal-config';
+import { Observable } from 'rxjs';
 
 @Component({
-  selector: 'sp-security-configuration',
-  templateUrl: './security-configuration.component.html',
-  styleUrls: ['./security-configuration.component.scss']
+  selector: 'sp-security-user-config',
+  templateUrl: './security-user-config.component.html',
+  styleUrls: ['./security-user-config.component.scss']
 })
-export class SecurityConfigurationComponent implements OnInit {
-  ngOnInit(): void {
+export class SecurityUserConfigComponent extends AbstractSecurityPrincipalConfig<UserAccount> {
+
+
+  displayedColumns: string[] = ['username', 'fullName', 'email', 'edit'];
+
+
+  getObservable(): Observable<UserAccount[]> {
+    return this.userService.getAllUserAccounts();
+  }
+
+  editUser(account: UserAccount) {
+    this.openEditDialog(account, true);
+  }
+
+  getNewInstance(): UserAccount {
+    return new UserAccount();
   }
 
+
+
 }
diff --git a/ui/src/app/core-model/gen/streampipes-model-client.ts b/ui/src/app/core-model/gen/streampipes-model-client.ts
index f6b10b4..1564e3a 100644
--- a/ui/src/app/core-model/gen/streampipes-model-client.ts
+++ b/ui/src/app/core-model/gen/streampipes-model-client.ts
@@ -19,7 +19,7 @@
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 2.27.744 on 2021-10-01 13:23:19.
+// Generated using typescript-generator version 2.27.744 on 2021-10-04 22:00:31.
 
 export class Element {
     elementId: string;
@@ -139,7 +139,7 @@ export class Principal implements UserDetails {
     ownSources: Element[];
     password: string;
     principalId: string;
-    principalName: string;
+    principalType: PrincipalType;
     rev: string;
     roles: Role[];
     username: string;
@@ -153,19 +153,19 @@ export class Principal implements UserDetails {
         instance.password = data.password;
         instance.username = data.username;
         instance.authorities = __getCopyArrayFn(__identity<GrantedAuthority>())(data.authorities);
+        instance.accountNonLocked = data.accountNonLocked;
         instance.accountNonExpired = data.accountNonExpired;
         instance.credentialsNonExpired = data.credentialsNonExpired;
-        instance.accountNonLocked = data.accountNonLocked;
         instance.principalId = data.principalId;
         instance.rev = data.rev;
         instance.accountEnabled = data.accountEnabled;
         instance.accountLocked = data.accountLocked;
         instance.accountExpired = data.accountExpired;
-        instance.principalName = data.principalName;
         instance.ownSources = __getCopyArrayFn(Element.fromData)(data.ownSources);
         instance.ownSepas = __getCopyArrayFn(Element.fromData)(data.ownSepas);
         instance.ownActions = __getCopyArrayFn(Element.fromData)(data.ownActions);
         instance.roles = __getCopyArrayFn(__identity<Role>())(data.roles);
+        instance.principalType = data.principalType;
         return instance;
     }
 }
@@ -189,6 +189,20 @@ export class RawUserApiToken {
     }
 }
 
+export class ServiceAccount extends Principal {
+    clientSecret: string;
+
+    static fromData(data: ServiceAccount, target?: ServiceAccount): ServiceAccount {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new ServiceAccount();
+        super.fromData(data, instance);
+        instance.clientSecret = data.clientSecret;
+        return instance;
+    }
+}
+
 export class UserAccount extends Principal {
     darkMode: boolean;
     email: string;
@@ -248,14 +262,14 @@ export class UserInfo {
     email: string;
     roles: string[];
     showTutorial: boolean;
-    userId: string;
+    username: string;
 
     static fromData(data: UserInfo, target?: UserInfo): UserInfo {
         if (!data) {
             return data;
         }
         const instance = target || new UserInfo();
-        instance.userId = data.userId;
+        instance.username = data.username;
         instance.displayName = data.displayName;
         instance.email = data.email;
         instance.roles = __getCopyArrayFn(__identity<string>())(data.roles);
@@ -265,7 +279,9 @@ export class UserInfo {
     }
 }
 
-export type Role = "SYSTEM_ADMINISTRATOR" | "MANAGER" | "OPERATOR" | "DIMENSION_OPERATOR" | "USER_DEMO" | "BUSINESS_ANALYST";
+export type PrincipalType = "USER_ACCOUNT" | "SERVICE_ACCOUNT";
+
+export type Role = "ADMIN" | "PIPELINE_ADMIN" | "DASHBOARD_ADMIN" | "DATA_EXPLORER_ADMIN" | "CONNECT_ADMIN" | "DASHBOARD_USER" | "DATA_EXPLORER_USER" | "PIPELINE_USER" | "APP_USER";
 
 function __getCopyArrayFn<T>(itemCopyFn: (item: T) => T): (array: T[]) => T[] {
     return (array: T[]) => __copyArray(array, itemCopyFn);
diff --git a/ui/src/app/core-ui/split-section/split-section.component.scss b/ui/src/app/core-ui/split-section/split-section.component.scss
index 2d75a6f..fa1ede2 100644
--- a/ui/src/app/core-ui/split-section/split-section.component.scss
+++ b/ui/src/app/core-ui/split-section/split-section.component.scss
@@ -17,7 +17,9 @@
  */
 
 :host {
+  margin-top: 20px;
   width: 100%;
+  margin-bottom: 20px;
 }
 
 .split-section-title {
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.ts b/ui/src/app/core/components/toolbar/toolbar.component.ts
index 3cd5c33..ee52a28 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.ts
+++ b/ui/src/app/core/components/toolbar/toolbar.component.ts
@@ -55,7 +55,7 @@ export class ToolbarComponent extends BaseNavigationComponent implements OnInit
     this.getVersion();
     this.authService.user$.subscribe(user => {
       this.userEmail = user.displayName;
-      this.profileService.getUserProfile().subscribe(userInfo => {
+      this.profileService.getUserProfile(user.username).subscribe(userInfo => {
         this.darkMode = this.authService.darkMode$.getValue();
         this.modifyAppearance(userInfo.darkMode);
       });
diff --git a/ui/src/app/platform-services/apis/user.service.ts b/ui/src/app/platform-services/apis/user.service.ts
new file mode 100644
index 0000000..56e9979
--- /dev/null
+++ b/ui/src/app/platform-services/apis/user.service.ts
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://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.
+ *
+ */
+
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { map } from 'rxjs/operators';
+import { Observable } from 'rxjs';
+import { PlatformServicesCommons } from './commons.service';
+import { ServiceAccount, UserAccount } from '../../core-model/gen/streampipes-model-client';
+
+@Injectable()
+export class UserService {
+
+  constructor(private http: HttpClient,
+              private platformServicesCommons: PlatformServicesCommons) {
+  }
+
+  getAllUserAccounts(): Observable<UserAccount[]> {
+    return this.http.get(`${this.usersPath}?type=USER_ACCOUNT`)
+        .pipe(map(response => {
+          return (response as any[]).map(p => UserAccount.fromData(p));
+        }));
+  }
+
+  getAllServiceAccounts(): Observable<ServiceAccount[]> {
+    return this.http.get(`${this.usersPath}?type=SERVICE_ACCOUNT`)
+        .pipe(map(response => {
+          return (response as any[]).map(p => ServiceAccount.fromData(p));
+        }));
+  }
+
+  public updateUser(user: (UserAccount)): Observable<any> {
+    return this.http.put(`${this.usersPath}/user/${user.principalId}`, user);
+  }
+
+  public updateService(user: (ServiceAccount)): Observable<any> {
+    return this.http.put(`${this.usersPath}/service/${user.principalId}`, user);
+  }
+
+  public createUser(user: UserAccount): Observable<any> {
+    return this.http.post(`${this.usersPath}/user`, user);
+  }
+
+  public createServiceAccount(user: ServiceAccount): Observable<any> {
+    return this.http.post(`${this.usersPath}/service`, user);
+  }
+
+  public deleteUser(principalId: string): Observable<any> {
+    return this.http.delete(`${this.usersPath}/${principalId}`);
+  }
+
+  private get usersPath() {
+    return this.platformServicesCommons.apiBasePath + '/users';
+  }
+}
diff --git a/ui/src/app/platform-services/platform.module.ts b/ui/src/app/platform-services/platform.module.ts
index ee9e3a4..8a3ed07 100644
--- a/ui/src/app/platform-services/platform.module.ts
+++ b/ui/src/app/platform-services/platform.module.ts
@@ -28,6 +28,7 @@ import { PipelineMonitoringService } from './apis/pipeline-monitoring.service';
 import { SemanticTypesService } from './apis/semantic-types.service';
 import { PipelineCanvasMetadataService } from './apis/pipeline-canvas-metadata.service';
 import { PipelineTemplateService } from './apis/pipeline-template.service';
+import { UserService } from './apis/user.service';
 
 @NgModule({
   imports: [],
@@ -43,7 +44,8 @@ import { PipelineTemplateService } from './apis/pipeline-template.service';
     PipelineMonitoringService,
     PipelineService,
     SemanticTypesService,
-    PipelineTemplateService
+    PipelineTemplateService,
+    UserService
   ],
   entryComponents: []
 })
diff --git a/ui/src/app/profile/components/basic-profile-settings.ts b/ui/src/app/profile/components/basic-profile-settings.ts
index cb9cc33..ea7ad9b 100644
--- a/ui/src/app/profile/components/basic-profile-settings.ts
+++ b/ui/src/app/profile/components/basic-profile-settings.ts
@@ -21,6 +21,7 @@ import { UserAccount } from '../../core-model/gen/streampipes-model-client';
 import { Directive } from '@angular/core';
 import { AppConstants } from '../../services/app.constants';
 import { JwtTokenStorageService } from '../../services/jwt-token-storage.service';
+import { AuthService } from '../../services/auth.service';
 
 @Directive()
 export abstract class BasicProfileSettings {
@@ -32,13 +33,14 @@ export abstract class BasicProfileSettings {
 
   constructor(protected profileService: ProfileService,
               public appConstants: AppConstants,
-              private tokenService: JwtTokenStorageService) {
+              private tokenService: JwtTokenStorageService,
+              protected authService: AuthService) {
 
   }
 
   receiveUserData() {
     this.profileService
-        .getUserProfile()
+        .getUserProfile(this.authService.user$.getValue().username)
         .subscribe(userData => {
           this.userData = userData;
           this.onUserDataReceived();
diff --git a/ui/src/app/profile/components/general/general-profile-settings.component.ts b/ui/src/app/profile/components/general/general-profile-settings.component.ts
index 8cefffa..1f0fa45 100644
--- a/ui/src/app/profile/components/general/general-profile-settings.component.ts
+++ b/ui/src/app/profile/components/general/general-profile-settings.component.ts
@@ -34,11 +34,11 @@ export class GeneralProfileSettingsComponent extends BasicProfileSettings implem
   originalDarkMode = false;
   darkModeChanged = false;
 
-  constructor(private authService: AuthService,
+  constructor(authService: AuthService,
               profileService: ProfileService,
               appConstants: AppConstants,
               tokenService: JwtTokenStorageService) {
-    super(profileService, appConstants, tokenService);
+    super(profileService, appConstants, tokenService, authService);
   }
 
   ngOnInit(): void {
@@ -62,7 +62,7 @@ export class GeneralProfileSettingsComponent extends BasicProfileSettings implem
   }
 
   updateAppearanceMode() {
-    this.profileService.updateAppearanceMode(this.darkMode).subscribe(response => {
+    this.profileService.updateAppearanceMode(this.userData.username, this.darkMode).subscribe(response => {
       this.darkModeChanged = true;
     });
   }
diff --git a/ui/src/app/profile/components/token/token-management-settings.component.ts b/ui/src/app/profile/components/token/token-management-settings.component.ts
index dfced55..d44e866 100644
--- a/ui/src/app/profile/components/token/token-management-settings.component.ts
+++ b/ui/src/app/profile/components/token/token-management-settings.component.ts
@@ -16,10 +16,10 @@
  *
  */
 
-import {Component, OnInit} from "@angular/core";
-import {BasicProfileSettings} from "../basic-profile-settings";
-import {RawUserApiToken, UserApiToken} from "../../../core-model/gen/streampipes-model-client";
-import {MatTableDataSource} from "@angular/material/table";
+import { Component, OnInit } from "@angular/core";
+import { BasicProfileSettings } from "../basic-profile-settings";
+import { RawUserApiToken, UserApiToken } from "../../../core-model/gen/streampipes-model-client";
+import { MatTableDataSource } from "@angular/material/table";
 
 @Component({
   selector: 'token-management-settings',
@@ -41,7 +41,7 @@ export class TokenManagementSettingsComponent extends BasicProfileSettings imple
 
   requestNewKey() {
     let baseToken: RawUserApiToken = this.makeBaseToken();
-    this.profileService.requestNewApiToken(baseToken).subscribe(result => {
+    this.profileService.requestNewApiToken(this.userData.username, baseToken).subscribe(result => {
       this.newlyCreatedToken = result;
       this.newTokenCreated = true;
       this.newTokenName = "";
@@ -60,7 +60,7 @@ export class TokenManagementSettingsComponent extends BasicProfileSettings imple
     this.userData.userApiTokens.splice(removeIndex, 1);
     this.profileService.updateUserProfile(this.userData).subscribe(response => {
       this.receiveUserData();
-    })
+    });
   }
 
   onUserDataReceived() {
diff --git a/ui/src/app/profile/profile.service.ts b/ui/src/app/profile/profile.service.ts
index 262e89c..dc4b7a6 100644
--- a/ui/src/app/profile/profile.service.ts
+++ b/ui/src/app/profile/profile.service.ts
@@ -32,32 +32,33 @@ export class ProfileService {
 
   }
 
-  getUserProfile(): Observable<UserAccount> {
-    return this.http.get(this.profilePath).pipe(map(response => {
+  getUserProfile(username: string): Observable<UserAccount> {
+    return this.http.get(this.profilePath + '/' + username).pipe(map(response => {
       return UserAccount.fromData(response as any);
     }));
   }
 
   updateUserProfile(userData: UserAccount): Observable<Message> {
-    return this.http.put(this.profilePath, userData).pipe(map(response => {
+    return this.http.put(this.profilePath + '/' + userData.username, userData).pipe(map(response => {
       return Message.fromData(response as any);
     }));
   }
 
-  updateAppearanceMode(darkMode: boolean): Observable<Message> {
-    return this.http.put(`${this.profilePath}/appearance/mode/${darkMode}`, {}).pipe(map(response => {
+  updateAppearanceMode(username, darkMode: boolean): Observable<Message> {
+    return this.http.put(`${this.profilePath}/${username}/appearance/mode/${darkMode}`, {}).pipe(map(response => {
       return Message.fromData(response as any);
     }));
   }
 
-  requestNewApiToken(baseToken: RawUserApiToken): Observable<RawUserApiToken> {
-    return this.http.post(this.profilePath + '/tokens', baseToken)
+  requestNewApiToken(username: string,
+                     baseToken: RawUserApiToken): Observable<RawUserApiToken> {
+    return this.http.post(this.profilePath + '/' + username + '/tokens', baseToken)
         .pipe(map(response => {
           return RawUserApiToken.fromData(response as any);
         }));
   }
 
   private get profilePath(): string {
-    return this.platformServicesCommons.apiBasePath + '/users/profile';
+    return this.platformServicesCommons.apiBasePath + '/users';
   }
 }
diff --git a/ui/src/app/services/auth.service.ts b/ui/src/app/services/auth.service.ts
index cf2e33c..c6eff77 100644
--- a/ui/src/app/services/auth.service.ts
+++ b/ui/src/app/services/auth.service.ts
@@ -55,7 +55,6 @@ export class AuthService {
     public login(data) {
         const jwtHelper: JwtHelperService = new JwtHelperService({});
         const decodedToken = jwtHelper.decodeToken(data.accessToken);
-        console.log(decodedToken);
         this.tokenStorage.saveToken(data.accessToken);
         this.tokenStorage.saveUser(decodedToken.user);
         this.authToken$.next(data.accessToken);
@@ -145,14 +144,10 @@ export class AuthService {
         if (!result) {
             this.router.navigate(['']);
         }
-        console.log(pageNames);
-        console.log(result);
         return result;
     }
 
     isAccessGranted(pageName: PageName) {
-        console.log(pageName);
-        console.log(this.hasRole(UserRole.ADMIN));
         if (this.hasRole(UserRole.ADMIN)) {
             return true;
         }
diff --git a/ui/src/scss/sp/widgets.scss b/ui/src/scss/sp/widgets.scss
index 26b6539..58f8c6e 100644
--- a/ui/src/scss/sp/widgets.scss
+++ b/ui/src/scss/sp/widgets.scss
@@ -39,3 +39,17 @@
   overflow-y: auto;
   overflow-x: auto;
 }
+
+.general-options-panel {
+  margin-top: 4px;
+  margin-bottom: 4px;
+  border: 1px solid var(--color-bg-3);
+  background: var(--color-bg-1);
+  padding: 5px;
+}
+
+.general-options-header {
+  margin-right: 10px;
+  margin-bottom: 10px;
+  font-weight: bold;
+}