diff --git a/app/client/src/components/editorComponents/SelectComponent.tsx b/app/client/src/components/editorComponents/SelectComponent.tsx index 46013203f2..305e26120e 100644 --- a/app/client/src/components/editorComponents/SelectComponent.tsx +++ b/app/client/src/components/editorComponents/SelectComponent.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { ReactNode } from "react"; import CustomizedDropdown, { CustomizedDropdownProps, } from "pages/common/CustomizedDropdown/index"; @@ -8,7 +8,7 @@ type SelectComponentProps = { value?: string; onChange?: (value: string) => void; }; - options?: Array<{ id: string; name: string }>; + options?: Array<{ id: string; name: string; content?: ReactNode }>; placeholder?: string; size?: "large" | "small"; outline?: boolean; @@ -22,7 +22,7 @@ export const SelectComponent = (props: SelectComponentProps) => { options: props.options && props.options.map(option => ({ - content: option.name, + content: option.content ? option.content : option.name, onSelect: () => { props.input.onChange && props.input.onChange(option.id); }, diff --git a/app/client/src/pages/organization/InviteUsersFromv2.tsx b/app/client/src/pages/organization/InviteUsersFromv2.tsx index 9776258611..21e8a79731 100644 --- a/app/client/src/pages/organization/InviteUsersFromv2.tsx +++ b/app/client/src/pages/organization/InviteUsersFromv2.tsx @@ -35,6 +35,19 @@ const OrgInviteTitle = styled.div` padding: 10px 0px; `; +const DropDownOption = styled.div` + padding: 10px 0; +`; + +const OptionTitle = styled.div` + font-weight: bold; +`; + +const OptionDescription = styled.div` + padding: 5px 0px; + max-width: 250px; +`; + const StyledForm = styled.form` width: 100%; background: white; @@ -158,6 +171,19 @@ const InviteUsersForm = (props: any) => { fetchCurrentOrg(props.orgId); }, [props.orgId, fetchUser, fetchAllRoles, fetchCurrentOrg]); + const styledRoles = props.roles.map((role: any) => { + return { + id: role.id, + name: role.name, + content: ( + + {role.name} + {role.description} + + ), + }; + }); + return ( <> {applicationId && ( @@ -199,7 +225,7 @@ const InviteUsersForm = (props: any) => { { return { id: role[0], name: role[0], + description: role[1], }; }); }); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AclPermission.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AclPermission.java index 04fa782436..61237b275a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AclPermission.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AclPermission.java @@ -41,10 +41,16 @@ public enum AclPermission { ORGANIZATION_READ_APPLICATIONS("read:orgApplications", Organization.class), ORGANIZATION_PUBLISH_APPLICATIONS("publish:orgApplications", Organization.class), + // Invitation related permissions + ORGANIZATION_INVITE_USERS("inviteUsers:organization", Organization.class), + MANAGE_APPLICATIONS("manage:applications", Application.class), READ_APPLICATIONS("read:applications", Application.class), PUBLISH_APPLICATIONS("publish:applications", Application.class), + // Making an application public permission at Organization level + MAKE_PUBLIC_APPLICATIONS("makePublic:applications", Application.class), + MANAGE_PAGES("manage:pages", Page.class), READ_PAGES("read:pages", Page.class), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AppsmithRole.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AppsmithRole.java index 99afb078df..61059a685c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AppsmithRole.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/AppsmithRole.java @@ -8,6 +8,7 @@ import java.util.Set; import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_ORGANIZATIONS; +import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS; import static com.appsmith.server.acl.AclPermission.ORGANIZATION_MANAGE_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.ORGANIZATION_PUBLISH_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.ORGANIZATION_READ_APPLICATIONS; @@ -19,9 +20,9 @@ public enum AppsmithRole { APPLICATION_ADMIN("Application Administrator", "", Set.of(MANAGE_APPLICATIONS)), APPLICATION_VIEWER("Application Viewer", "", Set.of(READ_APPLICATIONS)), ORGANIZATION_ADMIN("Administrator", "Can edit, view applications and invite other user to organization", - Set.of(MANAGE_ORGANIZATIONS)), + Set.of(MANAGE_ORGANIZATIONS, ORGANIZATION_INVITE_USERS)), ORGANIZATION_DEVELOPER("Developer", "Can edit and view applications", Set.of(READ_ORGANIZATIONS, - ORGANIZATION_MANAGE_APPLICATIONS, ORGANIZATION_READ_APPLICATIONS, ORGANIZATION_PUBLISH_APPLICATIONS)), + ORGANIZATION_MANAGE_APPLICATIONS, ORGANIZATION_READ_APPLICATIONS, ORGANIZATION_PUBLISH_APPLICATIONS, ORGANIZATION_INVITE_USERS)), ORGANIZATION_VIEWER("App Viewer", "Can view applications", Set.of(READ_ORGANIZATIONS, ORGANIZATION_READ_APPLICATIONS)); private Set permissions; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/PolicyGenerator.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/PolicyGenerator.java index c7f9e918fc..3903eac04a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/PolicyGenerator.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/PolicyGenerator.java @@ -20,6 +20,7 @@ import java.util.stream.Collectors; import static com.appsmith.server.acl.AclPermission.EXECUTE_ACTIONS; import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES; +import static com.appsmith.server.acl.AclPermission.MAKE_PUBLIC_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES; @@ -107,6 +108,7 @@ public class PolicyGenerator { hierarchyGraph.addEdge(ORGANIZATION_MANAGE_APPLICATIONS, MANAGE_APPLICATIONS); hierarchyGraph.addEdge(ORGANIZATION_READ_APPLICATIONS, READ_APPLICATIONS); hierarchyGraph.addEdge(ORGANIZATION_PUBLISH_APPLICATIONS, PUBLISH_APPLICATIONS); + hierarchyGraph.addEdge(MANAGE_ORGANIZATIONS, MAKE_PUBLIC_APPLICATIONS); // If the user is being given MANAGE_APPLICATION permission, they must also be given READ_APPLICATION perm lateralGraph.addEdge(MANAGE_APPLICATIONS, READ_APPLICATIONS); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/RoleGraph.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/RoleGraph.java new file mode 100644 index 0000000000..5ce6d06834 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/acl/RoleGraph.java @@ -0,0 +1,55 @@ +package com.appsmith.server.acl; + +import lombok.extern.slf4j.Slf4j; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.graph.DirectedMultigraph; +import org.jgrapht.traverse.BreadthFirstIterator; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import static com.appsmith.server.acl.AppsmithRole.APPLICATION_ADMIN; +import static com.appsmith.server.acl.AppsmithRole.APPLICATION_VIEWER; +import static com.appsmith.server.acl.AppsmithRole.ORGANIZATION_ADMIN; +import static com.appsmith.server.acl.AppsmithRole.ORGANIZATION_DEVELOPER; +import static com.appsmith.server.acl.AppsmithRole.ORGANIZATION_VIEWER; + +@Slf4j +@Component +public class RoleGraph { + /** + * This graph defines the hierarchy of permissions from parent objects + */ + Graph hierarchyGraph = new DirectedMultigraph<>(DefaultEdge.class); + + @PostConstruct + public void createPolicyGraph() { + + // Initialization of the hierarchical and lateral graphs by adding all the vertices + EnumSet.allOf(AppsmithRole.class) + .forEach(role -> { + hierarchyGraph.addVertex(role); + }); + + hierarchyGraph.addEdge(ORGANIZATION_ADMIN, ORGANIZATION_DEVELOPER); + hierarchyGraph.addEdge(ORGANIZATION_DEVELOPER, ORGANIZATION_VIEWER); + hierarchyGraph.addEdge(APPLICATION_ADMIN, APPLICATION_VIEWER); + } + + public Set generateHierarchicalRoles(String roleName) { + AppsmithRole role = AppsmithRole.generateAppsmithRoleFromName(roleName); + + Set childrenRoles = new HashSet<>(); + childrenRoles.add(role); + BreadthFirstIterator breadthFirstIterator = new BreadthFirstIterator<>(hierarchyGraph, role); + while(breadthFirstIterator.hasNext()) { + childrenRoles.add(breadthFirstIterator.next()); + } + + return childrenRoles; + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java index 42058529cf..1b43ef6c8f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @@ -35,8 +36,8 @@ public class OrganizationController extends BaseController>> getUserRolesForOrganization() { - return service.getUserRolesForOrganization() + public Mono>> getUserRolesForOrganization(@RequestParam String organizationId) { + return service.getUserRolesForOrganization(organizationId) .map(permissions -> new ResponseDTO<>(HttpStatus.OK.value(), permissions, null)); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java index d27734c659..8bf3a9decf 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java @@ -35,6 +35,7 @@ public enum AppsmithError { DUPLICATE_KEY(409, 4024, "Duplicate key error"), USER_ALREADY_EXISTS_SIGNUP(409, 4025, "There is already an account registered with this username {0}. Please sign in."), UNAUTHORIZED_ACCESS(403, 4025, "Unauthorized access"), + ACTION_IS_NOT_AUTHORIZED(403, 4026, "Sorry. You do not have permissions to perform this action"), INVALID_DATASOURCE_NAME(400, 4026, "Invalid datasource name. Check again."), NO_RESOURCE_FOUND(404, 4027, "Unable to find {0} with id {1}"), ACL_NO_RESOURCE_FOUND(404, 4028, "Unable to find {0} with id {1}. Either the asset doesn't exist or you don't have required permissions"), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java index 16b58b94f2..688ab8a365 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java @@ -2,6 +2,7 @@ package com.appsmith.server.migrations; import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.Policy; +import com.appsmith.server.acl.AppsmithRole; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; @@ -47,6 +48,8 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.appsmith.server.acl.AclPermission.EXECUTE_ACTIONS; +import static com.appsmith.server.acl.AclPermission.MAKE_PUBLIC_APPLICATIONS; +import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS; import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; import static org.springframework.data.mongodb.core.query.Criteria.where; import static org.springframework.data.mongodb.core.query.Query.query; @@ -577,4 +580,68 @@ public class DatabaseChangelog { } } } + + @ChangeSet(order = "021", id = "invite-and-public-permissions", author = "") + public void giveInvitePermissionToOrganizationsAndPublicPermissionsToApplications(MongoTemplate mongoTemplate) { + final List organizations = mongoTemplate.find( + query(where("userRoles").exists(true)), + Organization.class + ); + + final List applications = mongoTemplate.find( + query(where("policies").exists(true)), + Application.class + ); + + for (final Organization organization : organizations) { + Set adminUsernames = organization.getUserRoles() + .stream() + .filter(role -> (role.getRole().equals(AppsmithRole.ORGANIZATION_ADMIN))) + .map(role -> role.getUsername()) + .collect(Collectors.toSet()); + + Set developerUsernames = organization.getUserRoles() + .stream() + .filter(role -> (role.getRole().equals(AppsmithRole.ORGANIZATION_DEVELOPER))) + .map(role -> role.getUsername()) + .collect(Collectors.toSet()); + + // All the developers and administrators of the organization should be allowed to get invite permissions + Set invitePermissionUsernames = new HashSet<>(); + invitePermissionUsernames.addAll(developerUsernames); + invitePermissionUsernames.addAll(adminUsernames); + + Set policies = organization.getPolicies(); + if (policies == null) { + policies = new HashSet<>(); + } + + Policy inviteUserPolicy = Policy.builder().permission(ORGANIZATION_INVITE_USERS.getValue()) + .users(invitePermissionUsernames).build(); + + policies.add(inviteUserPolicy); + organization.setPolicies(policies); + mongoTemplate.save(organization); + + // Update the applications with public view policy for all administrators of the organization + Set orgApplications = applications + .stream() + .filter(application -> application.getOrganizationId().equals(organization.getId())) + .collect(Collectors.toSet()); + + for (final Application application : orgApplications) { + Set applicationPolicies = application.getPolicies(); + if (applicationPolicies == null) { + applicationPolicies = new HashSet<>(); + } + + Policy newPublicAppPolicy = Policy.builder().permission(MAKE_PUBLIC_APPLICATIONS.getValue()) + .users(adminUsernames).build(); + applicationPolicies.add(newPublicAppPolicy); + application.setPolicies(applicationPolicies); + + mongoTemplate.save(application); + } + } + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java index 348db488e3..712ecf2abd 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java @@ -218,15 +218,7 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { return orgMono.map(org -> { application.setOrganizationId(org.getId()); - // At the organization level, filter out all the application specific policies and apply them - // to the new application that we are creating. - Set policySet = org.getPolicies().stream() - .filter(policy -> - policy.getPermission().equals(ORGANIZATION_MANAGE_APPLICATIONS.getValue()) || - policy.getPermission().equals(ORGANIZATION_READ_APPLICATIONS.getValue()) - ).collect(Collectors.toSet()); - - Set documentPolicies = policyGenerator.getAllChildPolicies(policySet, Organization.class, Application.class); + Set documentPolicies = policyGenerator.getAllChildPolicies(org.getPolicies(), Organization.class, Application.class); application.setPolicies(documentPolicies); return application; }); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java index 278c43e5d2..7231795560 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java @@ -34,7 +34,7 @@ import java.util.Map; import java.util.Set; import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES; -import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; +import static com.appsmith.server.acl.AclPermission.MAKE_PUBLIC_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES; import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS; @@ -184,7 +184,7 @@ public class ApplicationServiceImpl extends BaseService changeViewAccess(String id, ApplicationAccessDTO applicationAccessDTO) { return repository - .findById(id, MANAGE_APPLICATIONS) + .findById(id, MAKE_PUBLIC_APPLICATIONS) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION_ID, id))) .flatMap(application -> { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java index 9dff19d668..b682273306 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java @@ -29,7 +29,7 @@ public interface OrganizationService extends CrudService { Flux findByIdsIn(Set ids,AclPermission permission); - Mono> getUserRolesForOrganization(); + Mono> getUserRolesForOrganization(String orgId); Mono> getOrganizationMembers(String orgId); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java index b09f133bd9..ba0110e16a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java @@ -2,6 +2,7 @@ package com.appsmith.server.services; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.AppsmithRole; +import com.appsmith.server.acl.RoleGraph; import com.appsmith.server.constants.AnalyticsEvents; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Organization; @@ -29,13 +30,14 @@ import reactor.core.scheduler.Scheduler; import javax.validation.Validator; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import static com.appsmith.server.acl.AclPermission.MANAGE_ORGANIZATIONS; +import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS; import static com.appsmith.server.acl.AclPermission.READ_USERS; import static com.appsmith.server.acl.AclPermission.USER_MANAGE_ORGANIZATIONS; import static java.util.stream.Collectors.toMap; @@ -51,6 +53,7 @@ public class OrganizationServiceImpl extends BaseService> getUserRolesForOrganization() { - // Get all the roles for Organization entity from the enum AppsmithRole - Map appsmithRoles = Arrays.asList(AppsmithRole.values()) - .stream() - .filter(role -> { - Set permissions = role.getPermissions(); - if (permissions != null && !permissions.isEmpty()) { - for (AclPermission permission : permissions) { - if (permission.getEntity().equals(Organization.class)) { - return true; - } - } - } - return false; - }) - .collect(toMap(role -> role.getName(), AppsmithRole::getDescription)); + public Mono> getUserRolesForOrganization(String orgId) { + if (orgId == null || orgId.isEmpty()) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORGANIZATION_ID)); + } - return Mono.just(appsmithRoles); + Mono organizationMono = repository.findById(orgId, ORGANIZATION_INVITE_USERS); + Mono usernameMono = sessionUserService + .getCurrentUser() + .map(user -> user.getUsername()); + + return organizationMono + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, orgId))) + .zipWith(usernameMono) + .flatMap(tuple -> { + Organization organization = tuple.getT1(); + String username = tuple.getT2(); + + List userRoles = organization.getUserRoles(); + if (userRoles == null || userRoles.isEmpty()) { + return Mono.empty(); + } + + Optional optionalUserRole = userRoles.stream().filter(role -> role.getUsername().equals(username)).findFirst(); + if (!optionalUserRole.isPresent()) { + return Mono.empty(); + } + + UserRole currentUserRole = optionalUserRole.get(); + String roleName = currentUserRole.getRoleName(); + + Set appsmithRoles = roleGraph.generateHierarchicalRoles(roleName); + + Map appsmithRolesMap = appsmithRoles + .stream() + .collect(toMap(role -> role.getName(), AppsmithRole::getDescription)); + + return Mono.just(appsmithRolesMap); + }); } @Override public Mono> getOrganizationMembers(String orgId) { return repository - .findById(orgId, MANAGE_ORGANIZATIONS) + .findById(orgId, ORGANIZATION_INVITE_USERS) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, orgId))) .map(organization -> { final List userRoles = organization.getUserRoles(); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java index 24c6fa7978..5b40a3dea1 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java @@ -246,7 +246,9 @@ public class UserOrganizationServiceImpl implements UserOrganizationService { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "username")); } - Mono organizationMono = organizationRepository.findById(orgId, MANAGE_ORGANIZATIONS); + Mono organizationMono = organizationRepository + .findById(orgId, MANAGE_ORGANIZATIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACTION_IS_NOT_AUTHORIZED))); Mono userMono = userRepository.findByEmail(userRole.getUsername()); Mono currentUserMono = sessionUserService.getCurrentUser(); @@ -266,7 +268,7 @@ public class UserOrganizationServiceImpl implements UserOrganizationService { if (role.getUsername().equals(userRole.getUsername())) { // User found in the organization. - if (role.getRoleName().equals(userRole.getRoleName())) { + if (role.getRole().equals(userRole)) { // No change in the role. Do nothing. Mono.just(userRole); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java index fec743578f..7e6b7773db 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java @@ -2,6 +2,8 @@ package com.appsmith.server.services; import com.appsmith.external.models.Policy; import com.appsmith.server.acl.AclPermission; +import com.appsmith.server.acl.AppsmithRole; +import com.appsmith.server.acl.RoleGraph; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.InviteUser; @@ -9,6 +11,7 @@ import com.appsmith.server.domains.LoginSource; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.PasswordResetToken; import com.appsmith.server.domains.User; +import com.appsmith.server.domains.UserRole; import com.appsmith.server.dtos.InviteUsersDTO; import com.appsmith.server.dtos.ResetUserPasswordDTO; import com.appsmith.server.exceptions.AppsmithError; @@ -44,8 +47,8 @@ import java.util.Set; import java.util.UUID; import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; -import static com.appsmith.server.acl.AclPermission.MANAGE_ORGANIZATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_USERS; +import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS; import static com.appsmith.server.acl.AclPermission.USER_MANAGE_ORGANIZATIONS; @Slf4j @@ -62,6 +65,7 @@ public class UserServiceImpl extends BaseService i private final PolicyUtils policyUtils; private final OrganizationRepository organizationRepository; private final UserOrganizationService userOrganizationService; + private final RoleGraph roleGraph; private static final String WELCOME_USER_EMAIL_TEMPLATE = "email/welcomeUserTemplate.html"; private static final String FORGOT_PASSWORD_EMAIL_TEMPLATE = "email/forgotPasswordTemplate.html"; @@ -87,7 +91,8 @@ public class UserServiceImpl extends BaseService i ApplicationRepository applicationRepository, PolicyUtils policyUtils, OrganizationRepository organizationRepository, - UserOrganizationService userOrganizationService) { + UserOrganizationService userOrganizationService, + RoleGraph roleGraph) { super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService); this.organizationService = organizationService; this.analyticsService = analyticsService; @@ -99,6 +104,7 @@ public class UserServiceImpl extends BaseService i this.policyUtils = policyUtils; this.organizationRepository = organizationRepository; this.userOrganizationService = userOrganizationService; + this.roleGraph = roleGraph; } @Override @@ -567,11 +573,23 @@ public class UserServiceImpl extends BaseService i return Flux.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ROLE)); } - Mono organizationMono = organizationRepository.findById(inviteUsersDTO.getOrgId(), MANAGE_ORGANIZATIONS) + Mono currentUserMono = sessionUserService.getCurrentUser().cache(); + + // Check if the current user has invite permissions + Mono organizationMono = organizationRepository.findById(inviteUsersDTO.getOrgId(), ORGANIZATION_INVITE_USERS) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, inviteUsersDTO.getOrgId()))) + .zipWith(currentUserMono) + .flatMap(tuple -> { + Organization organization = tuple.getT1(); + User currentUser = tuple.getT2(); + + // This code segment checks if the current user can invite for the invited role. + + return isUserPermittedToInviteForGivenRole(organization, currentUser.getUsername(), inviteUsersDTO.getRoleName()) + .thenReturn(organization); + }) .cache(); - Mono currentUserMono = sessionUserService.getCurrentUser(); // Check if the invited user exists. If yes, return the user, else create a new user by triggering // createNewUserAndSendInviteEmail. In both the cases, send the appropriate emails @@ -684,4 +702,35 @@ public class UserServiceImpl extends BaseService i }); } + private Mono isUserPermittedToInviteForGivenRole(Organization organization, String username, String invitedRoleName) { + List userRoles = organization.getUserRoles(); + + // The current organization has no members. Clearly the current user is also not present + if (userRoles == null || userRoles.isEmpty()) { + return Mono.error(new AppsmithException(AppsmithError.ACTION_IS_NOT_AUTHORIZED)); + } + + Optional optionalUserRole = userRoles.stream().filter(role -> role.getUsername().equals(username)).findFirst(); + // If the current user is not present in the organization, the user would also not be permitted to invite + if (!optionalUserRole.isPresent()) { + return Mono.error(new AppsmithException(AppsmithError.ACTION_IS_NOT_AUTHORIZED)); + } + + UserRole currentUserRole = optionalUserRole.get(); + String currentUserRoleName = currentUserRole.getRoleName(); + + AppsmithRole invitedRole = AppsmithRole.generateAppsmithRoleFromName(invitedRoleName); + + // Generate all the roles for which the current user can invite other users + Set appsmithRoles = roleGraph.generateHierarchicalRoles(currentUserRoleName); + + // If the role for which users are being invited is not in the list of permissible roles that the + // current user can invite for, throw an error + if (!appsmithRoles.contains(invitedRole)) { + return Mono.error(new AppsmithException(AppsmithError.ACTION_IS_NOT_AUTHORIZED)); + } + + return Mono.just(Boolean.TRUE); + } + } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/SeedMongoData.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/SeedMongoData.java index d6e8f99629..f1e728b98b 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/SeedMongoData.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/SeedMongoData.java @@ -1,6 +1,7 @@ package com.appsmith.server.configurations; import com.appsmith.external.models.Policy; +import com.appsmith.server.acl.AppsmithRole; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.OrganizationPlugin; @@ -8,6 +9,7 @@ import com.appsmith.server.domains.Page; import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.PluginType; import com.appsmith.server.domains.User; +import com.appsmith.server.domains.UserRole; import com.appsmith.server.domains.UserState; import com.appsmith.server.repositories.ApplicationRepository; import com.appsmith.server.repositories.OrganizationRepository; @@ -32,6 +34,7 @@ import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_ORGANIZATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; import static com.appsmith.server.acl.AclPermission.MANAGE_USERS; +import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS; import static com.appsmith.server.acl.AclPermission.ORGANIZATION_MANAGE_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.READ_ORGANIZATIONS; @@ -72,6 +75,10 @@ public class SeedMongoData { .users(Set.of(API_USER_EMAIL, TEST_USER_EMAIL)) .build(); + Policy inviteUserOrgPolicy = Policy.builder().permission(ORGANIZATION_INVITE_USERS.getValue()) + .users(Set.of(API_USER_EMAIL)) + .build(); + Policy managePagePolicy = Policy.builder().permission(MANAGE_PAGES.getValue()) .users(Set.of(API_USER_EMAIL)) .build(); @@ -106,9 +113,9 @@ public class SeedMongoData { }; Object[][] orgData = { {"Spring Test Organization", "appsmith-spring-test.com", "appsmith.com", "spring-test-organization", - Set.of(manageOrgAppPolicy, manageOrgPolicy, readOrgPolicy)}, + Set.of(manageOrgAppPolicy, manageOrgPolicy, readOrgPolicy, inviteUserOrgPolicy)}, {"Another Test Organization", "appsmith-another-test.com", "appsmith.com", "another-test-organization", - Set.of(manageOrgAppPolicy, manageOrgPolicy, readOrgPolicy)} + Set.of(manageOrgAppPolicy, manageOrgPolicy, readOrgPolicy, inviteUserOrgPolicy)} }; Object[][] appData = { @@ -173,6 +180,17 @@ public class SeedMongoData { List orgPlugins = new ArrayList<>(); orgPlugins.add(orgPlugin); organization.setPlugins(orgPlugins); + + List userRoles = new ArrayList<>(); + UserRole userRole = new UserRole(); + String roleName = "Administrator"; + userRole.setRole(AppsmithRole.generateAppsmithRoleFromName(roleName)); + userRole.setUsername(API_USER_EMAIL); + userRole.setRoleName(roleName); + userRoles.add(userRole); + organization.setUserRoles(userRoles); + + log.debug("In the orgFlux. Create Organization: {}", organization); return organization; }).flatMap(organizationRepository::save) diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java index b3596cb418..26dd37b135 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java @@ -3,6 +3,7 @@ package com.appsmith.server.services; import com.appsmith.external.models.Policy; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.AppsmithRole; +import com.appsmith.server.acl.RoleGraph; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Datasource; @@ -76,6 +77,9 @@ public class OrganizationServiceTest { @Autowired DatasourceRepository datasourceRepository; + @Autowired + RoleGraph roleGraph; + Organization organization; @Before @@ -248,8 +252,10 @@ public class OrganizationServiceTest { } @Test - public void getAllUserRolesForOrganizationDomain() { - Mono> userRolesForOrganization = organizationService.getUserRolesForOrganization(); + @WithUserDetails(value = "api_user") + public void getAllUserRolesForOrganizationDomainAsAdministrator() { + Mono> userRolesForOrganization = organizationService.create(organization) + .flatMap(createdOrg -> organizationService.getUserRolesForOrganization(createdOrg.getId())); StepVerifier.create(userRolesForOrganization) .assertNext(roles -> { @@ -319,7 +325,7 @@ public class OrganizationServiceTest { assertThat(org).isNotNull(); assertThat(org.getName()).isEqualTo("Spring Test Organization"); - assertThat(org.getUserRoles().get(0).getUsername()).isEqualTo("usertest@usertest.com"); + assertThat(org.getUserRoles().get(1).getUsername()).isEqualTo("usertest@usertest.com"); Policy manageOrgAppPolicy = Policy.builder().permission(ORGANIZATION_MANAGE_APPLICATIONS.getValue()) .users(Set.of("api_user", "usertest@usertest.com")) @@ -874,4 +880,50 @@ public class OrganizationServiceTest { .verifyComplete(); } + @Test + @WithUserDetails(value = "api_user") + public void inviteRolesGivenAdministrator() { + Set roles = roleGraph.generateHierarchicalRoles("Administrator"); + AppsmithRole administratorRole = AppsmithRole.generateAppsmithRoleFromName("Administrator"); + AppsmithRole developerRole = AppsmithRole.generateAppsmithRoleFromName("Developer"); + AppsmithRole viewerRole = AppsmithRole.generateAppsmithRoleFromName("App Viewer"); + + StepVerifier.create(Mono.just(roles)) + .assertNext(appsmithRoles -> { + assertThat(appsmithRoles).isNotNull(); + assertThat(appsmithRoles).containsAll(Set.of(administratorRole, developerRole, viewerRole)); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void inviteRolesGivenDeveloper() { + Set roles = roleGraph.generateHierarchicalRoles("Developer"); + AppsmithRole developerRole = AppsmithRole.generateAppsmithRoleFromName("Developer"); + AppsmithRole viewerRole = AppsmithRole.generateAppsmithRoleFromName("App Viewer"); + + StepVerifier.create(Mono.just(roles)) + .assertNext(appsmithRoles -> { + assertThat(appsmithRoles).isNotNull(); + assertThat(appsmithRoles).containsAll(Set.of(developerRole, viewerRole)); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void inviteRolesGivenViewer() { + Set roles = roleGraph.generateHierarchicalRoles("App Viewer"); + AppsmithRole viewerRole = AppsmithRole.generateAppsmithRoleFromName("App Viewer"); + + StepVerifier.create(Mono.just(roles)) + .assertNext(appsmithRoles -> { + assertThat(appsmithRoles).isNotNull(); + assertThat(appsmithRoles).hasSize(1); + assertThat(appsmithRoles).containsAll(Set.of(viewerRole)); + }) + .verifyComplete(); + } + }