From 626731b2570287cb5505a7ef2b435a2d2ecfee07 Mon Sep 17 00:00:00 2001 From: dengfeige Date: Tue, 8 Nov 2022 18:33:17 +0800 Subject: [PATCH] feat: Support user role settings --- .gitignore | 2 + .../api/aop/WebExceptionAspect.java | 1 + .../api/auth/AuthenticatedMember.java | 2 - .../api/auth/GuestAuthenticationProvider.java | 6 +- .../api/auth/GuestAuthenticationToken.java | 10 -- .../api/auth/JwtConfiguration.java | 55 ++++++----- .../com/featureprobe/api/auth/JwtHelper.java | 38 ++------ .../api/auth/LoginSuccessHandler.java | 32 ++++++- .../featureprobe/api/auth/SecurityConfig.java | 8 +- .../featureprobe/api/auth/TokenHelper.java | 5 +- .../UserPasswordAuthenticationProvider.java | 5 +- .../auth/UserPasswordAuthenticationToken.java | 12 --- .../api/dto/MemberCreateRequest.java | 4 + .../featureprobe/api/dto/MemberResponse.java | 2 + .../api/dto/MemberUpdateRequest.java | 6 ++ .../featureprobe/api/mapper/MemberMapper.java | 1 - .../api/service/GuestService.java | 43 +++++---- .../api/service/MemberService.java | 92 ++++++++++++++----- .../db/migration/V39__delete_member_role.sql | 1 + .../api/service/GuestServiceSpec.groovy | 14 ++- .../api/service/MemberServiceSpec.groovy | 41 +++++---- .../service/OrganizationServiceSpec.groovy | 3 +- .../UserPasswordAuthenticationSpec.groovy | 3 +- .../api/base/enums/OrganizationRoleEnum.java | 9 +- .../featureprobe/api/base/enums/RoleEnum.java | 2 +- .../featureprobe/api/dao/entity/Member.java | 29 +++--- .../api/dao/entity/Organization.java | 6 -- .../api/dao/entity/OrganizationMember.java | 15 +-- 28 files changed, 258 insertions(+), 189 deletions(-) create mode 100644 feature-probe-admin/src/main/resources/db/migration/V39__delete_member_role.sql diff --git a/.gitignore b/.gitignore index 94a0491..a6edb8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ output **/target/ +/target/ /logs/ *.iml .arcconfig @@ -12,3 +13,4 @@ pom.xml.versionsBackup .DS_Store *.tar.gz *.log +.fleet diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/aop/WebExceptionAspect.java b/feature-probe-admin/src/main/java/com/featureprobe/api/aop/WebExceptionAspect.java index 1a07b09..b900ab3 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/aop/WebExceptionAspect.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/aop/WebExceptionAspect.java @@ -74,6 +74,7 @@ public void invalidArgumentHandler(HttpServletResponse response, IllegalArgument response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.getWriter().write(toErrorResponse(ResponseCode.INVALID_REQUEST, i18nConverter.get(e.getMessage()))); + log.error("invalidArgumentHandler", e); } private String toErrorResponse(ResponseCode resourceCode) { diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/AuthenticatedMember.java b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/AuthenticatedMember.java index 4863f2f..35c9cf3 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/AuthenticatedMember.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/AuthenticatedMember.java @@ -13,7 +13,6 @@ public class AuthenticatedMember implements AuthenticatedPrincipal { private Long id; private String name; - private String role; private List organizations; @@ -22,7 +21,6 @@ public static AuthenticatedMember create(Member member) { AuthenticatedMember authenticatedMember = new AuthenticatedMember(); authenticatedMember.setId(member.getId()); authenticatedMember.setName(member.getAccount()); - authenticatedMember.setRole(member.getRole().name()); authenticatedMember.setOrganizations(member.getOrganizations()); return authenticatedMember; } diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProvider.java b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProvider.java index 5141b38..4a0fe67 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProvider.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/GuestAuthenticationProvider.java @@ -12,6 +12,7 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; import java.util.Optional; @@ -27,6 +28,7 @@ public class GuestAuthenticationProvider implements AuthenticationProvider { private OperationLogService operationLogService; @Override + @Transactional(rollbackFor = Exception.class) public Authentication authenticate(Authentication authentication) throws AuthenticationException { GuestAuthenticationToken token = (GuestAuthenticationToken) authentication; Optional member = memberService.findByAccount(token.getAccount()); @@ -36,12 +38,12 @@ public Authentication authenticate(Authentication authentication) throws Authent memberService.updateVisitedTime(token.getAccount()); operationLogService.save(log); return new UserPasswordAuthenticationToken(AuthenticatedMember.create(member.get()), - Arrays.asList(new SimpleGrantedAuthority(member.get().getRole().name()))); + Arrays.asList()); } else { Member newMember = guestService.initGuest(token.getAccount(), token.getSource()); operationLogService.save(log); return new UserPasswordAuthenticationToken(AuthenticatedMember.create(newMember), - Arrays.asList(new SimpleGrantedAuthority(newMember.getRole().name()))); + Arrays.asList()); } } diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/GuestAuthenticationToken.java b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/GuestAuthenticationToken.java index 10a195b..2df2bca 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/GuestAuthenticationToken.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/GuestAuthenticationToken.java @@ -1,6 +1,5 @@ package com.featureprobe.api.auth; -import com.featureprobe.api.base.enums.RoleEnum; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -31,9 +30,6 @@ public GuestAuthenticationToken(AuthenticatedMember principal, Collection organizations, + String roleName) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(Instant.now().toEpochMilli()); calendar.add(Calendar.HOUR, 12); JWTCreator.Builder jwtBuilder = JWT.create().withSubject(member.getName()); jwtBuilder.withClaim(ACCOUNT_KEY, member.getName()); jwtBuilder.withClaim(USER_ID_KEY, member.getId()); - jwtBuilder.withClaim(ROLE_KEY, member.getRole()); - List organizations = new ArrayList<>(); - for (Organization organization : member.getOrganizations()) { - OrganizationMemberModel organizationMemberModel = organizationService - .queryOrganizationMember(organization.getId(), member.getId()); - organizations.add(organizationMemberModel); - } Map organizationMemberModelMap = organizations.stream().collect(Collectors .toMap(OrganizationMemberModel::getOrganizationId, Function.identity())); jwtBuilder.withClaim(ORGANIZATIONS, JsonMapper.toJSONString(organizationMemberModelMap)); - if (CollectionUtils.isNotEmpty(organizations)) { - jwtBuilder.withClaim(AUTHORITIES_CLAIM_NAME, organizations.get(0).getRoleName()); - } + jwtBuilder.withClaim(AUTHORITIES_CLAIM_NAME, roleName); + return jwtBuilder .withNotBefore(new Date()) .withExpiresAt(calendar.getTime()) - .sign(Algorithm.RSA256(publicKey, privateKey)); + .sign(Algorithm.RSA256(configuration.getRsaPublicKey(), configuration.getRsaPrivateKey())); } } diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/LoginSuccessHandler.java b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/LoginSuccessHandler.java index ed48d23..f78b740 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/LoginSuccessHandler.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/LoginSuccessHandler.java @@ -1,11 +1,15 @@ package com.featureprobe.api.auth; -import com.featureprobe.api.dto.CertificationUserResponse; +import com.featureprobe.api.base.model.OrganizationMemberModel; import com.featureprobe.api.base.util.JsonMapper; +import com.featureprobe.api.dao.entity.Organization; +import com.featureprobe.api.dto.CertificationUserResponse; +import com.featureprobe.api.service.OrganizationService; import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @@ -15,12 +19,15 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; @Component @AllArgsConstructor public class LoginSuccessHandler implements AuthenticationSuccessHandler { - private JwtHelper jwtHelper; + private final OrganizationService organizationService; + private final JwtConfiguration jwtConfiguration; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, @@ -31,11 +38,28 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo UserPasswordAuthenticationToken token = (UserPasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); AuthenticatedMember principal = token.getPrincipal(); - String jwt = jwtHelper.createJwtForMember(principal); + + List organizations = getOrganizationMemberModels(principal); + if (CollectionUtils.isEmpty(organizations)) { + throw new AuthenticationServiceException(principal.getName() + " organization is empty!"); + } + String roleName = organizations.get(0).getRoleName(); + String jwt = JwtHelper.createJwtForMember(jwtConfiguration, principal, organizations, roleName); + Long organizationId = CollectionUtils.isEmpty(principal.getOrganizations()) ? null : principal.getOrganizations().get(0).getId(); response.getWriter().write(JsonMapper.toJSONString(new CertificationUserResponse(token.getAccount(), - principal.getRole(), organizationId, jwt))); + roleName, organizationId, jwt))); + } + + private List getOrganizationMemberModels(AuthenticatedMember principal) { + List organizations = new ArrayList<>(); + for (Organization organization : principal.getOrganizations()) { + OrganizationMemberModel organizationMemberModel = organizationService + .queryOrganizationMember(organization.getId(), principal.getId()); + organizations.add(organizationMemberModel); + } + return organizations; } } diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/SecurityConfig.java b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/SecurityConfig.java index 752f9f5..34fa58b 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/SecurityConfig.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/SecurityConfig.java @@ -22,7 +22,10 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import javax.annotation.PostConstruct; import java.nio.charset.StandardCharsets; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; import java.util.Arrays; @Slf4j @@ -32,12 +35,11 @@ @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { - private LoginFailureHandler loginFailureHandler; private LoginSuccessHandler loginSuccessHandler; - private JWTConfig JWTConfig; + private JWTConfig jwtConfig; private UserPasswordAuthenticationProvider userPasswordAuthenticationProvider; @@ -99,7 +101,7 @@ protected void configure(HttpSecurity http) throws Exception { .authenticationEntryPoint(authenticationEntryPoint()); http.addFilterBefore(userPasswordAuthenticationProcessingFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class); - if (!JWTConfig.isGuestDisabled()) { + if (!jwtConfig.isGuestDisabled()) { http.addFilterBefore(guestAuthenticationProcessingFilter(authenticationManager()), UserPasswordAuthenticationProcessingFilter.class); } diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/TokenHelper.java b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/TokenHelper.java index 9e4d3e7..0c75cf4 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/TokenHelper.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/TokenHelper.java @@ -1,5 +1,6 @@ package com.featureprobe.api.auth; +import com.featureprobe.api.base.enums.OrganizationRoleEnum; import com.featureprobe.api.base.enums.RoleEnum; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; @@ -28,9 +29,9 @@ public static final String getRole() { return (String) authentication.getTokenAttributes().get(ROLE_KEY); } - public static final boolean isAdmin() { + public static final boolean isOwner() { JwtAuthenticationToken authentication = (JwtAuthenticationToken) SecurityContextHolder. getContext().getAuthentication(); - return RoleEnum.ADMIN.name().equals((String) authentication.getTokenAttributes().get(ROLE_KEY)); + return OrganizationRoleEnum.OWNER.name().equals(authentication.getTokenAttributes().get(ROLE_KEY)); } } diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProvider.java b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProvider.java index 16c5a1d..6600bf0 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProvider.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationProvider.java @@ -13,6 +13,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; import java.util.Optional; @@ -26,6 +27,7 @@ public class UserPasswordAuthenticationProvider implements AuthenticationProvide private OperationLogService operationLogService; @Override + @Transactional(rollbackFor = Exception.class) public Authentication authenticate(Authentication authentication) throws AuthenticationException { UserPasswordAuthenticationToken token = (UserPasswordAuthenticationToken) authentication; if (StringUtils.isNotBlank(token.getAccount()) && StringUtils.isNotBlank(token.getPassword())) { @@ -36,8 +38,7 @@ public Authentication authenticate(Authentication authentication) throws Authent && new BCryptPasswordEncoder().matches(token.getPassword(), member.get().getPassword())) { memberService.updateVisitedTime(token.getAccount()); operationLogService.save(log); - return new UserPasswordAuthenticationToken(AuthenticatedMember.create(member.get()), - Arrays.asList(new SimpleGrantedAuthority(member.get().getRole().name()))); + return new UserPasswordAuthenticationToken(AuthenticatedMember.create(member.get()), Arrays.asList()); } } return null; diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationToken.java b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationToken.java index 42ca78b..1224058 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationToken.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/auth/UserPasswordAuthenticationToken.java @@ -1,6 +1,5 @@ package com.featureprobe.api.auth; -import com.featureprobe.api.base.enums.RoleEnum; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -32,10 +31,6 @@ public UserPasswordAuthenticationToken(AuthenticatedMember principal, super.setAuthenticated(true); } - public boolean isAdmin() { - return RoleEnum.ADMIN.name().equals(getRole()); - } - @Override public Object getCredentials() { return null; @@ -58,11 +53,4 @@ public String getPassword() { return password; } - public String getRole() { - if (principal == null) { - return null; - } - return principal.getRole(); - } - } diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberCreateRequest.java b/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberCreateRequest.java index e80b476..d10c914 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberCreateRequest.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberCreateRequest.java @@ -1,5 +1,6 @@ package com.featureprobe.api.dto; +import com.featureprobe.api.base.enums.OrganizationRoleEnum; import lombok.Data; import javax.validation.constraints.NotBlank; @@ -14,6 +15,9 @@ public class MemberCreateRequest { private String source; + @NotNull + private OrganizationRoleEnum role; + @NotBlank private String password; diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberResponse.java b/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberResponse.java index 0b8e70f..47808e6 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberResponse.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberResponse.java @@ -15,6 +15,8 @@ public class MemberResponse { private String role; + private boolean allowEdit; + private String createdBy; private Date visitedTime; diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberUpdateRequest.java b/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberUpdateRequest.java index 0346ec2..208c107 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberUpdateRequest.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/dto/MemberUpdateRequest.java @@ -1,8 +1,10 @@ package com.featureprobe.api.dto; +import com.featureprobe.api.base.enums.OrganizationRoleEnum; import lombok.Data; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; @Data public class MemberUpdateRequest { @@ -13,4 +15,8 @@ public class MemberUpdateRequest { @NotBlank private String password; + @NotNull + private OrganizationRoleEnum role; + + } diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/mapper/MemberMapper.java b/feature-probe-admin/src/main/java/com/featureprobe/api/mapper/MemberMapper.java index efb2b5d..7c5049c 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/mapper/MemberMapper.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/mapper/MemberMapper.java @@ -16,7 +16,6 @@ public interface MemberMapper extends BaseMapper { MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class); - @Mapping(target = "role", expression = "java(member.getRole().name())") @Mapping(target = "account", expression = "java(member.getAccount())") @Mapping(target = "createdBy", expression = "java(getAccount(member.getCreatedBy()))") @Mapping(target = "visitedTime", expression = "java(member.getVisitedTime())") diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/service/GuestService.java b/feature-probe-admin/src/main/java/com/featureprobe/api/service/GuestService.java index ff26033..a3948e0 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/service/GuestService.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/service/GuestService.java @@ -1,12 +1,13 @@ package com.featureprobe.api.service; +import com.featureprobe.api.base.enums.OrganizationRoleEnum; import com.featureprobe.api.config.JWTConfig; import com.featureprobe.api.base.db.ExcludeTenant; +import com.featureprobe.api.dao.repository.OrganizationRepository; import com.featureprobe.api.dto.ProjectCreateRequest; import com.featureprobe.api.dao.entity.Member; import com.featureprobe.api.dao.entity.Organization; import com.featureprobe.api.dao.repository.MemberRepository; -import com.featureprobe.api.base.enums.RoleEnum; import com.featureprobe.api.base.tenant.TenantContext; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -27,8 +28,6 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.List; @Slf4j @AllArgsConstructor @@ -40,6 +39,8 @@ public class GuestService { private MemberRepository memberRepository; + private OrganizationRepository organizationRepository; + @PersistenceContext public EntityManager entityManager; @@ -53,24 +54,30 @@ public class GuestService { @Transactional(rollbackFor = Exception.class) public Member initGuest(String account, String source) { - Member createdMember = new Member(); - createdMember.setAccount(account); - createdMember.setPassword(passwordEncoder.encode(JWTConfig.getGuestDefaultPassword())); - createdMember.setRole(RoleEnum.ADMIN); - List organizations = new ArrayList<>(1); - organizations.add(new Organization(account)); - createdMember.setOrganizations(organizations); - createdMember.setSource(source); - Member savedMember = memberRepository.save(createdMember); + Organization organization = organizationRepository.save(new Organization(account)); + Member guestMember = createGuestMember(account, source, organization); + + loginGuestUser(guestMember); + initProjectEnvironment(String.valueOf(organization.getId()), GUEST_INIT_PROJECT_KEY); + initToggles(organization.getId(), guestMember.getId()); + return guestMember; + } + + private Member createGuestMember(String account, String source, Organization organization) { + Member member = new Member(); + member.setAccount(account); + member.setPassword(passwordEncoder.encode(JWTConfig.getGuestDefaultPassword())); + member.setSource(source); + member.addOrganization(organization, OrganizationRoleEnum.OWNER); + return memberRepository.save(member); + } + + private void loginGuestUser(Member member) { SecurityContextHolder.setContext(new SecurityContextImpl(new JwtAuthenticationToken(Jwt.withTokenValue("_") - .claim("userId", savedMember.getId()).claim("account", savedMember.getAccount()) - .claim("role", savedMember.getRole().name()) + .claim("userId", member.getId()).claim("account", member.getAccount()) + .claim("role", member.getOrganizationMembers().get(0).getRole()) .header("iss", "") .build()))); - Organization organization = savedMember.getOrganizations().get(0); - initProjectEnvironment(String.valueOf(organization.getId()), GUEST_INIT_PROJECT_KEY); - initToggles(organization.getId(), savedMember.getId()); - return savedMember; } private void initProjectEnvironment(String tenantId, String projectName) { diff --git a/feature-probe-admin/src/main/java/com/featureprobe/api/service/MemberService.java b/feature-probe-admin/src/main/java/com/featureprobe/api/service/MemberService.java index 5434070..02bf6e3 100644 --- a/feature-probe-admin/src/main/java/com/featureprobe/api/service/MemberService.java +++ b/feature-probe-admin/src/main/java/com/featureprobe/api/service/MemberService.java @@ -2,25 +2,25 @@ import com.featureprobe.api.auth.TokenHelper; import com.featureprobe.api.base.constants.MessageKey; +import com.featureprobe.api.base.db.ExcludeTenant; +import com.featureprobe.api.base.enums.OrganizationRoleEnum; +import com.featureprobe.api.base.enums.ResourceType; import com.featureprobe.api.base.exception.ForbiddenException; +import com.featureprobe.api.base.tenant.TenantContext; +import com.featureprobe.api.dao.entity.Member; +import com.featureprobe.api.dao.entity.Organization; +import com.featureprobe.api.dao.entity.OrganizationMember; import com.featureprobe.api.dao.exception.ResourceNotFoundException; +import com.featureprobe.api.dao.repository.MemberRepository; +import com.featureprobe.api.dao.repository.OrganizationMemberRepository; +import com.featureprobe.api.dao.repository.OrganizationRepository; import com.featureprobe.api.dao.utils.PageRequestUtil; -import com.featureprobe.api.base.db.ExcludeTenant; import com.featureprobe.api.dto.MemberCreateRequest; import com.featureprobe.api.dto.MemberModifyPasswordRequest; import com.featureprobe.api.dto.MemberResponse; import com.featureprobe.api.dto.MemberSearchRequest; import com.featureprobe.api.dto.MemberUpdateRequest; -import com.featureprobe.api.dao.entity.Member; -import com.featureprobe.api.dao.entity.Organization; -import com.featureprobe.api.dao.entity.OrganizationMember; -import com.featureprobe.api.base.enums.ResourceType; -import com.featureprobe.api.base.enums.RoleEnum; import com.featureprobe.api.mapper.MemberMapper; -import com.featureprobe.api.dao.repository.MemberRepository; -import com.featureprobe.api.dao.repository.OrganizationMemberRepository; -import com.featureprobe.api.dao.repository.OrganizationRepository; -import com.featureprobe.api.base.tenant.TenantContext; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -35,7 +35,6 @@ import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.criteria.Predicate; -import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; @@ -60,8 +59,6 @@ public class MemberService { @PersistenceContext public EntityManager entityManager; - private static final String API_CREATE_MEMBER_SOURCE = "INTERNAL"; - private static final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); @Transactional(rollbackFor = Exception.class) @@ -76,6 +73,14 @@ public MemberResponse update(MemberUpdateRequest updateRequest) { verifyAdminPrivileges(); Member member = findMemberByAccount(updateRequest.getAccount()); MemberMapper.INSTANCE.mapEntity(updateRequest, member); + OrganizationMember organizationMember = member.getOrganizationMembers() + .stream() + .filter(it -> it.getOrganization().getId().equals(TenantContext.getCurrentOrganization() + .getOrganizationId())).findFirst() + .orElseThrow(() -> new ResourceNotFoundException(ResourceType.ORGANIZATION_MEMBER, + member.getAccount())); + organizationMember.setRole(updateRequest.getRole()); + return MemberMapper.INSTANCE.entityToResponse(memberRepository.save(member)); } @@ -111,19 +116,20 @@ private List newNumbers(MemberCreateRequest createRequest) { return createRequest.getAccounts() .stream() .filter(account -> memberIncludeDeletedService.validateAccountIncludeDeleted(account)) - .map(account -> newMember(account, createRequest.getSource(), createRequest.getPassword())) + .map(account -> newMember(account, createRequest)) .collect(Collectors.toList()); } - private Member newMember(String account, String source, String password) { + private Member newMember(String account, MemberCreateRequest createRequest) { Member member = new Member(); member.setAccount(account); - member.setSource(source); - member.setRole(RoleEnum.MEMBER); - member.setPassword(new BCryptPasswordEncoder().encode(password)); + member.setSource(createRequest.getSource()); + member.setPassword(new BCryptPasswordEncoder().encode(createRequest.getPassword())); + Organization organization = organizationRepository.findById(TenantContext.getCurrentOrganization() .getOrganizationId()).get(); - member.setOrganizations(Arrays.asList(organization)); + member.addOrganization(organization, createRequest.getRole()); + return member; } @@ -138,7 +144,7 @@ public Optional findByAccount(String account) { } private void verifyAdminPrivileges() { - if (!TokenHelper.isAdmin()) { + if (!TokenHelper.isOwner()) { throw new ForbiddenException(); } } @@ -146,17 +152,53 @@ private void verifyAdminPrivileges() { public Page list(MemberSearchRequest searchRequest) { Pageable pageable = PageRequestUtil.toPageable(searchRequest, Sort.Direction.DESC, "createdTime"); Specification spec = (root, query, cb) -> { - Predicate p1 = cb.equal(root.get("organizationId"), TenantContext.getCurrentOrganization() + Predicate p1 = cb.equal(root.get("organization").get("id"), TenantContext.getCurrentOrganization() .getOrganizationId()); return query.where(cb.and(p1)).getRestriction(); }; Page organizationMembers = organizationMemberRepository.findAll(spec, pageable); - List memberIds = organizationMembers.getContent().stream().map(OrganizationMember::getMemberId) + List memberIds = organizationMembers.getContent() + .stream() + .map(organizationMember -> organizationMember.getMember().getId()) .collect(Collectors.toList()); - Map memberMap = memberRepository.findAllById(memberIds).stream() + Map idToMember = memberRepository.findAllById(memberIds).stream() .collect(Collectors.toMap(Member::getId, Function.identity())); - return organizationMembers.map(item -> - MemberMapper.INSTANCE.entityToResponse(memberMap.get(item.getMemberId()))); + + return convertToResponse(getOwnerTotalCount(), + TokenHelper.isOwner(), + organizationMembers, idToMember); + } + + private long getOwnerTotalCount() { + Specification spec = (root, query, cb) -> { + Predicate p1 = cb.equal(root.get("organization").get("id"), TenantContext.getCurrentOrganization() + .getOrganizationId()); + Predicate p2 = cb.equal(root.get("role"), OrganizationRoleEnum.OWNER); + return query.where(cb.and(p1, p2)).getRestriction(); + }; + return organizationMemberRepository.count(spec); + + } + + private Page convertToResponse(long ownerCount, + boolean currentIsOwner, + Page organizationMembers, + Map idToMember) { + return organizationMembers.map(item -> { + MemberResponse response = MemberMapper.INSTANCE.entityToResponse(idToMember.get(item.getMember().getId())); + if (item.getRole() == null) { + response.setAllowEdit(false); + return response; + } + response.setRole(item.getRole().name()); + boolean allowEdit = currentIsOwner; + + if (allowEdit && item.getRole().isOwner() && ownerCount == 1) { + allowEdit = false; + } + response.setAllowEdit(allowEdit); + return response; + }); } public MemberResponse queryByAccount(String account) { diff --git a/feature-probe-admin/src/main/resources/db/migration/V39__delete_member_role.sql b/feature-probe-admin/src/main/resources/db/migration/V39__delete_member_role.sql new file mode 100644 index 0000000..422b4de --- /dev/null +++ b/feature-probe-admin/src/main/resources/db/migration/V39__delete_member_role.sql @@ -0,0 +1 @@ +alter table member drop column role; \ No newline at end of file diff --git a/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/GuestServiceSpec.groovy b/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/GuestServiceSpec.groovy index 910bb78..7e551f9 100644 --- a/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/GuestServiceSpec.groovy +++ b/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/GuestServiceSpec.groovy @@ -1,5 +1,6 @@ package com.featureprobe.api.service +import com.featureprobe.api.base.enums.OrganizationRoleEnum import com.featureprobe.api.component.SpringBeanManager import com.featureprobe.api.config.JWTConfig import com.featureprobe.api.base.enums.RoleEnum @@ -7,7 +8,9 @@ import com.featureprobe.api.dao.entity.Dictionary import com.featureprobe.api.dao.entity.Environment import com.featureprobe.api.dao.entity.Member import com.featureprobe.api.dao.entity.Organization +import com.featureprobe.api.dao.entity.OrganizationMember import com.featureprobe.api.dao.entity.Project +import com.featureprobe.api.dao.repository.OrganizationRepository import com.featureprobe.api.dao.repository.PublishMessageRepository import com.featureprobe.api.dao.repository.DictionaryRepository import com.featureprobe.api.dao.repository.EnvironmentRepository @@ -37,7 +40,9 @@ class GuestServiceSpec extends Specification { EntityManager entityManager - PublishMessageRepository changeLogRepository; + OrganizationRepository organizationRepository + + PublishMessageRepository changeLogRepository DictionaryRepository dictionaryRepository @@ -56,12 +61,13 @@ class GuestServiceSpec extends Specification { entityManager = Mock(SessionImpl) projectRepository = Mock(ProjectRepository) environmentRepository = Mock(EnvironmentRepository) + organizationRepository = Mock(OrganizationRepository) targetingSketchRepository = Mock(TargetingSketchRepository) changeLogRepository = Mock(PublishMessageRepository) dictionaryRepository = Mock(DictionaryRepository) changeLogService = new ChangeLogService(changeLogRepository, environmentRepository, dictionaryRepository) projectService = new ProjectService(projectRepository, environmentRepository, targetingSketchRepository, changeLogService, entityManager) - guestService = new GuestService(appConfig, memberRepository, entityManager, projectService) + guestService = new GuestService(appConfig, memberRepository, organizationRepository, entityManager, projectService) applicationContext = Mock(ApplicationContext) SpringBeanManager.applicationContext = applicationContext } @@ -73,7 +79,9 @@ class GuestServiceSpec extends Specification { def guest = guestService.initGuest("Admin", "test") then: 1 * applicationContext.getBean(_) >> new FeatureProbe("_") - 1 * memberRepository.save(_) >> new Member(id: 1, account: "Admin", role: RoleEnum.ADMIN, organizations: [new Organization(id: 1)]) + 1 * memberRepository.save(_) >> new Member(id: 1, account: "Admin", + organizationMembers: [new OrganizationMember(role: OrganizationRoleEnum.OWNER)]) + 1 * organizationRepository.save(_) >> new Organization(name: "Admin") 1 * projectRepository.count() >> 2 1 * projectRepository.save(_) >> new Project(name: "projectName", key: "projectKey", environments: [new Environment()]) diff --git a/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/MemberServiceSpec.groovy b/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/MemberServiceSpec.groovy index e64ae06..c9b293b 100644 --- a/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/MemberServiceSpec.groovy +++ b/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/MemberServiceSpec.groovy @@ -58,7 +58,7 @@ class MemberServiceSpec extends Specification { then: 1 * memberRepository.existsByAccount("root") >> false 1 * organizationRepository.findById(_) >> Optional.of(new Organization(id: 1, name: "organization name")) - 1 * memberRepository.saveAll(_) >> [new Member(account: "root", password: "password", role: RoleEnum.MEMBER)] + 1 * memberRepository.saveAll(_) >> [new Member(account: "root", password: "password")] with(savedMember) { 1 == savedMember.size() } @@ -77,25 +77,25 @@ class MemberServiceSpec extends Specification { def "update a member"() { given: - setAuthContext("Admin", "ADMIN") + setAuthContext("Admin", "OWNER") when: def response = memberService.update(new MemberUpdateRequest(account: "root", password: "root")) then: 1 * memberRepository.findByAccount("root") >> - Optional.of(new Member(account: "root", password: "root", role: "MEMBER")) - 1 * memberRepository.save(_) >> new Member(account: "root", password: "root", role: "MEMBER") + Optional.of(new Member(account: "root", password: "root", + organizationMembers: [new OrganizationMember(organization: new Organization(id: 1))])) + 1 * memberRepository.save(_) >> new Member(account: "root", password: "root") with(response) { "root" == account - "MEMBER" == role } } def "modify member password success"() { given: - setAuthContext("test", "MEMBER") + setAuthContext("test", "WRITER") when: def modify = memberService.modifyPassword(new MemberModifyPasswordRequest(newPassword: "root", @@ -104,19 +104,18 @@ class MemberServiceSpec extends Specification { then: 1 * memberRepository.findByAccount("test") >> Optional.of(new Member(account: "test", - password: "\$2a\$10\$WO5tC7A/nsPe5qmVmjTIPeKD0R/Tm2YsNiVP0geCerT0hIRLBCxZ6", role: "MEMBER")) - 1 * memberRepository.save(_) >> new Member(account: "root", password: "323232", role: "MEMBER") + password: "\$2a\$10\$WO5tC7A/nsPe5qmVmjTIPeKD0R/Tm2YsNiVP0geCerT0hIRLBCxZ6")) + 1 * memberRepository.save(_) >> new Member(account: "root", password: "323232") with(modify) { "root" == account - "MEMBER" == role } } def "modify member password failed when old password error"() { given: - setAuthContext("test", "MEMBER") + setAuthContext("test", "WRITER") memberRepository.findByAccount("test") >> Optional.of( - new Member(account: "test", password: "abcdefg", role: "MEMBER")) + new Member(account: "test", password: "abcdefg")) when: memberService.modifyPassword(new MemberModifyPasswordRequest(newPassword: "root", oldPassword: "Pass1234")) @@ -127,27 +126,26 @@ class MemberServiceSpec extends Specification { def "delete a member"() { given: - setAuthContext("Admin", "ADMIN") + setAuthContext("Admin", "OWNER") TenantContext.setCurrentTenant("1") when: def response = memberService.delete("root") then: 1 * memberRepository.findByAccount("root") >> - Optional.of(new Member(id: 1, account: "root", password: "root", role: "MEMBER")) + Optional.of(new Member(id: 1, account: "root", password: "root")) 1 * organizationMemberRepository.findByOrganizationIdAndMemberId(1, 1) >> Optional.of(new OrganizationMember()) 1 * organizationMemberRepository.delete(_) - 1 * memberRepository.save(_) >> new Member(account: "root", password: "root", role: "MEMBER") + 1 * memberRepository.save(_) >> new Member(account: "root", password: "root") with(response) { "root" == account - "MEMBER" == role } } def "delete member failed when logged user is not admin"() { given: - setAuthContext("user", "MEMBER") + setAuthContext("user", "WRITER") when: memberService.delete("s1") @@ -162,19 +160,22 @@ class MemberServiceSpec extends Specification { then: 1 * memberRepository.findByAccount("test") >> - Optional.of(new Member(account: "test", password: "test", role: "MEMBER")) + Optional.of(new Member(account: "test", password: "test")) 1 * memberRepository.save(_) } def "query member list"() { + given: + setAuthContext("user", "WRITER") + when: def list = memberService.list(new MemberSearchRequest(keyword: "root", pageIndex: 0, pageSize: 10)) then: - 1 * organizationMemberRepository.findAll(_, _) >> new PageImpl<>([new OrganizationMember(memberId: 1)], + 1 * organizationMemberRepository.findAll(_, _) >> new PageImpl<>([new OrganizationMember(member: new Member(id: 1), role: OrganizationRoleEnum.OWNER)], PageRequest.of(1, 10), 1) - 1 * memberRepository.findAllById([1]) >> [new Member()] + 1 * memberRepository.findAllById([1]) >> [new Member(id: 1)] with(list) { 1 == size() } @@ -185,7 +186,7 @@ class MemberServiceSpec extends Specification { def response = memberService.queryByAccount("root") then: - 1 * memberRepository.findByAccount("root") >> Optional.of(new Member(account: "root", role: RoleEnum.ADMIN)) + 1 * memberRepository.findByAccount("root") >> Optional.of(new Member(account: "root")) with(response) { "root" == account } diff --git a/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/OrganizationServiceSpec.groovy b/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/OrganizationServiceSpec.groovy index f06ed93..9979c1c 100644 --- a/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/OrganizationServiceSpec.groovy +++ b/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/OrganizationServiceSpec.groovy @@ -1,6 +1,7 @@ package com.featureprobe.api.service import com.featureprobe.api.base.enums.OrganizationRoleEnum +import com.featureprobe.api.dao.entity.Member import com.featureprobe.api.dao.entity.Organization import com.featureprobe.api.dao.entity.OrganizationMember import com.featureprobe.api.dao.repository.OrganizationMemberRepository @@ -24,7 +25,7 @@ class OrganizationServiceSpec extends Specification { def organizationMember = organizationService.queryOrganizationMember(1, 1) then: 1 * organizationMemberRepository.findByOrganizationIdAndMemberId(1, 1) >> - Optional.of(new OrganizationMember(1, 1, OrganizationRoleEnum.OWNER)) + Optional.of(new OrganizationMember(new Organization(id: 1), new Member(id: 1), OrganizationRoleEnum.OWNER)) 1 * organizationRepository.getById(1) >> new Organization(name: "Admin") "Admin" == organizationMember.organizationName } diff --git a/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/UserPasswordAuthenticationSpec.groovy b/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/UserPasswordAuthenticationSpec.groovy index 844dbae..b1afd5d 100644 --- a/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/UserPasswordAuthenticationSpec.groovy +++ b/feature-probe-admin/src/test/groovy/com/featureprobe/api/service/UserPasswordAuthenticationSpec.groovy @@ -32,8 +32,7 @@ class UserPasswordAuthenticationSpec extends Specification { def authenticate = userPasswordAuthenticationProvider.authenticate(token) then: 1 * memberService.findByAccount("admin") >> Optional.of(new Member(account: "Admin", - password: "\$2a\$10\$jeJ25nROU8APkG2ixK6zyecwzIJ8oHz0ZNqBDiwMXcy9lo9S3YGma", - role: RoleEnum.ADMIN)) + password: "\$2a\$10\$jeJ25nROU8APkG2ixK6zyecwzIJ8oHz0ZNqBDiwMXcy9lo9S3YGma")) 1 * memberService.updateVisitedTime("admin") 1 * operationLogRepository.save(_) with(authenticate) { diff --git a/feature-probe-base/src/main/java/com/featureprobe/api/base/enums/OrganizationRoleEnum.java b/feature-probe-base/src/main/java/com/featureprobe/api/base/enums/OrganizationRoleEnum.java index e6be3fa..630341c 100644 --- a/feature-probe-base/src/main/java/com/featureprobe/api/base/enums/OrganizationRoleEnum.java +++ b/feature-probe-base/src/main/java/com/featureprobe/api/base/enums/OrganizationRoleEnum.java @@ -2,5 +2,10 @@ public enum OrganizationRoleEnum { - OWNER, WRITER -} + OWNER, WRITER; + + public boolean isOwner() { + return this == OWNER; + } + +} \ No newline at end of file diff --git a/feature-probe-base/src/main/java/com/featureprobe/api/base/enums/RoleEnum.java b/feature-probe-base/src/main/java/com/featureprobe/api/base/enums/RoleEnum.java index abe83e7..57a2dd4 100644 --- a/feature-probe-base/src/main/java/com/featureprobe/api/base/enums/RoleEnum.java +++ b/feature-probe-base/src/main/java/com/featureprobe/api/base/enums/RoleEnum.java @@ -2,6 +2,6 @@ public enum RoleEnum { - ADMIN, MEMBER; + OWNER, WRITER } diff --git a/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/Member.java b/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/Member.java index 2a07e1c..535e63b 100644 --- a/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/Member.java +++ b/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/Member.java @@ -1,6 +1,6 @@ package com.featureprobe.api.dao.entity; -import com.featureprobe.api.base.enums.RoleEnum; +import com.featureprobe.api.base.enums.OrganizationRoleEnum; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -16,16 +16,13 @@ import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; import javax.persistence.FetchType; -import javax.persistence.JoinColumn; -import javax.persistence.JoinTable; -import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; import javax.persistence.Table; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; @NoArgsConstructor @AllArgsConstructor @@ -46,22 +43,26 @@ public class Member extends AbstractAuditEntity { @Column(name = "visited_time") private Date visitedTime; - @Enumerated(EnumType.STRING) - private RoleEnum role; - @Column(columnDefinition = "TINYINT") private Boolean deleted; private String source; - @ManyToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY) - @JoinTable(name = "organization_member", joinColumns = @JoinColumn(name = "member_id"), - inverseJoinColumns = @JoinColumn(name = "organization_id")) - @Fetch(FetchMode.JOIN) - private List organizations = new ArrayList<>(); + @OneToMany(mappedBy = "member", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY) + private List organizationMembers = new ArrayList<>(); public Member(Long id, String account) { super.setId(id); this.account = account; } + + public List getOrganizations() { + return organizationMembers.stream() + .map(OrganizationMember::getOrganization) + .collect(Collectors.toList()); + } + + public void addOrganization(Organization organization, OrganizationRoleEnum role) { + this.organizationMembers.add(new OrganizationMember(organization, this, role)); + } } diff --git a/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/Organization.java b/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/Organization.java index 35634e3..1b0be26 100644 --- a/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/Organization.java +++ b/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/Organization.java @@ -14,10 +14,7 @@ import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.ManyToMany; import javax.persistence.Table; -import java.util.ArrayList; -import java.util.List; @NoArgsConstructor @AllArgsConstructor @@ -37,9 +34,6 @@ public class Organization extends AbstractAuditEntity { @Column(columnDefinition = "TINYINT") private boolean deleted; - @ManyToMany(mappedBy = "organizations") - private List members = new ArrayList<>(); - public Organization(String name) { this.name = name; } diff --git a/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/OrganizationMember.java b/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/OrganizationMember.java index 3f7ea61..104ab79 100644 --- a/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/OrganizationMember.java +++ b/feature-probe-dao/src/main/java/com/featureprobe/api/dao/entity/OrganizationMember.java @@ -6,10 +6,12 @@ import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicInsert; -import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; import javax.persistence.Table; @NoArgsConstructor @@ -20,13 +22,14 @@ @DynamicInsert public class OrganizationMember extends AbstractAuditEntity { - @Column(name = "organization_id") - private Long organizationId; + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "organization_id") + Organization organization; - @Column(name = "member_id") - private Long memberId; + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "member_id") + private Member member; @Enumerated(EnumType.STRING) private OrganizationRoleEnum role; - }