BJ-1016: Organization join (#3)

* BJ-1016: Organization join

* tests fix

* keycloak bump

* org find refactor

* Update src/main/java/com/rbkmoney/orgmanager/service/OrganizationService.java

Co-authored-by: Alexander Romanov <a.romanov@rbkmoney.com>

* Update src/main/java/com/rbkmoney/orgmanager/service/OrganizationService.java

Co-authored-by: Alexander Romanov <a.romanov@rbkmoney.com>

* keycloak config refactor

* method name typo

Co-authored-by: vitaxa <v.banin@rbkmoney.com>
Co-authored-by: Alexander Romanov <a.romanov@rbkmoney.com>
This commit is contained in:
vitaxa 2020-12-04 12:24:37 +03:00 committed by GitHub
parent 7bb623537e
commit b3b45c70f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1074 additions and 84 deletions

73
pom.xml
View File

@ -24,7 +24,7 @@
<dockerfile.base.service.tag>57e26d8ee999d7b0b50248c22afc63e6f926d276</dockerfile.base.service.tag>
<dockerfile.registry>dr2.rbkmoney.com</dockerfile.registry>
<shared.resources.version>0.3.6</shared.resources.version>
<sonar.jacoco.reportPaths>${project.basedir}/target/jacoco.exec</sonar.jacoco.reportPaths>
<keycloak.version>11.0.3</keycloak.version>
</properties>
<dependencies>
@ -52,7 +52,7 @@
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>swag-organizations</artifactId>
<version>1.8-1de59d5-epic-server</version>
<version>1.10-260e548-server</version>
</dependency>
<!--spring-->
@ -72,6 +72,40 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>${keycloak.version}</version>
<exclusions>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-multipart-provider</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>${keycloak.version}</version>
</dependency>
<!--third party-->
<dependency>
@ -98,6 +132,12 @@
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.1</version>
</dependency>
<!--test-->
<dependency>
@ -105,6 +145,18 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<version>2.2.4.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
@ -173,23 +225,6 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.5</version>
<configuration>
<destFile>${sonar.jacoco.reportPaths}</destFile>
<append>true</append>
</configuration>
<executions>
<execution>
<id>agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -2,9 +2,11 @@ package com.rbkmoney.orgmanager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@ConfigurationPropertiesScan
@SpringBootApplication
public class OrgManagerApplication extends SpringApplication {

View File

@ -0,0 +1,130 @@
package com.rbkmoney.orgmanager.config;
import com.google.common.base.Strings;
import com.rbkmoney.orgmanager.config.properties.KeyCloakProperties;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.management.HttpSessionManager;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
@EnableWebSecurity
@ComponentScan(
basePackageClasses = KeycloakSecurityComponents.class,
excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "org.keycloak.adapters.springsecurity.management.HttpSessionManager"
)
)
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
@ConditionalOnProperty(value = "auth.enabled", havingValue = "true")
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
private KeyCloakProperties keyCloakProperties;
@Override
protected HttpSessionManager httpSessionManager() {
return super.httpSessionManager();
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers(HttpMethod.GET, "/**/health").permitAll()
.anyRequest().authenticated();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(keycloakAuthenticationProvider());
}
@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return facade -> {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(adapterConfig());
deployment.setNotBefore(keyCloakProperties.getNotBefore());
return deployment;
};
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.applyPermitDefaultValues();
configuration.addAllowedMethod(HttpMethod.PUT);
configuration.addAllowedMethod(HttpMethod.DELETE);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
private AdapterConfig adapterConfig() {
String keycloakRealmPublicKey;
if (!Strings.isNullOrEmpty(keyCloakProperties.getRealmPublicKeyFilePath())) {
keycloakRealmPublicKey = readKeyFromFile(keyCloakProperties.getRealmPublicKeyFilePath());
} else {
keycloakRealmPublicKey = keyCloakProperties.getRealmPublicKey();
}
AdapterConfig adapterConfig = new AdapterConfig();
adapterConfig.setRealm(keyCloakProperties.getRealm());
adapterConfig.setRealmKey(keycloakRealmPublicKey);
adapterConfig.setResource(keyCloakProperties.getResource());
adapterConfig.setAuthServerUrl(keyCloakProperties.getAuthServerUrl());
adapterConfig.setUseResourceRoleMappings(true);
adapterConfig.setBearerOnly(true);
adapterConfig.setSslRequired(keyCloakProperties.getSslRequired());
return adapterConfig;
}
private String readKeyFromFile(String filePath) {
try {
List<String> strings = Files.readAllLines(Paths.get(filePath));
strings.remove(strings.size() - 1);
strings.remove(0);
return strings.stream().map(String::trim).collect(Collectors.joining());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}

View File

@ -0,0 +1,26 @@
package com.rbkmoney.orgmanager.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "keycloak")
@Data
public class KeyCloakProperties {
private String realm;
private String resource;
private String realmPublicKey;
private String realmPublicKeyFilePath;
private String authServerUrl;
private String sslRequired;
private int notBefore;
}

View File

@ -41,9 +41,7 @@ public class OrgsController implements OrgsApi {
}
@Override
public ResponseEntity<InlineResponse2002> listOrgMembers(
String xRequestID,
String orgId) {
public ResponseEntity<InlineResponse2001> listOrgMembers(String xRequestID, String orgId) {
return organizationService.listMembers(orgId);
}
@ -65,20 +63,13 @@ public class OrgsController implements OrgsApi {
}
@Override
public ResponseEntity<InlineResponse2003> listInvitations(
String xRequestID,
String orgId,
InvitationStatusName status) {
public ResponseEntity<InlineResponse2002> listInvitations(String xRequestID, String orgId, InvitationStatusName status) {
return invitationService.list(orgId, status);
}
@Override
public ResponseEntity<Void> revokeInvitation(
String xRequestID,
String orgId,
String invitationId,
InlineObject request) {
return invitationService.revoke(invitationId, request);
public ResponseEntity<Void> revokeInvitation(String xRequestID, String orgId, String invitationId, InlineObject inlineObject) {
return invitationService.revoke(orgId, invitationId, inlineObject);
}
@Override
@ -90,9 +81,7 @@ public class OrgsController implements OrgsApi {
}
@Override
public ResponseEntity<InlineResponse2001> listOrgRoles(
String xRequestID,
String orgId) {
public ResponseEntity<InlineResponse200> listOrgRoles(String xRequestID, String orgId) {
return organizationRoleService.list(orgId);
}

View File

@ -1,38 +1,64 @@
package com.rbkmoney.orgmanager.controller;
import com.rbkmoney.orgmanager.entity.OrganizationEntityPageable;
import com.rbkmoney.orgmanager.service.KeycloakService;
import com.rbkmoney.orgmanager.service.OrganizationService;
import com.rbkmoney.swag.organizations.api.UserApi;
import com.rbkmoney.swag.organizations.model.InlineResponse200;
import com.rbkmoney.swag.organizations.model.OrganizationJoinRequest;
import com.rbkmoney.swag.organizations.model.OrganizationMembership;
import com.rbkmoney.swag.organizations.model.OrganizationSearchResult;
import lombok.RequiredArgsConstructor;
import org.keycloak.representations.AccessToken;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class UserController implements UserApi {
private final OrganizationService organizationService;
private final KeycloakService keycloakService;
@Override
public ResponseEntity<Void> cancelOrgMembership(
String xRequestID,
String orgId) {
throw new UnsupportedOperationException(); // TODO [a.romanov]: impl
AccessToken accessToken = keycloakService.getAccessToken();
return organizationService.cancelOrgMembership(orgId, accessToken.getSubject(), accessToken.getEmail());
}
@Override
public ResponseEntity<OrganizationMembership> inquireOrgMembership(
String xRequestID,
String orgId) {
throw new UnsupportedOperationException(); // TODO [a.romanov]: impl
AccessToken accessToken = keycloakService.getAccessToken();
return organizationService.getMembership(orgId, accessToken.getSubject(), accessToken.getEmail());
}
@Override
public ResponseEntity<OrganizationMembership> joinOrg(
String xRequestID,
OrganizationJoinRequest body) {
throw new UnsupportedOperationException(); // TODO [a.romanov]: impl
AccessToken accessToken = keycloakService.getAccessToken();
return organizationService.joinOrganization(body.getInvitation(), accessToken.getSubject(), accessToken.getEmail());
}
@Override
public ResponseEntity<InlineResponse200> listOrgMembership(String xRequestID) {
throw new UnsupportedOperationException(); // TODO [a.romanov]: impl
public ResponseEntity<OrganizationSearchResult> listOrgMembership(String xRequestID, Integer limit, String continuationToken) {
OrganizationEntityPageable organizationEntityPageable;
if (continuationToken == null) {
organizationEntityPageable = organizationService.findAllOrganizations(limit);
} else {
organizationEntityPageable = organizationService.findAllOrganizations(continuationToken, limit);
}
OrganizationSearchResult organizationSearchResult = new OrganizationSearchResult();
organizationSearchResult.setContinuationToken(organizationEntityPageable.getContinuationToken());
organizationSearchResult.setResults(organizationEntityPageable.getOrganizations());
return ResponseEntity.ok(organizationSearchResult);
}
}

View File

@ -6,6 +6,7 @@ import com.rbkmoney.swag.organizations.model.Invitation;
import com.rbkmoney.swag.organizations.model.Invitee;
import com.rbkmoney.swag.organizations.model.InviteeContact;
import lombok.RequiredArgsConstructor;
import org.openapitools.jackson.nullable.JsonNullable;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@ -48,7 +49,7 @@ public class InvitationConverter {
.map(role -> memberRoleConverter.toEntity(role, orgId))
.collect(toSet()))
.metadata(jsonMapper.toJson(invitation.getMetadata()))
.status(invitation.getStatus())
.status(invitation.getStatus().get().toString())
.acceptToken(UUID.randomUUID().toString()) // TODO [a.romanov]: token
.build();
}
@ -68,7 +69,7 @@ public class InvitationConverter {
.stream()
.map(memberRoleConverter::toDomain)
.collect(toSet())));
invitation.setStatus(entity.getStatus());
invitation.setStatus(JsonNullable.of(entity.getStatus()));
return invitation;
}

View File

@ -17,8 +17,8 @@ public class MemberRoleConverter {
.id(UUID.randomUUID().toString())
.organizationId(orgId)
.resourceId(role.getScope().getResourceId())
.roleId(role.getRoleId().getValue())
.scopeId(role.getScope().getId().getValue())
.roleId(role.getRoleId().toString())
.scopeId(role.getScope().getId().toString())
.build();
}

View File

@ -22,7 +22,7 @@ public class OrganizationConverter {
.id(UUID.randomUUID().toString())
.createdAt(LocalDateTime.now())
.name(organization.getName())
.owner(organization.getOwner())
.owner(organization.getOwner().toString())
.metadata(jsonMapper.toJson(organization.getMetadata()))
.build();
}
@ -33,6 +33,6 @@ public class OrganizationConverter {
.createdAt(OffsetDateTime.of(entity.getCreatedAt(), ZoneOffset.UTC))
.name(entity.getName())
.owner(entity.getOwner())
.metadata(jsonMapper.toMap(entity.getMetadata()));
.metadata(entity.getMetadata() != null ? jsonMapper.toMap(entity.getMetadata()) : null);
}
}

View File

@ -19,7 +19,7 @@ public class MemberEntity implements Serializable {
@ToString.Exclude
@EqualsAndHashCode.Exclude
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(
name = "member_to_member_role",
joinColumns = @JoinColumn(name = "member_id"),

View File

@ -1,6 +1,8 @@
package com.rbkmoney.orgmanager.entity;
import lombok.*;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import javax.persistence.*;
import java.io.Serializable;
@ -20,7 +22,7 @@ public class OrganizationEntity implements Serializable {
@ToString.Exclude
@EqualsAndHashCode.Exclude
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(
name = "member_to_organization",
joinColumns = @JoinColumn(name = "organization_id"),
@ -29,7 +31,8 @@ public class OrganizationEntity implements Serializable {
@ToString.Exclude
@EqualsAndHashCode.Exclude
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@OneToMany(cascade = CascadeType.ALL)
@Fetch(FetchMode.SUBSELECT)
@JoinColumn(name = "organizationId")
private Set<OrganizationRoleEntity> roles;

View File

@ -0,0 +1,13 @@
package com.rbkmoney.orgmanager.entity;
import com.rbkmoney.swag.organizations.model.Organization;
import lombok.Data;
import java.util.List;
@Data
public class OrganizationEntityPageable {
private final String continuationToken;
private final int limit;
private final List<Organization> organizations;
}

View File

@ -0,0 +1,80 @@
package com.rbkmoney.orgmanager.pagination;
import lombok.EqualsAndHashCode;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import java.io.Serializable;
@EqualsAndHashCode
public class OffsetBasedPageRequest implements Pageable, Serializable {
private static final long serialVersionUID = -35826477329623545L;
private int limit;
private long offset;
private final Sort sort;
public OffsetBasedPageRequest(long offset, int limit, Sort sort) {
if (offset < 0) {
throw new IllegalArgumentException("Offset index must not be less than zero!");
}
if (limit < 1) {
throw new IllegalArgumentException("Limit must not be less than one!");
}
this.limit = limit;
this.offset = offset;
this.sort = sort;
}
public OffsetBasedPageRequest(long offset, int limit) {
this(offset, limit, Sort.unsorted());
}
@Override
public int getPageNumber() {
return Math.toIntExact(offset / limit);
}
@Override
public int getPageSize() {
return limit;
}
@Override
public long getOffset() {
return offset;
}
@Override
public Sort getSort() {
return sort;
}
@Override
public Pageable next() {
return new OffsetBasedPageRequest(getOffset() + getPageSize(), getPageSize(), getSort());
}
public OffsetBasedPageRequest previous() {
return hasPrevious() ? new OffsetBasedPageRequest(getOffset() - getPageSize(), getPageSize(), getSort()) : this;
}
@Override
public Pageable previousOrFirst() {
return hasPrevious() ? previous() : first();
}
@Override
public Pageable first() {
return new OffsetBasedPageRequest(0, getPageSize(), getSort());
}
@Override
public boolean hasPrevious() {
return offset > limit;
}
}

View File

@ -5,9 +5,12 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface InvitationRepository extends JpaRepository<InvitationEntity, String> {
List<InvitationEntity> findByOrganizationIdAndStatus(String organizationId, String status);
Optional<InvitationEntity> findByAcceptToken(String token);
}

View File

@ -2,8 +2,16 @@ package com.rbkmoney.orgmanager.repository;
import com.rbkmoney.orgmanager.entity.OrganizationEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface OrganizationRepository extends JpaRepository<OrganizationEntity, String> {
@Query(value = "SELECT * FROM org_manager.organization AS o WHERE o.id < ?1 ORDER BY o.id DESC LIMIT ?2",
nativeQuery = true)
List<OrganizationEntity> fetchAll(String continuationId, int limit);
}

View File

@ -5,7 +5,7 @@ import com.rbkmoney.orgmanager.entity.InvitationEntity;
import com.rbkmoney.orgmanager.repository.InvitationRepository;
import com.rbkmoney.orgmanager.repository.OrganizationRepository;
import com.rbkmoney.swag.organizations.model.InlineObject;
import com.rbkmoney.swag.organizations.model.InlineResponse2003;
import com.rbkmoney.swag.organizations.model.InlineResponse2002;
import com.rbkmoney.swag.organizations.model.Invitation;
import com.rbkmoney.swag.organizations.model.InvitationStatusName;
import lombok.RequiredArgsConstructor;
@ -55,7 +55,7 @@ public class InvitationService {
.body(invitation);
}
public ResponseEntity<InlineResponse2003> list(String orgId, InvitationStatusName status) {
public ResponseEntity<InlineResponse2002> list(String orgId, InvitationStatusName status) {
boolean isOrganizationExist = organizationRepository.existsById(orgId);
if (!isOrganizationExist) {
@ -71,11 +71,11 @@ public class InvitationService {
return ResponseEntity
.status(HttpStatus.OK)
.body(new InlineResponse2003()
.body(new InlineResponse2002()
.results(invitations));
}
public ResponseEntity<Void> revoke(String invitationId, InlineObject request) {
public ResponseEntity<Void> revoke(String orgId, String invitationId, InlineObject inlineObject) {
Optional<InvitationEntity> entity = invitationRepository.findById(invitationId);
if (entity.isEmpty()) {
@ -85,8 +85,8 @@ public class InvitationService {
}
InvitationEntity updatedEntity = entity.get();
updatedEntity.setStatus(request.getStatus().getValue());
updatedEntity.setRevocationReason(request.getReason());
updatedEntity.setStatus(inlineObject.getStatus().getValue());
updatedEntity.setRevocationReason(inlineObject.getReason());
invitationRepository.save(updatedEntity);

View File

@ -0,0 +1,19 @@
package com.rbkmoney.orgmanager.service;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.representations.AccessToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Service
public class KeycloakService {
public AccessToken getAccessToken() {
KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
return keycloakPrincipal.getKeycloakSecurityContext().getToken();
}
}

View File

@ -5,7 +5,7 @@ import com.rbkmoney.orgmanager.entity.OrganizationEntity;
import com.rbkmoney.orgmanager.entity.OrganizationRoleEntity;
import com.rbkmoney.orgmanager.repository.OrganizationRepository;
import com.rbkmoney.orgmanager.repository.OrganizationRoleRepository;
import com.rbkmoney.swag.organizations.model.InlineResponse2001;
import com.rbkmoney.swag.organizations.model.InlineResponse200;
import com.rbkmoney.swag.organizations.model.Role;
import com.rbkmoney.swag.organizations.model.RoleId;
import lombok.RequiredArgsConstructor;
@ -35,7 +35,7 @@ public class OrganizationRoleService {
.build();
}
Optional<OrganizationRoleEntity> entity = organizationRoleRepository.findByOrganizationIdAndRoleId(orgId, roleId.getValue());
Optional<OrganizationRoleEntity> entity = organizationRoleRepository.findByOrganizationIdAndRoleId(orgId, roleId.toString());
Role role = organizationRoleConverter.toDomain(entity.get());
return ResponseEntity
@ -43,7 +43,7 @@ public class OrganizationRoleService {
.body(role);
}
public ResponseEntity<InlineResponse2001> list(String orgId) {
public ResponseEntity<InlineResponse200> list(String orgId) {
Optional<OrganizationEntity> entity = organizationRepository.findById(orgId);
if (entity.isEmpty()) {
@ -59,7 +59,7 @@ public class OrganizationRoleService {
return ResponseEntity
.status(HttpStatus.OK)
.body(new InlineResponse2001()
.body(new InlineResponse200()
.results(roles));
}
}

View File

@ -2,18 +2,28 @@ package com.rbkmoney.orgmanager.service;
import com.rbkmoney.orgmanager.converter.MemberConverter;
import com.rbkmoney.orgmanager.converter.OrganizationConverter;
import com.rbkmoney.orgmanager.entity.InvitationEntity;
import com.rbkmoney.orgmanager.entity.MemberEntity;
import com.rbkmoney.orgmanager.entity.OrganizationEntity;
import com.rbkmoney.orgmanager.entity.OrganizationEntityPageable;
import com.rbkmoney.orgmanager.repository.InvitationRepository;
import com.rbkmoney.orgmanager.repository.MemberRepository;
import com.rbkmoney.orgmanager.repository.OrganizationRepository;
import com.rbkmoney.swag.organizations.model.InlineResponse2002;
import com.rbkmoney.swag.organizations.model.InlineResponse2001;
import com.rbkmoney.swag.organizations.model.Member;
import com.rbkmoney.swag.organizations.model.Organization;
import com.rbkmoney.swag.organizations.model.OrganizationMembership;
import lombok.RequiredArgsConstructor;
import org.hibernate.Hibernate;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@ -23,10 +33,26 @@ import static java.util.stream.Collectors.toList;
@RequiredArgsConstructor
public class OrganizationService {
public static final Integer DEFAULT_ORG_LIMIT = 20;
private final OrganizationConverter organizationConverter;
private final OrganizationRepository organizationRepository;
private final MemberConverter memberConverter;
private final MemberRepository memberRepository;
private final InvitationRepository invitationRepository;
@Transactional(readOnly = true)
public Optional<OrganizationEntity> findById(String orgId) {
Optional<OrganizationEntity> organizationEntityOptional = organizationRepository.findById(orgId);
if (organizationEntityOptional.isPresent()) {
OrganizationEntity organizationEntity = organizationEntityOptional.get();
Hibernate.initialize(organizationEntity.getMembers());
Hibernate.initialize(organizationEntity.getRoles());
return Optional.of(organizationEntity);
}
return Optional.empty();
}
// TODO [a.romanov]: idempotency
public ResponseEntity<Organization> create(
@ -57,6 +83,7 @@ public class OrganizationService {
}
@Transactional
public ResponseEntity<Member> getMember(String userId) {
Optional<MemberEntity> entity = memberRepository.findById(userId);
@ -72,7 +99,8 @@ public class OrganizationService {
.body(member);
}
public ResponseEntity<InlineResponse2002> listMembers(String orgId) {
@Transactional
public ResponseEntity<InlineResponse2001> listMembers(String orgId) {
Optional<OrganizationEntity> entity = organizationRepository.findById(orgId);
if (entity.isEmpty()) {
@ -88,8 +116,115 @@ public class OrganizationService {
return ResponseEntity
.status(HttpStatus.OK)
.body(new InlineResponse2002()
.body(new InlineResponse2001()
.results(members));
}
public OrganizationEntityPageable findAllOrganizations(int limit) {
if (limit == 0) {
limit = DEFAULT_ORG_LIMIT;
}
Page<OrganizationEntity> organizationEntitiesPage = organizationRepository.findAll(PageRequest.of(0, limit, Sort.by("id").descending()));
List<OrganizationEntity> organizationEntities = organizationEntitiesPage.getContent();
String continuationToken = null;
if (organizationEntitiesPage.hasNext()) {
continuationToken = organizationEntities.get(organizationEntities.size() - 1).getId();
}
List<Organization> organizations = organizationEntities
.stream().map(organizationConverter::toDomain)
.collect(toList());
return new OrganizationEntityPageable(
continuationToken,
limit,
organizations);
}
public OrganizationEntityPageable findAllOrganizations(String continuationId, int limit) {
if (limit == 0) {
limit = DEFAULT_ORG_LIMIT;
}
List<OrganizationEntity> organizationEntities = organizationEntities = organizationRepository.fetchAll(continuationId, limit);
String continuationToken = null;
if (organizationEntities.size() > 1 && organizationEntities.size() > limit) {
continuationToken = organizationEntities.get(organizationEntities.size() - 1).getId();
}
List<Organization> organizations = organizationEntities
.stream().map(organizationConverter::toDomain)
.collect(toList());
return new OrganizationEntityPageable(
continuationToken,
limit,
organizations);
}
@Transactional
public ResponseEntity<Void> cancelOrgMembership(String orgId, String userId, String userEmail) {
Optional<OrganizationEntity> organizationEntityOptional = organizationRepository.findById(orgId);
if (organizationEntityOptional.isEmpty()) return ResponseEntity.notFound().build();
Optional<MemberEntity> memberEntityOptional = memberRepository.findById(userId);
if (memberEntityOptional.isEmpty()) return ResponseEntity.notFound().build();
organizationEntityOptional.get().getMembers()
.removeIf(memberEntity -> memberEntity.getId().equals(memberEntityOptional.get().getId()));
return ResponseEntity.ok().build();
}
@Transactional
public ResponseEntity<OrganizationMembership> getMembership(String orgId, String userId, String userEmail) {
Optional<OrganizationEntity> organizationEntityOptional = organizationRepository.findById(orgId);
if (organizationEntityOptional.isEmpty()) return ResponseEntity.notFound().build();
Optional<MemberEntity> memberEntityOptional = memberRepository.findById(userId);
if (memberEntityOptional.isEmpty()) return ResponseEntity.notFound().build();
OrganizationMembership organizationMembership = new OrganizationMembership();
organizationMembership.setMember(memberConverter.toDomain(memberEntityOptional.get()));
organizationMembership.setOrg(organizationConverter.toDomain(organizationEntityOptional.get()));
return ResponseEntity.ok(organizationMembership);
}
@Transactional
public ResponseEntity<OrganizationMembership> joinOrganization(String token, String userId, String userEmail) {
Optional<InvitationEntity> invitationEntityOptional = invitationRepository.findByAcceptToken(token);
if (invitationEntityOptional.isEmpty()) return ResponseEntity.notFound().build();
InvitationEntity invitationEntity = invitationEntityOptional.get();
Optional<OrganizationEntity> organizationEntityOptional = organizationRepository.findById(invitationEntity.getOrganizationId());
if (organizationEntityOptional.isEmpty()) return ResponseEntity.notFound().build();
OrganizationEntity organizationEntity = organizationEntityOptional.get();
MemberEntity memberEntity = findOrCreateMember(userId, userEmail);
organizationEntity.getMembers().add(memberEntity);
OrganizationMembership organizationMembership = new OrganizationMembership();
organizationMembership.setMember(memberConverter.toDomain(memberEntity));
organizationMembership.setOrg(organizationConverter.toDomain(organizationEntity));
return ResponseEntity.ok(organizationMembership);
}
private MemberEntity findOrCreateMember(String userId, String userEmail) {
Optional<MemberEntity> memberEntityOptional = memberRepository.findById(userId);
if (memberEntityOptional.isEmpty()) {
return memberRepository.save(new MemberEntity(userId, Collections.emptySet(), userEmail));
}
return memberEntityOptional.get();
}
}

View File

@ -41,3 +41,14 @@ hibernate:
info:
version: '@project.version@'
stage: dev
keycloak:
realm: internal
auth-server-url: http://keycloak:8080/auth
resource: common-api
not-before: 0
ssl-required: none
realm-public-key-file-path:
realm-public-key:
auth.enabled: true

View File

@ -0,0 +1,58 @@
package com.rbkmoney.orgmanager.controller;
import org.junit.ClassRule;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;
import org.testcontainers.containers.PostgreSQLContainer;
import java.io.IOException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.time.Duration;
import java.util.Base64;
import java.util.Properties;
@Import(KeycloakTestConfig.class)
public abstract class AbstractControllerTest {
@ClassRule
@SuppressWarnings("rawtypes")
public static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:9.6")
.withStartupTimeout(Duration.ofMinutes(5));
public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=" + postgres.getJdbcUrl(),
"spring.datasource.username=" + postgres.getUsername(),
"spring.datasource.password=" + postgres.getPassword(),
"spring.flyway.url=" + postgres.getJdbcUrl(),
"spring.flyway.user=" + postgres.getUsername(),
"spring.flyway.password=" + postgres.getPassword())
.and(configurableApplicationContext.getEnvironment().getActiveProfiles())
.applyTo(configurableApplicationContext);
}
}
@Autowired
private KeycloakOpenIdStub keycloakOpenIdStub;
protected String generateJwt(long iat, long exp, String... roles) {
return keycloakOpenIdStub.generateJwt(iat, exp, roles);
}
protected String generateRBKadminJwt() {
return keycloakOpenIdStub.generateJwt("RBKadmin");
}
}

View File

@ -0,0 +1,85 @@
package com.rbkmoney.orgmanager.controller;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.PrivateKey;
import java.time.Instant;
import java.util.UUID;
public class JwtTokenBuilder {
public final static String DEFAULT_USERNAME = "Darth Vader";
public final static String DEFAULT_EMAIL = "darkside-the-best@mail.com";
private final String userId;
private final String username;
private final String email;
private final PrivateKey privateKey;
public JwtTokenBuilder(PrivateKey privateKey) {
this(UUID.randomUUID().toString(), DEFAULT_USERNAME, DEFAULT_EMAIL, privateKey);
}
public JwtTokenBuilder(String userId, String username, String email, PrivateKey privateKey) {
this.userId = userId;
this.username = username;
this.email = email;
this.privateKey = privateKey;
}
public String getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public String generateJwtWithRoles(String issuer, String... roles) {
long iat = Instant.now().getEpochSecond();
long exp = iat + 60 * 10;
return generateJwtWithRoles(iat, exp, issuer, roles);
}
public String generateJwtWithRoles(long iat, long exp, String issuer, String... roles) {
String payload;
try {
payload = new JSONObject()
.put("jti", UUID.randomUUID().toString())
.put("exp", exp)
.put("nbf", "0")
.put("iat", iat)
.put("iss", issuer)
.put("aud", "private-api")
.put("sub", userId)
.put("typ", "Bearer")
.put("azp", "private-api")
.put("resource_access", new JSONObject()
.put("common-api", new JSONObject()
.put("roles", new JSONArray(roles))))
.put("preferred_username", username)
.put("email", email).toString();
} catch (JSONException e) {
throw new RuntimeException(e);
}
String jwt = Jwts.builder()
.setPayload(payload)
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
return jwt;
}
}

View File

@ -0,0 +1,46 @@
package com.rbkmoney.orgmanager.controller;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
public class KeycloakOpenIdStub {
private final String keycloakRealm;
private final String issuer;
private final String openidConfig;
private final JwtTokenBuilder jwtTokenBuilder;
public KeycloakOpenIdStub(String keycloakAuthServerUrl, String keycloakRealm, JwtTokenBuilder jwtTokenBuilder) {
this.keycloakRealm = keycloakRealm;
this.jwtTokenBuilder = jwtTokenBuilder;
this.issuer = keycloakAuthServerUrl + "/realms/" + keycloakRealm;
this.openidConfig = "{\n" +
" \"issuer\": \"" + issuer + "\",\n" +
" \"authorization_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/auth\",\n" +
" \"token_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/token\",\n" +
" \"token_introspection_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/token/introspect\",\n" +
" \"userinfo_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/userinfo\",\n" +
" \"end_session_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/logout\",\n" +
" \"jwks_uri\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/certs\",\n" +
" \"check_session_iframe\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/login-status-iframe.html\",\n" +
" \"registration_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + "/clients-registrations/openid-connect\",\n" +
" \"introspection_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/token/introspect\"\n" +
"}";
}
public void givenStub() {
stubFor(get(urlEqualTo(String.format("/auth/realms/%s/.well-known/openid-configuration", keycloakRealm)))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody(openidConfig)
)
);
}
public String generateJwt(String... roles) {
return jwtTokenBuilder.generateJwtWithRoles(issuer, roles);
}
public String generateJwt(long iat, long exp, String... roles) {
return jwtTokenBuilder.generateJwtWithRoles(iat, exp, issuer, roles);
}
}

View File

@ -0,0 +1,52 @@
package com.rbkmoney.orgmanager.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Properties;
@TestConfiguration
public class KeycloakTestConfig {
@Bean
public KeycloakOpenIdStub keycloakOpenIdStub(@Value("${keycloak.auth-server-url}") String keycloakAuthServerUrl,
@Value("${keycloak.realm}") String keycloakRealm,
JwtTokenBuilder jwtTokenBuilder) {
return new KeycloakOpenIdStub(keycloakAuthServerUrl, keycloakRealm, jwtTokenBuilder);
}
@Bean
public JwtTokenBuilder JwtTokenBuilder(KeyPair keyPair) {
return new JwtTokenBuilder(keyPair.getPrivate());
}
@Bean
public KeyPair keyPair() throws GeneralSecurityException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
return keyGen.generateKeyPair();
}
@Bean
public static PropertySourcesPlaceholderConfigurer properties(KeyPair keyPair) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
KeyFactory fact = KeyFactory.getInstance("RSA");
X509EncodedKeySpec spec = fact.getKeySpec(keyPair.getPublic(), X509EncodedKeySpec.class);
String publicKey = Base64.getEncoder().encodeToString(spec.getEncoded());
PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
Properties properties = new Properties();
properties.load(new ClassPathResource("application.yml").getInputStream());
properties.setProperty("keycloak.realm-public-key", publicKey);
pspc.setProperties(properties);
pspc.setLocalOverride(true);
return pspc;
}
}

View File

@ -0,0 +1,254 @@
package com.rbkmoney.orgmanager.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rbkmoney.orgmanager.OrgManagerApplication;
import com.rbkmoney.orgmanager.entity.InvitationEntity;
import com.rbkmoney.orgmanager.entity.MemberEntity;
import com.rbkmoney.orgmanager.entity.MemberRoleEntity;
import com.rbkmoney.orgmanager.entity.OrganizationEntity;
import com.rbkmoney.orgmanager.repository.InvitationRepository;
import com.rbkmoney.orgmanager.repository.InvitationRepositoryTest;
import com.rbkmoney.orgmanager.repository.OrganizationRepository;
import com.rbkmoney.orgmanager.service.OrganizationService;
import com.rbkmoney.swag.organizations.model.InlineResponse2001;
import com.rbkmoney.swag.organizations.model.OrganizationJoinRequest;
import com.rbkmoney.swag.organizations.model.OrganizationMembership;
import com.rbkmoney.swag.organizations.model.OrganizationSearchResult;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DirtiesContext
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = {OrgManagerApplication.class, UserController.class})
@RunWith(SpringRunner.class)
@ContextConfiguration(initializers = InvitationRepositoryTest.Initializer.class)
@AutoConfigureMockMvc
@AutoConfigureWireMock(port = 0)
@TestPropertySource(locations = "classpath:wiremock.properties")
public class UserControllerTest extends AbstractControllerTest {
public static String ORGANIZATION_ID = "3Kf21K54ldE3";
public static String INVITATION_ID = "DL3Mc9dEqAlP";
public static String MEMBER_ID = "L6Mc2la1D9Rg";
public static String ACCEPT_TOKEN = "testToken";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private InvitationRepository invitationRepository;
@Autowired
private OrganizationRepository organizationRepository;
@Autowired
private KeycloakOpenIdStub keycloakOpenIdStub;
@SpyBean
private OrganizationService organizationService;
@Before
public void setUp() throws Exception {
keycloakOpenIdStub.givenStub();
OrganizationEntity organizationEntity = buildOrganization();
organizationRepository.save(organizationEntity);
InvitationEntity invitationEntity = buildInvitation();
invitationRepository.save(invitationEntity);
}
@Test
public void joinOrgTest() throws Exception {
OrganizationJoinRequest organizationJoinRequest = new OrganizationJoinRequest();
organizationJoinRequest.setInvitation(ACCEPT_TOKEN);
MvcResult mvcResult = mockMvc.perform(post("/user/membership")
.contentType("application/json")
.content(objectMapper.writeValueAsString(organizationJoinRequest))
.header("Authorization", "Bearer " + generateRBKadminJwt())
.header("X-Request-ID", "testRequestId")
).andExpect(status().isOk()).andReturn();
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
verify(organizationService, atMostOnce()).joinOrganization(anyString(), argumentCaptor.capture(), anyString());
String userId = argumentCaptor.getValue();
OrganizationMembership organizationMembership = objectMapper.readValue(
mvcResult.getResponse().getContentAsString(), OrganizationMembership.class);
Assert.assertEquals(ORGANIZATION_ID, organizationMembership.getOrg().getId());
Assert.assertEquals(userId, organizationMembership.getMember().getId());
}
@Test
public void cancelOrgMembershipTest() throws Exception {
String jwtToken = generateRBKadminJwt();
OrganizationJoinRequest organizationJoinRequest = new OrganizationJoinRequest();
organizationJoinRequest.setInvitation(ACCEPT_TOKEN);
mockMvc.perform(post("/user/membership")
.contentType("application/json")
.content(objectMapper.writeValueAsString(organizationJoinRequest))
.header("Authorization", "Bearer " + jwtToken)
.header("X-Request-ID", "testRequestId")
).andExpect(status().isOk()).andReturn();
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
verify(organizationService, atMostOnce()).joinOrganization(anyString(), argumentCaptor.capture(), anyString());
String userId = argumentCaptor.getValue();
MvcResult mvcResult = mockMvc.perform(delete("/user/membership/{orgId}", ORGANIZATION_ID)
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + jwtToken)
.header("X-Request-ID", "testRequestId")
).andExpect(status().isOk()).andReturn();
ResponseEntity<InlineResponse2001> response = organizationService.listMembers(ORGANIZATION_ID);
final boolean isMemberFounded = response.getBody().getResults()
.stream().anyMatch(member -> member.getId().equals(userId));
Assert.assertFalse(isMemberFounded);
}
@Test
public void inquireOrgMembershipTest() throws Exception {
String jwtToken = generateRBKadminJwt();
// Join organization
OrganizationJoinRequest organizationJoinRequest = new OrganizationJoinRequest();
organizationJoinRequest.setInvitation(ACCEPT_TOKEN);
MvcResult mvcResult = mockMvc.perform(post("/user/membership")
.contentType("application/json")
.content(objectMapper.writeValueAsString(organizationJoinRequest))
.header("Authorization", "Bearer " + generateRBKadminJwt())
.header("X-Request-ID", "testRequestId")
).andExpect(status().isOk()).andReturn();
// get membership
mockMvc.perform(get("/user/membership/{orgId}", ORGANIZATION_ID)
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + jwtToken)
.header("X-Request-ID", "testRequestId")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.org").exists())
.andExpect(jsonPath("$.member").exists());
}
@Test
public void listOrgMembershipTest() throws Exception {
String jwtToken = generateRBKadminJwt();
for (int i = 0; i < 8; i++) {
OrganizationEntity organizationEntity = buildOrganization();
organizationEntity.setId(UUID.randomUUID().toString());
Set<MemberEntity> memberEntities = organizationEntity.getMembers().stream()
.peek(memberEntity -> memberEntity.setId(UUID.randomUUID().toString()))
.collect(Collectors.toSet());
organizationEntity.setMembers(memberEntities);
organizationRepository.save(organizationEntity);
}
MvcResult mvcResultFirst = mockMvc.perform(get("/user/membership")
.queryParam("limit", "5")
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + jwtToken)
.header("X-Request-ID", "testRequestId")
).andExpect(status().isOk()).andReturn();
OrganizationSearchResult organizationSearchResultFirst = objectMapper.readValue(
mvcResultFirst.getResponse().getContentAsString(), OrganizationSearchResult.class);
Assert.assertEquals(5, organizationSearchResultFirst.getResults().size());
MvcResult mvcResultSecond = mockMvc.perform(get("/user/membership")
.queryParam("limit", "5")
.queryParam("continuationToken", organizationSearchResultFirst.getContinuationToken())
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + jwtToken)
.header("X-Request-ID", "testRequestId")
).andExpect(status().isOk()).andReturn();
OrganizationSearchResult organizationSearchResultSecond = objectMapper.readValue(
mvcResultSecond.getResponse().getContentAsString(), OrganizationSearchResult.class);
Assert.assertEquals(4, organizationSearchResultSecond.getResults().size());
Assert.assertNull(organizationSearchResultSecond.getContinuationToken());
}
private InvitationEntity buildInvitation() {
return InvitationEntity.builder()
.id(INVITATION_ID)
.acceptToken(ACCEPT_TOKEN)
.createdAt(LocalDateTime.now())
.expiresAt(LocalDateTime.now())
.inviteeContactEmail("contactEmail")
.inviteeContactType("contactType")
.metadata("metadata")
.organizationId(ORGANIZATION_ID)
.status("Pending")
.inviteeRoles(Set.of(
MemberRoleEntity.builder()
.id("role1")
.roleId("role1")
.resourceId("resource1")
.scopeId("scope1")
.organizationId(ORGANIZATION_ID)
.build(),
MemberRoleEntity.builder()
.id("role2")
.roleId("role2")
.resourceId("resource2")
.scopeId("scope2")
.organizationId(ORGANIZATION_ID)
.build()))
.build();
}
private OrganizationEntity buildOrganization() {
MemberEntity member = MemberEntity.builder()
.id(MEMBER_ID)
.email("email")
.build();
return OrganizationEntity.builder()
.id(ORGANIZATION_ID)
.createdAt(LocalDateTime.now())
.name("name")
.owner("owner")
.members(Set.of(member))
.build();
}
}

View File

@ -7,6 +7,7 @@ import com.rbkmoney.orgmanager.util.JsonMapper;
import com.rbkmoney.swag.organizations.model.*;
import org.junit.Before;
import org.junit.Test;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
@ -48,7 +49,7 @@ public class InvitationConverterTest {
.roles(Set.of(new MemberRole())))
.expiresAt(OffsetDateTime.parse("2019-08-24T14:15:22Z"))
.metadata(Map.of("a", "b"));
invitation.setStatus("Pending");
invitation.setStatus(JsonNullable.of("Pending"));
// When
InvitationEntity entity = converter.toEntity(invitation, "org");
@ -101,8 +102,8 @@ public class InvitationConverterTest {
.roles(Set.of(new MemberRole())))
.acceptToken("token")
.metadata(Map.of("a", "b"));
expected.setStatus("Pending");
expected.setStatus(JsonNullable.of("Pending"));
assertThat(invitation).isEqualToComparingFieldByField(expected);
}
}
}

View File

@ -18,6 +18,7 @@ public abstract class AbstractRepositoryTest {
public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
postgres.start();
TestPropertyValues.of(
"spring.datasource.url=" + postgres.getJdbcUrl(),
"spring.datasource.username=" + postgres.getUsername(),

View File

@ -5,10 +5,13 @@ import com.rbkmoney.orgmanager.entity.MemberEntity;
import com.rbkmoney.orgmanager.entity.OrganizationEntity;
import com.rbkmoney.orgmanager.entity.OrganizationRoleEntity;
import com.rbkmoney.orgmanager.entity.ScopeEntity;
import com.rbkmoney.orgmanager.service.OrganizationService;
import com.rbkmoney.swag.organizations.model.Organization;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@ -18,6 +21,7 @@ import java.util.Optional;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DirtiesContext
@ -31,6 +35,9 @@ public class OrganizationRepositoryTest extends AbstractRepositoryTest {
@Autowired
private OrganizationRepository organizationRepository;
@Autowired
private OrganizationService organizationService;
@Autowired
private MemberRepository memberRepository;
@ -47,7 +54,6 @@ public class OrganizationRepositoryTest extends AbstractRepositoryTest {
.id("memberId")
.email("email")
.build();
OrganizationEntity organization = OrganizationEntity.builder()
.id(ORGANIZATION_ID)
.createdAt(LocalDateTime.now())
@ -60,7 +66,7 @@ public class OrganizationRepositoryTest extends AbstractRepositoryTest {
organizationRepository.save(organization);
// Then
Optional<OrganizationEntity> savedOrganization = organizationRepository.findById(ORGANIZATION_ID);
Optional<OrganizationEntity> savedOrganization = organizationService.findById(ORGANIZATION_ID);
assertTrue(savedOrganization.isPresent());
assertThat(savedOrganization.get().getMembers()).hasSize(1);
@ -95,13 +101,13 @@ public class OrganizationRepositoryTest extends AbstractRepositoryTest {
organizationRepository.save(organization);
// Then
Optional<OrganizationEntity> savedOrganization = organizationRepository.findById(ORGANIZATION_ID);
assertTrue(savedOrganization.isPresent());
assertThat(savedOrganization.get().getRoles()).hasSize(1);
savedOrganization.get().getRoles().forEach(
Optional<OrganizationEntity> savedOrganizationOptional = organizationService.findById(ORGANIZATION_ID);
assertTrue(savedOrganizationOptional.isPresent());
assertThat(savedOrganizationOptional.get().getRoles()).hasSize(1);
savedOrganizationOptional.get().getRoles().forEach(
r -> assertThat(r.getPossibleScopes()).hasSize(1));
Optional<OrganizationRoleEntity> savedMember = organizationRoleRepository.findById("roleId");
assertTrue(savedMember.isPresent());
}
}
}

View File

@ -5,7 +5,7 @@ import com.rbkmoney.orgmanager.entity.InvitationEntity;
import com.rbkmoney.orgmanager.repository.InvitationRepository;
import com.rbkmoney.orgmanager.repository.OrganizationRepository;
import com.rbkmoney.swag.organizations.model.InlineObject;
import com.rbkmoney.swag.organizations.model.InlineResponse2003;
import com.rbkmoney.swag.organizations.model.InlineResponse2002;
import com.rbkmoney.swag.organizations.model.Invitation;
import com.rbkmoney.swag.organizations.model.InvitationStatusName;
import org.junit.Test;
@ -115,7 +115,7 @@ public class InvitationServiceTest {
.thenReturn(invitation);
// When
ResponseEntity<InlineResponse2003> response = service.list(orgId, InvitationStatusName.PENDING);
ResponseEntity<InlineResponse2002> response = service.list(orgId, InvitationStatusName.PENDING);
// Then
assertThat(response.getStatusCode())
@ -134,7 +134,7 @@ public class InvitationServiceTest {
.thenReturn(false);
// When
ResponseEntity<InlineResponse2003> response = service.list(orgId, InvitationStatusName.ACCEPTED);
ResponseEntity<InlineResponse2002> response = service.list(orgId, InvitationStatusName.ACCEPTED);
// Then
assertThat(response.getStatusCode())
@ -146,6 +146,7 @@ public class InvitationServiceTest {
@Test
public void shouldRevoke() {
// Given
String orgId = "orgId";
String invitationId = "invitationId";
InvitationEntity entity = new InvitationEntity();
@ -153,7 +154,7 @@ public class InvitationServiceTest {
.thenReturn(Optional.of(entity));
// When
ResponseEntity<Void> response = service.revoke(invitationId, new InlineObject()
ResponseEntity<Void> response = service.revoke(orgId, invitationId, new InlineObject()
.reason("reason")
.status(InlineObject.StatusEnum.REVOKED));
@ -171,16 +172,17 @@ public class InvitationServiceTest {
@Test
public void shouldReturnNotFoundIfInvitationDoesNotExist() {
// Given
String orgId = "orgId";
String invitationId = "invitationId";
when(invitationRepository.findById(invitationId))
.thenReturn(Optional.empty());
// When
ResponseEntity<Void> response = service.revoke(invitationId, new InlineObject());
ResponseEntity<Void> response = service.revoke(orgId, invitationId, new InlineObject());
// Then
assertThat(response.getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
}
}
}

View File

@ -5,6 +5,7 @@ import com.rbkmoney.orgmanager.entity.OrganizationEntity;
import com.rbkmoney.orgmanager.entity.OrganizationRoleEntity;
import com.rbkmoney.orgmanager.repository.OrganizationRepository;
import com.rbkmoney.orgmanager.repository.OrganizationRoleRepository;
import com.rbkmoney.swag.organizations.model.InlineResponse200;
import com.rbkmoney.swag.organizations.model.InlineResponse2001;
import com.rbkmoney.swag.organizations.model.Role;
import com.rbkmoney.swag.organizations.model.RoleId;
@ -49,7 +50,7 @@ public class OrganizationRoleServiceTest {
.thenReturn(role);
// When
ResponseEntity<InlineResponse2001> response = service.list(orgId);
ResponseEntity<InlineResponse200> response = service.list(orgId);
// Then
assertThat(response.getStatusCode())
@ -69,7 +70,7 @@ public class OrganizationRoleServiceTest {
.thenReturn(Optional.empty());
// When
ResponseEntity<InlineResponse2001> response = service.list(orgId);
ResponseEntity<InlineResponse200> response = service.list(orgId);
// Then
assertThat(response.getStatusCode())
@ -122,4 +123,4 @@ public class OrganizationRoleServiceTest {
assertThat(response.getBody())
.isNull();
}
}
}

View File

@ -6,6 +6,7 @@ import com.rbkmoney.orgmanager.entity.MemberEntity;
import com.rbkmoney.orgmanager.entity.OrganizationEntity;
import com.rbkmoney.orgmanager.repository.MemberRepository;
import com.rbkmoney.orgmanager.repository.OrganizationRepository;
import com.rbkmoney.swag.organizations.model.InlineResponse2001;
import com.rbkmoney.swag.organizations.model.InlineResponse2002;
import com.rbkmoney.swag.organizations.model.Member;
import com.rbkmoney.swag.organizations.model.Organization;
@ -118,7 +119,7 @@ public class OrganizationServiceTest {
.thenReturn(member);
// When
ResponseEntity<InlineResponse2002> response = service.listMembers(orgId);
ResponseEntity<InlineResponse2001> response = service.listMembers(orgId);
// Then
assertThat(response.getStatusCode())
@ -138,7 +139,7 @@ public class OrganizationServiceTest {
.thenReturn(Optional.empty());
// When
ResponseEntity<InlineResponse2002> response = service.listMembers(orgId);
ResponseEntity<InlineResponse2001> response = service.listMembers(orgId);
// Then
assertThat(response.getStatusCode())

View File

@ -0,0 +1,2 @@
wiremock.server.baseUrl=http://localhost:${wiremock.server.port}
keycloak.auth-server-url=${wiremock.server.baseUrl}/auth