diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java index bfc843edb4..9b6262fa60 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java @@ -70,6 +70,7 @@ public Map getConfigurationParameters() { return Collections.emptyMap(); } + @NotNull public String[] getTeams() { return user.getUserTeams(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index ac154189aa..340da012c0 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -685,6 +685,10 @@ public List getAvailableAuthRoles() { return List.of(); } + public List getAvailableTeamRoles() { + return List.of(); + } + @Override public WSEventController getEventController() { return eventController; diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index 586a0e8f96..559b9b90e4 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -11,6 +11,11 @@ type AdminConnectionGrantInfo { subjectType: AdminSubjectType! } +type AdminUserTeamGrantInfo @since(version: "24.0.5"){ + userId: ID! + teamRole: String +} + type AdminObjectPermissions { objectId: ID! permissions: [String!]! @@ -53,6 +58,7 @@ type AdminTeamInfo { metaParameters: Object! grantedUsers: [ID!]! + grantedUsersInfo: [AdminUserTeamGrantInfo!]! @since(version: "24.0.5") grantedConnections: [AdminConnectionGrantInfo!]! teamPermissions: [ID!]! @@ -128,6 +134,7 @@ extend type Query { listTeams(teamId: ID): [AdminTeamInfo!]! listPermissions: [AdminPermissionInfo!]! listAuthRoles: [String!]! + listTeamRoles: [String!]! listTeamMetaParameters: [ObjectPropertyInfo!]! createUser(userId: ID!, enabled: Boolean!, authRole: String): AdminUserInfo! @@ -150,6 +157,8 @@ extend type Query { setUserAuthRole(userId: ID!, authRole: String): Boolean + setUserTeamRole(userId: ID!, teamId: ID!, teamRole: String): Boolean @since(version: "24.0.5") + #### Connection management # All connection configurations diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminTeamInfo.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminTeamInfo.java index 3df4023bbe..6951cbf6b0 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminTeamInfo.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminTeamInfo.java @@ -21,6 +21,7 @@ import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.security.SMDataSourceGrant; import org.jkiss.dbeaver.model.security.SMObjectType; +import org.jkiss.dbeaver.model.security.SMTeamMemberInfo; import org.jkiss.dbeaver.model.security.user.SMTeam; import java.util.ArrayList; @@ -88,4 +89,8 @@ public String[] getGrantedUsers() throws DBException { return session.getAdminSecurityController().getTeamMembers(getTeamId()); } + @Property + public List getGrantedUsersInfo() throws DBException { + return session.getAdminSecurityController().getTeamMembersInfo(getTeamId()); + } } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java index 19271b41f5..f23808efa2 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java @@ -69,6 +69,9 @@ AdminUserInfo createUser( @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) List listAuthRoles(); + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + List listTeamRoles(); + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) boolean deleteUser(@NotNull WebSession webSession, String userName) throws DBWebException; @@ -202,4 +205,10 @@ WebPropertyInfo saveUserMetaParameter(WebSession webSession, String id, String d @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) Boolean setUserAuthRole(WebSession webSession, String userId, String authRole) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + Boolean setUserTeamRole( + @NotNull WebSession webSession, @NotNull String userId, + @NotNull String teamId, @Nullable String teamRole + ) throws DBWebException; + } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java index 9bbc7895ad..973f010f6d 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java @@ -60,6 +60,8 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env -> getService(env).listPermissions(getWebSession(env))) .dataFetcher("listAuthRoles", env -> getService(env).listAuthRoles()) + .dataFetcher("listTeamRoles", + env -> getService(env).listTeamRoles()) .dataFetcher("listTeamMetaParameters", env -> getService(env).listTeamMetaParameters(getWebSession(env))) .dataFetcher("createUser", @@ -106,6 +108,14 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env -> getService(env).enableUser(getWebSession(env), env.getArgument("userId"), env.getArgument("enabled"))) .dataFetcher("setUserAuthRole", env -> getService(env).setUserAuthRole(getWebSession(env), env.getArgument("userId"), env.getArgument("authRole"))) + .dataFetcher("setUserTeamRole", + env -> getService(env).setUserTeamRole( + getWebSession(env), + env.getArgument("userId"), + env.getArgument("teamId"), + env.getArgument("teamRole") + ) + ) .dataFetcher("searchConnections", env -> getService(env).searchConnections(getWebSession(env), env.getArgument("hostNames"))) .dataFetcher("getConnectionSubjectAccess", env -> getService(env).getConnectionSubjectAccess( diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index 69c203f7c8..b7a32416a6 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -173,6 +173,11 @@ public List listAuthRoles() { return CBApplication.getInstance().getAvailableAuthRoles(); } + @Override + public List listTeamRoles() { + return CBApplication.getInstance().getAvailableTeamRoles(); + } + @Override public boolean deleteUser(@NotNull WebSession webSession, String userName) throws DBWebException { if (CommonUtils.equalObjects(userName, webSession.getUser().getUserId())) { @@ -392,6 +397,21 @@ public Boolean setUserAuthRole(WebSession webSession, String userId, String auth } } + @Override + public Boolean setUserTeamRole( + @NotNull WebSession webSession, + @NotNull String userId, + @NotNull String teamId, + @Nullable String teamRole + ) throws DBWebException { + try { + webSession.getAdminSecurityController().setUserTeamRole(userId, teamId, teamRole); + return true; + } catch (Exception e) { + throw new DBWebException("Error updating user auth role", e); + } + } + //////////////////////////////////////////////////////////////////// // Connection management diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index 6ffefdf0f7..eb5b4b4d43 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -134,7 +134,14 @@ type UserInfo { metaParameters: Object! # User configuration parameters configurationParameters: Object! + # User teams + teams: [UserTeamInfo!]! +} +type UserTeamInfo { + teamId: String! + teamName: String! + teamRole: String } extend type Query { diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java index 7232a030de..8e9c44e00c 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java @@ -19,6 +19,7 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebUser; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.meta.Property; @@ -88,4 +89,19 @@ public Map getConfigurationParameters() throws DBWebException { return session.getUserContext().getPreferenceStore().getCustomUserParameters(); } + @NotNull + @Property + public List getTeams() throws DBWebException { + if (session.getUserContext().isNonAnonymousUserAuthorizedInSM()) { + try { + return Arrays.stream(session.getSecurityController().getCurrentUserTeams()) + .map(WebUserTeamInfo::new) + .toList(); + } catch (DBException e) { + throw new DBWebException("Error reading user's teams", e); + } + } else { + return List.of(); + } + } } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserTeamInfo.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserTeamInfo.java new file mode 100644 index 0000000000..029d0dd08a --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserTeamInfo.java @@ -0,0 +1,49 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed 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 io.cloudbeaver.service.auth; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.security.user.SMUserTeam; + +public class WebUserTeamInfo { + @NotNull + private final SMUserTeam userTeam; + + public WebUserTeamInfo(@NotNull SMUserTeam userTeam) { + this.userTeam = userTeam; + } + + @NotNull + @Property + public String getTeamId() { + return userTeam.getTeamId(); + } + + @NotNull + @Property + public String getTeamName() { + return userTeam.getTeamName(); + } + + @Nullable + @Property + public String getTeamRole() { + return userTeam.getTeamRole(); + } +} diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql index af81539fcf..3e290e80ea 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql @@ -130,6 +130,7 @@ CREATE TABLE {table_prefix}CB_USER_TEAM ( USER_ID VARCHAR(128) NOT NULL, TEAM_ID VARCHAR(128) NOT NULL, + TEAM_ROLE VARCHAR(128), GRANT_TIME TIMESTAMP NOT NULL, GRANTED_BY VARCHAR(128) NOT NULL, diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_20.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_20.sql new file mode 100644 index 0000000000..230dfbec43 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_20.sql @@ -0,0 +1,2 @@ +ALTER TABLE {table_prefix}CB_USER_TEAM + ADD TEAM_ROLE VARCHAR(128) NULL; diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index 0c03282900..c39e8a7d1f 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -19,6 +19,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; +import io.cloudbeaver.DBWConstants; import io.cloudbeaver.auth.*; import io.cloudbeaver.model.app.WebAppConfiguration; import io.cloudbeaver.model.app.WebAuthApplication; @@ -162,7 +163,7 @@ public void createUser( dbStat.execute(); } saveSubjectMetas(dbCon, userId, metaParameters); - String defaultTeamName = application.getAppConfiguration().getDefaultUserTeam(); + String defaultTeamName = getDefaultUserTeam(); if (!CommonUtils.isEmpty(defaultTeamName)) { setUserTeams(dbCon, userId, new String[]{defaultTeamName}, userId); } @@ -223,23 +224,72 @@ public void setUserTeams(String userId, String[] teamIds, String grantorId) thro addSubjectPermissionsUpdateEvent(userId, SMSubjectType.user); } - public void setUserTeams(@NotNull Connection dbCon, String userId, String[] teamIds, String grantorId) + @Override + public void setUserTeamRole( + @NotNull String userId, + @NotNull String teamId, + @Nullable String teamRole + ) throws DBException { + if (!isSubjectExists(userId)) { + throw new DBCException("User '" + userId + "' doesn't exists"); + } + if (!isSubjectExists(teamId)) { + throw new DBCException("Team '" + teamId + "' doesn't exists"); + } + + try ( + var dbCon = database.openConnection(); + PreparedStatement dbStat = dbCon.prepareStatement( + database.normalizeTableNames("UPDATE {table_prefix}CB_USER_TEAM " + + "SET TEAM_ROLE=? WHERE USER_ID=? AND TEAM_ID=?")) + ) { + JDBCUtils.setStringOrNull(dbStat, 1, teamRole); + dbStat.setString(2, userId); + dbStat.setString(3, teamId); + dbStat.execute(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + //TODO implement add/delete user teams api + protected void setUserTeams(@NotNull Connection dbCon, String userId, String[] teamIds, String grantorId) throws SQLException { - JDBCUtils.executeStatement( - dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?"), - userId - ); - String defaultUserTeam = application.getAppConfiguration().getDefaultUserTeam(); + + String deleteUserTeamsSql = "DELETE FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?"; + + if (!ArrayUtils.isEmpty(teamIds)) { + deleteUserTeamsSql = + deleteUserTeamsSql + " AND TEAM_ID NOT IN (" + SQLUtils.generateParamList(teamIds.length) + ")"; + } + try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(deleteUserTeamsSql))) { + int index = 1; + dbStat.setString(index++, userId); + for (String teamId : teamIds) { + dbStat.setString(index++, teamId); + } + dbStat.execute(); + } + + String defaultUserTeam = getDefaultUserTeam(); if (CommonUtils.isNotEmpty(defaultUserTeam) && !ArrayUtils.contains(teamIds, defaultUserTeam)) { teamIds = ArrayUtils.add(String.class, teamIds, defaultUserTeam); } if (!ArrayUtils.isEmpty(teamIds)) { + Set currentUserTeams = new HashSet<>(JDBCUtils.queryStrings( + dbCon, + database.normalizeTableNames("SELECT TEAM_ID FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?"), + userId + )); + try (PreparedStatement dbStat = dbCon.prepareStatement( database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER_TEAM" + "(USER_ID,TEAM_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)")) ) { for (String teamId : teamIds) { + if (currentUserTeams.contains(teamId)) { + continue; + } dbStat.setString(1, userId); dbStat.setString(2, teamId); dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); @@ -252,12 +302,12 @@ public void setUserTeams(@NotNull Connection dbCon, String userId, String[] team @NotNull @Override - public SMTeam[] getUserTeams(String userId) throws DBException { - Map teams = new LinkedHashMap<>(); + public SMUserTeam[] getUserTeams(String userId) throws DBException { + Map teams = new LinkedHashMap<>(); try (Connection dbCon = database.openConnection()) { - String defaultUserTeam = application.getAppConfiguration().getDefaultUserTeam(); + String defaultUserTeam = getDefaultUserTeam(); try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames( - "SELECT R.*,S.IS_SECRET_STORAGE FROM {table_prefix}CB_USER_TEAM UR, {table_prefix}CB_TEAM R, " + + "SELECT R.*,S.IS_SECRET_STORAGE,UR.TEAM_ROLE FROM {table_prefix}CB_USER_TEAM UR, {table_prefix}CB_TEAM R, " + "{table_prefix}CB_AUTH_SUBJECT S " + "WHERE UR.USER_ID=? AND UR.TEAM_ID = R.TEAM_ID " + "AND S.SUBJECT_ID IN (R.TEAM_ID,?)")) @@ -267,12 +317,13 @@ public SMTeam[] getUserTeams(String userId) throws DBException { try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { var team = fetchTeam(dbResult); - teams.put(team.getTeamId(), team); + String teamRole = dbResult.getString("TEAM_ROLE"); + teams.put(team.getTeamId(), new SMUserTeam(team, teamRole)); } } } readSubjectsMetas(dbCon, SMSubjectType.team, null, teams); - return teams.values().toArray(new SMTeam[0]); + return teams.values().toArray(new SMUserTeam[0]); } catch (SQLException e) { throw new DBCException("Error while reading user teams", e); } @@ -296,7 +347,7 @@ private Set getAllLinkedSubjects(Connection dbCon, String subjectId) thr @NotNull @Override - public SMTeam[] getCurrentUserTeams() throws DBException { + public SMUserTeam[] getCurrentUserTeams() throws DBException { return getUserTeams(getUserIdOrThrow()); } @@ -324,7 +375,7 @@ public SMUser getUserById(String userId) throws DBException { try (PreparedStatement dbStat = dbCon.prepareStatement( database.normalizeTableNames("SELECT TEAM_ID FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?")) ) { - String defaultUserTeam = application.getAppConfiguration().getDefaultUserTeam(); + String defaultUserTeam = getDefaultUserTeam(); dbStat.setString(1, userId); try (ResultSet dbResult = dbStat.executeQuery()) { Set teamIDs = new LinkedHashSet<>(); @@ -935,7 +986,7 @@ public SMPropertyDescriptor[] getMetaParametersBySubjectType(SMSubjectType subje @Override public SMTeam[] readAllTeams() throws DBCException { try (Connection dbCon = database.openConnection()) { - String defaultUserTeam = application.getAppConfiguration().getDefaultUserTeam(); + String defaultUserTeam = getDefaultUserTeam(); Map teams = new LinkedHashMap<>(); String query = database.normalizeTableNames( "SELECT T.*, S.IS_SECRET_STORAGE FROM {table_prefix}CB_TEAM T, " + @@ -980,32 +1031,43 @@ public SMTeam findTeam(String teamId) throws DBCException { @NotNull @Override - public String[] getTeamMembers(String teamId) throws DBCException { + public String[] getTeamMembers(String teamId) throws DBException { + return getTeamMembersInfo(teamId).stream().map(SMTeamMemberInfo::userId).toArray(String[]::new); + } + + @NotNull + @Override + public List getTeamMembersInfo(@NotNull String teamId) throws DBException { try (Connection dbCon = database.openConnection()) { - if (application.getAppConfiguration().getDefaultUserTeam().equals(teamId)) { + Map usersRoles = new LinkedHashMap<>(); + if (getDefaultUserTeam().equals(teamId)) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT USER_ID FROM {table_prefix}CB_USER"))) { - List subjects = new ArrayList<>(); + database.normalizeTableNames("SELECT USER_ID FROM {table_prefix}CB_USER")) + ) { try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { - subjects.add(dbResult.getString(1)); + usersRoles.put(dbResult.getString(1), null); } } - return subjects.toArray(new String[0]); } - } else { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT USER_ID FROM {table_prefix}CB_USER_TEAM WHERE TEAM_ID=?"))) { - dbStat.setString(1, teamId); - List subjects = new ArrayList<>(); - try (ResultSet dbResult = dbStat.executeQuery()) { - while (dbResult.next()) { - subjects.add(dbResult.getString(1)); - } + } + try (PreparedStatement dbStat = dbCon.prepareStatement( + database.normalizeTableNames( + "SELECT USER_ID,TEAM_ROLE FROM {table_prefix}CB_USER_TEAM WHERE TEAM_ID=?")) + ) { + dbStat.setString(1, teamId); + try (ResultSet dbResult = dbStat.executeQuery()) { + while (dbResult.next()) { + String userId = dbResult.getString(1); + String teamRole = dbResult.getString(2); + usersRoles.put(userId, teamRole); } - return subjects.toArray(new String[0]); } } + return usersRoles.entrySet() + .stream() + .map(entry -> new SMTeamMemberInfo(entry.getKey(), entry.getValue())) + .toList(); } catch (SQLException e) { throw new DBCException("Error while reading team members", e); } @@ -1094,7 +1156,7 @@ public void updateTeam(String teamId, String name, String description) throws DB @Override public void deleteTeam(String teamId, boolean force) throws DBCException { - String defaultUsersTeam = application.getAppConfiguration().getDefaultUserTeam(); + String defaultUsersTeam = getDefaultUserTeam(); if (CommonUtils.isNotEmpty(defaultUsersTeam) && defaultUsersTeam.equals(teamId)) { throw new DBCException("Default users team cannot be deleted"); } @@ -2727,6 +2789,42 @@ public void deleteAllObjectPermissions(@NotNull String objectId, @NotNull SMObje } } + @Override + public boolean hasAccessToUsers(@NotNull String teamRole, @NotNull Set userIds) throws DBException { + if (CommonUtils.isEmpty(userIds)) { + return true; + } + String currentUserId = getUserIdOrThrow(); + var currentPermissions = getUserPermissions(currentUserId); + if (currentPermissions.contains(DBWConstants.PERMISSION_ADMIN)) { + return true; + } + + String sql = "SELECT COUNT(DISTINCT UT.USER_ID) FROM {table_prefix}CB_USER_TEAM UT " + + "WHERE TEAM_ID IN (SELECT TEAM_ID FROM {table_prefix}CB_USER_TEAM WHERE USER_ID = ? and TEAM_ROLE = ?) " + + "AND UT.USER_ID IN(" + SQLUtils.generateParamList(userIds.size()) + ")"; + try (var dbCon = database.openConnection(); + var dbStat = dbCon.prepareStatement(database.normalizeTableNames(sql)) + ) { + dbStat.setString(1, currentUserId); + dbStat.setString(2, teamRole); + int index = 3; + for (String userId : userIds) { + dbStat.setString(index++, userId); + } + try (ResultSet dbResult = dbStat.executeQuery()) { + if (dbResult.next()) { + int matchesUsersCount = dbResult.getInt(1); + return matchesUsersCount == userIds.size(); + } else { + return false; + } + } + } catch (SQLException e) { + throw new DBCException("Error validating user access", e); + } + } + @Override public void deleteAllSubjectObjectPermissions(@NotNull String subjectId, @NotNull SMObjectType objectType) throws DBException { try (Connection dbCon = database.openConnection()) { @@ -2977,7 +3075,7 @@ private String buildRedirectLink(String originalLink, String authId) { @NotNull - private String getUserIdOrThrow() throws SMException { + protected String getUserIdOrThrow() throws SMException { String userId = getUserIdOrNull(); if (userId == null) { throw new SMException("User not authenticated"); @@ -2986,7 +3084,7 @@ private String getUserIdOrThrow() throws SMException { } @Nullable - private String getUserIdOrNull() { + protected String getUserIdOrNull() { SMCredentials activeUserCredentials = credentialsProvider.getActiveUserCredentials(); if (activeUserCredentials == null || activeUserCredentials.getUserId() == null) { return null; @@ -3081,4 +3179,9 @@ private String getUserId() { var credentials = credentialsProvider.getActiveUserCredentials(); return credentials == null ? null : credentials.getUserId(); } + + @NotNull + private String getDefaultUserTeam() { + return application.getAppConfiguration().getDefaultUserTeam(); + } } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java index eed4c7926a..95268033c8 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java @@ -74,7 +74,7 @@ public class CBDatabase { public static final String SCHEMA_UPDATE_SQL_PATH = "db/cb_schema_update_"; private static final int LEGACY_SCHEMA_VERSION = 1; - private static final int CURRENT_SCHEMA_VERSION = 19; + private static final int CURRENT_SCHEMA_VERSION = 20; private static final String DEFAULT_DB_USER_NAME = "cb-data"; private static final String DEFAULT_DB_PWD_FILE = ".database-credentials.dat"; diff --git a/webapp/packages/core-authentication/src/TeamRolesResource.ts b/webapp/packages/core-authentication/src/TeamRolesResource.ts new file mode 100644 index 0000000000..cccfc91f56 --- /dev/null +++ b/webapp/packages/core-authentication/src/TeamRolesResource.ts @@ -0,0 +1,40 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { CachedDataResource } from '@cloudbeaver/core-resource'; +import { EAdminPermission, SessionPermissionsResource } from '@cloudbeaver/core-root'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; + +export const USER_TEAM_ROLE_SUPERVISOR = 'Supervisor'; + +@injectable() +export class TeamRolesResource extends CachedDataResource { + constructor( + private readonly graphQLService: GraphQLService, + sessionPermissionsResource: SessionPermissionsResource, + ) { + super(() => []); + + sessionPermissionsResource.require(this, EAdminPermission.admin).outdateResource(this); + } + + async assignTeamRoleToUser(userId: string, teamId: string, teamRole: string | null) { + const { result } = await this.graphQLService.sdk.updateUserTeamRole({ + userId, + teamId, + teamRole: teamRole ?? undefined, + }); + + return result; + } + + protected async loader(): Promise { + const { roles } = await this.graphQLService.sdk.getTeamRoles(); + return roles; + } +} diff --git a/webapp/packages/core-authentication/src/TeamsResource.ts b/webapp/packages/core-authentication/src/TeamsResource.ts index f12208a913..08373dfec0 100644 --- a/webapp/packages/core-authentication/src/TeamsResource.ts +++ b/webapp/packages/core-authentication/src/TeamsResource.ts @@ -15,12 +15,19 @@ import { type ResourceKeySimple, ResourceKeyUtils, } from '@cloudbeaver/core-resource'; -import { AdminConnectionGrantInfo, AdminTeamInfoFragment, GetTeamsListQueryVariables, GraphQLService } from '@cloudbeaver/core-sdk'; -import { isArraysEqual } from '@cloudbeaver/core-utils'; +import { + AdminConnectionGrantInfo, + AdminTeamInfoFragment, + AdminUserTeamGrantInfo, + GetTeamsListQueryVariables, + GraphQLService, +} from '@cloudbeaver/core-sdk'; +import { isArraysEqual, UndefinedToNull } from '@cloudbeaver/core-utils'; const NEW_TEAM_SYMBOL = Symbol('new-team'); export type TeamInfo = AdminTeamInfoFragment; +export type UserTeamGrantInfo = UndefinedToNull; type TeamResourceIncludes = Omit; type NewTeam = TeamInfo & { [NEW_TEAM_SYMBOL]: boolean; timestamp: number }; @@ -85,9 +92,9 @@ export class TeamsResource extends CachedMapResource { + async loadGrantedUsers(teamId: string): Promise { const { team } = await this.graphQLService.sdk.getTeamGrantedUsers({ teamId }); - return team[0].grantedUsers; + return team[0].grantedUsersInfo.map(user => ({ userId: user.userId, teamRole: user.teamRole ?? null })); } async getSubjectConnectionAccess(subjectId: string): Promise { diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index fb8b6e6729..31ac89d2da 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -40,6 +40,10 @@ export class UserInfoResource extends CachedDataResource import('./TeamMetaParametersResource').then(m => m.TeamMetaParametersResource), () => import('./TeamsManagerService').then(m => m.TeamsManagerService), () => import('./TeamsResource').then(m => m.TeamsResource), + () => import('./TeamRolesResource').then(m => m.TeamRolesResource), () => import('./UserConfigurationBootstrap').then(m => m.UserConfigurationBootstrap), () => import('./UserDataService').then(m => m.UserDataService), () => import('./UserInfoResource').then(m => m.UserInfoResource), diff --git a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx index b72d54e063..405d2cf99a 100644 --- a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx @@ -24,7 +24,10 @@ import { FieldDescription } from './FieldDescription'; import { FieldLabel } from './FieldLabel'; import { FormContext } from './FormContext'; -type BaseProps = Omit, 'onChange' | 'onSelect' | 'name' | 'value' | 'defaultValue'> & +export type ComboboxBaseProps = Omit< + React.InputHTMLAttributes, + 'onChange' | 'onSelect' | 'name' | 'value' | 'defaultValue' +> & ILayoutSizeProps & { propertyName?: string; items: TValue[]; @@ -41,7 +44,7 @@ type BaseProps = Omit, inline?: boolean; }; -type ControlledProps = BaseProps & { +type ControlledProps = ComboboxBaseProps & { name?: string; value?: TKey; onSelect?: (value: TKey, name: string | undefined, prev: TKey) => void; @@ -49,7 +52,7 @@ type ControlledProps = BaseProps & { state?: never; }; -type ObjectProps = BaseProps & { +type ObjectProps = ComboboxBaseProps & { name: TKey; state: TState; onSelect?: (value: TState[TKey], name: TKey | undefined, prev: TState[TKey]) => void; diff --git a/webapp/packages/core-blocks/src/FormControls/TagsCombobox.tsx b/webapp/packages/core-blocks/src/FormControls/TagsCombobox.tsx new file mode 100644 index 0000000000..ba5b604a21 --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/TagsCombobox.tsx @@ -0,0 +1,76 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Container } from '../Containers/Container'; +import { ITag, Tag } from '../Tags/Tag'; +import { Tags } from '../Tags/Tags'; +import { Combobox, ComboboxBaseProps } from './Combobox'; + +interface IItemValue { + id: string; + label: string; + icon?: string; +} + +// @TODO use [name] and [state] pattern for the component +export interface ITagsComboboxProps extends ComboboxBaseProps { + addedItems: string[]; + onAdd: (key: string) => void; + onRemove: (key: string, index: number) => void; +} + +export const TagsCombobox: React.FC = observer(function TagsCombobox({ addedItems, onAdd, onRemove, ...rest }) { + const tags: ITag[] = []; + const hasIcons = rest.items.some(item => !!item.icon); + + for (const addedItem of addedItems) { + const item = rest.items.find(item => item.id === addedItem); + + if (item) { + tags.push({ + id: item.id, + label: item.label, + icon: item.icon, + }); + } + } + + function add(key: string) { + if (!addedItems.includes(key)) { + onAdd(key); + } + } + + function remove(key: string) { + const index = addedItems.indexOf(key); + + if (index !== -1) { + onRemove(key, index); + } + } + + return ( + + item.id} + valueSelector={value => value.label} + iconSelector={hasIcons ? value => value.icon : undefined} + isDisabled={item => addedItems.includes(item.id)} + searchable + onSelect={add} + {...rest} + /> + + {tags.map(tag => ( + + ))} + + + ); +}); diff --git a/webapp/packages/core-blocks/src/FormControls/TagsComboboxLoader.ts b/webapp/packages/core-blocks/src/FormControls/TagsComboboxLoader.ts new file mode 100644 index 0000000000..ec8a0783ce --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/TagsComboboxLoader.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '../importLazyComponent'; + +export const TagsCombobox = importLazyComponent(() => import('./TagsCombobox').then(m => m.TagsCombobox)); diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 3915af37e3..65f199b25d 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -237,3 +237,4 @@ export * from './usePasswordValidation'; export * from './manifest'; export * from './importLazyComponent'; export * from './ClickableLoader'; +export * from './FormControls/TagsComboboxLoader'; diff --git a/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql b/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql index 614720b69d..6e8bca4484 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql @@ -6,8 +6,13 @@ query getActiveUser($includeMetaParameters: Boolean!, $includeConfigurationParam linkedAuthProviders metaParameters @include(if: $includeMetaParameters) configurationParameters @include(if: $includeConfigurationParameters) + teams { + teamId + teamName + teamRole + } authTokens { ...AuthToken } - } -} \ No newline at end of file + } +} diff --git a/webapp/packages/core-sdk/src/queries/authentication/teams/getTeamGrantedUsers.gql b/webapp/packages/core-sdk/src/queries/authentication/teams/getTeamGrantedUsers.gql index 9948138666..c17436c8e6 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/teams/getTeamGrantedUsers.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/teams/getTeamGrantedUsers.gql @@ -1,5 +1,8 @@ query getTeamGrantedUsers($teamId: ID!) { team: listTeams(teamId: $teamId) { - grantedUsers + grantedUsersInfo { + userId + teamRole + } } -} \ No newline at end of file +} diff --git a/webapp/packages/core-sdk/src/queries/authentication/teams/getTeamRoles.gql b/webapp/packages/core-sdk/src/queries/authentication/teams/getTeamRoles.gql new file mode 100644 index 0000000000..98a5456278 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/authentication/teams/getTeamRoles.gql @@ -0,0 +1,3 @@ +query getTeamRoles { + roles: listTeamRoles +} diff --git a/webapp/packages/core-sdk/src/queries/authentication/teams/updateUserTeamRole.gql b/webapp/packages/core-sdk/src/queries/authentication/teams/updateUserTeamRole.gql new file mode 100644 index 0000000000..a2bfdd5f63 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/authentication/teams/updateUserTeamRole.gql @@ -0,0 +1,3 @@ +query updateUserTeamRole($userId: ID!, $teamId: ID!, $teamRole: String) { + result: setUserTeamRole(userId: $userId, teamId: $teamId, teamRole: $teamRole) +} diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index d711d9f0f1..a421b6e031 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -87,3 +87,4 @@ export * from './formatNumber'; export * from './withTimestamp'; export * from './toSafeHtmlString'; export * from './getProgressPercent'; +export * from './types/UndefinedToNull'; diff --git a/webapp/packages/core-utils/src/types/UndefinedToNull.ts b/webapp/packages/core-utils/src/types/UndefinedToNull.ts new file mode 100644 index 0000000000..b465006fef --- /dev/null +++ b/webapp/packages/core-utils/src/types/UndefinedToNull.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +type UnionUndefinedToNull = T extends undefined ? null : T; + +export type UndefinedToNull = { + [Prop in keyof T]-?: T[Prop] extends object ? UndefinedToNull : UnionUndefinedToNull; +}; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx index 85a150b35d..5f3f332437 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx @@ -9,7 +9,7 @@ import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useCallback, useState } from 'react'; -import { UsersResource } from '@cloudbeaver/core-authentication'; +import { TeamRolesResource, UsersResource } from '@cloudbeaver/core-authentication'; import { Button, Container, @@ -22,28 +22,30 @@ import { TableColumnValue, TableItem, useObjectRef, + useResource, useS, useTranslate, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import type { TLocalizationToken } from '@cloudbeaver/core-localization'; import { ServerConfigResource } from '@cloudbeaver/core-root'; -import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; import { getFilteredUsers } from './getFilteredUsers'; import style from './GrantedUserList.m.css'; import { GrantedUsersTableHeader, IFilterState } from './GrantedUsersTableHeader/GrantedUsersTableHeader'; import { GrantedUsersTableInnerHeader } from './GrantedUsersTableHeader/GrantedUsersTableInnerHeader'; import { GrantedUsersTableItem } from './GrantedUsersTableItem'; +import type { IGrantedUser } from './IGrantedUser'; interface Props { - grantedUsers: AdminUserInfoFragment[]; + grantedUsers: IGrantedUser[]; disabled: boolean; onRevoke: (subjectIds: string[]) => void; + onTeamRoleAssign: (subjectId: string, teamRole: string | null) => void; onEdit: () => void; } -export const GrantedUserList = observer(function GrantedUserList({ grantedUsers, disabled, onRevoke, onEdit }) { +export const GrantedUserList = observer(function GrantedUserList({ grantedUsers, disabled, onRevoke, onTeamRoleAssign, onEdit }) { const styles = useS(style); const props = useObjectRef({ onRevoke, onEdit }); const translate = useTranslate(); @@ -51,12 +53,14 @@ export const GrantedUserList = observer(function GrantedUserList({ grante const usersResource = useService(UsersResource); const serverConfigResource = useService(ServerConfigResource); + const teamRolesResource = useResource(GrantedUserList, TeamRolesResource, undefined); + const [selectedSubjects] = useState>(() => observable(new Map())); const [filterState] = useState(() => observable({ filterValue: '' })); const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); - const users = getFilteredUsers(grantedUsers, filterState.filterValue); + const users = getFilteredUsers(grantedUsers, filterState.filterValue) as IGrantedUser[]; const keys = users.map(user => user.userId); const revoke = useCallback(() => { @@ -97,7 +101,7 @@ export const GrantedUserList = observer(function GrantedUserList({ grante isEditable(item)}> - + 0} /> {tableInfoText && ( @@ -112,7 +116,10 @@ export const GrantedUserList = observer(function GrantedUserList({ grante tooltip={isEditable(user.userId) ? user.userId : translate('administration_teams_team_granted_users_permission_denied')} icon="/icons/user.svg" iconTooltip={translate('authentication_user_icon_tooltip')} + teamRole={user.teamRole} + teamRoles={teamRolesResource.data} disabled={disabled} + onTeamRoleAssign={onTeamRoleAssign} /> ))} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx index f1fe31856d..445d0fe2fe 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx @@ -7,10 +7,9 @@ */ import { observer } from 'mobx-react-lite'; -import { AdminUser, UsersResource, UsersResourceFilterKey } from '@cloudbeaver/core-authentication'; +import { UsersResource, UsersResourceFilterKey } from '@cloudbeaver/core-authentication'; import { Container, - getComputed, Group, InfoItem, Loader, @@ -28,6 +27,7 @@ import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; import type { ITeamFormProps } from '../ITeamFormProps'; import { GrantedUserList } from './GrantedUserList'; import style from './GrantedUsers.m.css'; +import type { IGrantedUser } from './IGrantedUser'; import { useGrantedUsers } from './useGrantedUsers'; import { UserList } from './UserList'; @@ -45,9 +45,18 @@ export const GrantedUsers: TabContainerPanelComponent = observer active: selected && !isDefaultTeam, }); - const grantedUsers = getComputed(() => - users.data.filter((user): user is AdminUser => !!user && state.state.grantedUsers.includes(user.userId)), - ); + const grantedUsers: IGrantedUser[] = []; + + for (const user of users.data) { + const granted = state.state.grantedUsers.find(grantedUser => grantedUser.userId === user?.userId); + + if (granted && user) { + grantedUsers.push({ + ...user, + teamRole: granted.teamRole, + }); + } + } useAutoLoad(GrantedUsers, state, selected && !state.state.loaded && !isDefaultTeam); @@ -77,11 +86,17 @@ export const GrantedUsers: TabContainerPanelComponent = observer <> {formState.mode === 'edit' && state.changed && } - + {state.state.editing && ( user.userId)} disabled={formState.disabled} onGrant={state.grant} /> diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTabService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTabService.ts index d262c3a9a4..c631baa12f 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTabService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTabService.ts @@ -7,11 +7,11 @@ */ import React from 'react'; -import { TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { TeamRolesResource, TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { isArraysEqual, MetadataValueGetter } from '@cloudbeaver/core-utils'; +import { isArraysEqual, isObjectsEqual, MetadataValueGetter } from '@cloudbeaver/core-utils'; import { teamContext } from '../Contexts/teamContext'; import type { ITeamFormProps, ITeamFormSubmitData } from '../ITeamFormProps'; @@ -32,6 +32,7 @@ export class GrantedUsersTabService extends Bootstrap { private readonly usersResource: UsersResource, private readonly teamsResource: TeamsResource, private readonly notificationService: NotificationService, + private readonly teamRolesResource: TeamRolesResource, ) { super(); this.key = 'granted-users'; @@ -78,7 +79,7 @@ export class GrantedUsersTabService extends Bootstrap { const initial = await this.teamsResource.loadGrantedUsers(config.teamId); - const changed = !isArraysEqual(initial, state.grantedUsers); + const changed = !isArraysEqual(initial, state.grantedUsers, isObjectsEqual); if (!changed) { return; @@ -87,18 +88,26 @@ export class GrantedUsersTabService extends Bootstrap { const granted: string[] = []; const revoked: string[] = []; - const revokedUsers = initial.filter(user => !state.grantedUsers.includes(user)); + const revokedUsers = initial.filter(user => !state.grantedUsers.some(grantedUser => grantedUser.userId === user.userId)); try { for (const user of revokedUsers) { - await this.usersResource.revokeTeam(user, config.teamId); - revoked.push(user); + await this.usersResource.revokeTeam(user.userId, config.teamId); + revoked.push(user.userId); } for (const user of state.grantedUsers) { - if (!initial.includes(user)) { - await this.usersResource.grantTeam(user, config.teamId); - granted.push(user); + const initialUser = initial.find(grantedUser => grantedUser.userId === user.userId); + + if (!initialUser) { + await this.usersResource.grantTeam(user.userId, config.teamId); + granted.push(user.userId); + } + + const initialRole = initialUser?.teamRole ?? null; + + if (user.teamRole !== initialRole) { + await this.teamRolesResource.assignTeamRoleToUser(user.userId, config.teamId, user.teamRole); } } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx index 74b2888814..6508115c54 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx @@ -11,10 +11,11 @@ import { TableColumnHeader, TableHeader, TableSelect, useTranslate } from '@clou interface Props { disabled?: boolean; + showUserTeamRole?: boolean; className?: string; } -export const GrantedUsersTableInnerHeader = observer(function GrantedUsersTableInnerHeader({ disabled, className }) { +export const GrantedUsersTableInnerHeader = observer(function GrantedUsersTableInnerHeader({ disabled, showUserTeamRole, className }) { const translate = useTranslate(); return ( @@ -24,6 +25,11 @@ export const GrantedUsersTableInnerHeader = observer(function GrantedUser {translate('administration_teams_team_granted_users_user_id')} + {showUserTeamRole && ( + + {translate('plugin_authentication_administration_team_user_team_role_supervisor')} + + )} ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.m.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.m.css index 5378504fe0..bd43f5c8d6 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.m.css +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.m.css @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ .staticImage { display: flex; width: 24px; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.tsx index cd45c1d52f..45047fd881 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.tsx @@ -7,30 +7,55 @@ */ import { observer } from 'mobx-react-lite'; -import { StaticImage, TableColumnValue, TableItem, TableItemSelect } from '@cloudbeaver/core-blocks'; +import { USER_TEAM_ROLE_SUPERVISOR } from '@cloudbeaver/core-authentication'; +import { Checkbox, StaticImage, TableColumnValue, TableItem, TableItemSelect, useTranslate } from '@cloudbeaver/core-blocks'; -import style from './GrantedUsersTableItem.m.css'; +import classes from './GrantedUsersTableItem.m.css'; interface Props { id: any; name: string; icon: string; disabled: boolean; + teamRole: string | null; + teamRoles: string[]; iconTooltip?: string; tooltip?: string; + onTeamRoleAssign: (subjectId: string, teamRole: string | null) => void; className?: string; } -export const GrantedUsersTableItem = observer(function GrantedUsersTableItem({ id, name, icon, iconTooltip, tooltip, disabled, className }) { +export const GrantedUsersTableItem = observer(function GrantedUsersTableItem({ + id, + name, + icon, + iconTooltip, + tooltip, + teamRole, + teamRoles, + onTeamRoleAssign, + disabled, + className, +}) { + const translate = useTranslate(); + return ( - + {name} + {teamRoles.length > 0 && ( + + onTeamRoleAssign(id, value ? USER_TEAM_ROLE_SUPERVISOR : null)} + /> + + )} ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/IGrantedUser.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/IGrantedUser.ts new file mode 100644 index 0000000000..94c92e5e11 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/IGrantedUser.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; + +export interface IGrantedUser extends AdminUserInfoFragment { + teamRole: string | null; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/IGrantedUsersTabState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/IGrantedUsersTabState.ts index 88ae8e632a..ceb852c226 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/IGrantedUsersTabState.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/IGrantedUsersTabState.ts @@ -5,11 +5,12 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import type { UserTeamGrantInfo } from '@cloudbeaver/core-authentication'; export interface IGrantedUsersTabState { loading: boolean; loaded: boolean; - grantedUsers: string[]; - initialGrantedUsers: string[]; + grantedUsers: UserTeamGrantInfo[]; + initialGrantedUsers: UserTeamGrantInfo[]; editing: boolean; } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx index 85005f1a1f..c32f32e539 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx @@ -31,9 +31,9 @@ import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; import { getFilteredUsers } from './getFilteredUsers'; import { GrantedUsersTableHeader, IFilterState } from './GrantedUsersTableHeader/GrantedUsersTableHeader'; -import { GrantedUsersTableInnerHeader } from './GrantedUsersTableHeader/GrantedUsersTableInnerHeader'; -import { GrantedUsersTableItem } from './GrantedUsersTableItem'; import style from './UserList.m.css'; +import { UsersTableInnerHeader } from './UsersTableInnerHeader'; +import { UsersTableItem } from './UsersTableItem'; interface Props { userList: AdminUserInfoFragment[]; @@ -82,7 +82,7 @@ export const UserList = observer(function UserList({ userList, grantedUse
isEditable(item) && !grantedUsers.includes(item)}> - + {!users.length && filterState.filterValue && ( @@ -90,7 +90,7 @@ export const UserList = observer(function UserList({ userList, grantedUse )} {users.map(user => ( - (function UsersTableInnerHeader({ disabled, className }) { + const translate = useTranslate(); + + return ( + + + + + + {translate('administration_teams_team_granted_users_user_id')} + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UsersTableItem.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UsersTableItem.tsx new file mode 100644 index 0000000000..73d1799bd5 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UsersTableItem.tsx @@ -0,0 +1,36 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { StaticImage, TableColumnValue, TableItem, TableItemSelect } from '@cloudbeaver/core-blocks'; + +import style from './GrantedUsersTableItem.m.css'; + +interface Props { + id: any; + name: string; + icon: string; + disabled: boolean; + iconTooltip?: string; + tooltip?: string; + className?: string; +} + +export const UsersTableItem = observer(function UsersTableItem({ id, name, icon, iconTooltip, tooltip, disabled, className }) { + return ( + + + + + + + + {name} + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/useGrantedUsers.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/useGrantedUsers.tsx index 874917487e..f6e49ca23e 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/useGrantedUsers.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/useGrantedUsers.tsx @@ -5,14 +5,14 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, computed, observable } from 'mobx'; +import { action, computed, observable, toJS } from 'mobx'; import { TeamInfo, TeamsResource } from '@cloudbeaver/core-authentication'; import { useObservableRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { useTabState } from '@cloudbeaver/core-ui'; -import { ILoadableState, isArraysEqual } from '@cloudbeaver/core-utils'; +import { ILoadableState, isArraysEqual, isObjectsEqual } from '@cloudbeaver/core-utils'; import type { TeamFormMode } from '../ITeamFormProps'; import type { IGrantedUsersTabState } from './IGrantedUsersTabState'; @@ -23,6 +23,7 @@ interface State extends ILoadableState { edit: () => void; revoke: (subjectIds: string[]) => void; grant: (subjectIds: string[]) => void; + assignTeamRole: (subjectId: string, teamRole: string | null) => void; load: () => Promise; } @@ -34,7 +35,7 @@ export function useGrantedUsers(team: TeamInfo, mode: TeamFormMode): Readonly ({ get changed() { - return !isArraysEqual(this.state.initialGrantedUsers, this.state.grantedUsers); + return !isArraysEqual(this.state.initialGrantedUsers, this.state.grantedUsers, isObjectsEqual); }, isLoading() { return this.state.loading; @@ -49,10 +50,17 @@ export function useGrantedUsers(team: TeamInfo, mode: TeamFormMode): Readonly !subjectIds.includes(subject)); + this.state.grantedUsers = this.state.grantedUsers.filter(subject => !subjectIds.includes(subject.userId)); }, grant(subjectIds: string[]) { - this.state.grantedUsers.push(...subjectIds); + this.state.grantedUsers.push(...subjectIds.map(id => ({ userId: id, teamRole: null }))); + }, + assignTeamRole(subjectId: string, teamRole: string | null) { + const user = this.state.grantedUsers.find(user => user.userId === subjectId); + + if (user) { + user.teamRole = teamRole; + } }, async load() { if (this.state.loaded || this.state.loading) { @@ -65,7 +73,7 @@ export function useGrantedUsers(team: TeamInfo, mode: TeamFormMode): Readonly