Commit 4711f385 authored by Enriko Käsper's avatar Enriko Käsper
Browse files

Merge branch 'develop' into 'master'

Release

See merge request teis/auth-service!14
parents 1401b3fb b27545e7
......@@ -59,6 +59,7 @@ allprojects {
compile "ee.sm.ti.teis:common-api-gateway-lib:${commonApiGatewayVersion}"
compile "ee.sm.ti.teis:service-common-lib:${commonsVersion}"
compile "ee.sm.ti.teis:domain-request-lib:${commonsVersion}"
compile "ee.sm.ti.teis:scheduler-service-lib:${schedulerVersion}"
annotationProcessor(
"javax.annotation:javax.annotation-api:1.3.2",
)
......
theGroup=ee.sm.ti.teis
theVersion=0.2.0
commonsVersion=0.22.3-SNAPSHOT
commonApiGatewayVersion=0.23.0-SNAPSHOT
theVersion=0.3.0
commonsVersion=0.23.1
commonApiGatewayVersion=0.24.0
schedulerVersion=0.4.0
pluginVersion=0.0.19-SNAPSHOT
mapstructVersion=1.3.0.Final
......@@ -6,13 +6,16 @@ package ee.sm.ti.teis.authorization;
import ee.sm.ti.teis.aspects.RabbitListenerAspect;
import ee.sm.ti.teis.configuration.ExchangeConfig;
import ee.sm.ti.teis.configuration.QueueConfigurator;
import ee.sm.ti.teis.scheduler.job.executor.SystemJobExecutionListener;
import ee.sm.ti.teis.scheduler.job.executor.SystemJobsRegistry;
import ee.sm.ti.teis.servicecommon.config.PropertyLogger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
@SpringBootApplication
@Import({PropertyLogger.class, ExchangeConfig.class, QueueConfigurator.class, RabbitListenerAspect.class})
@Import({PropertyLogger.class, ExchangeConfig.class, QueueConfigurator.class, RabbitListenerAspect.class,
SystemJobExecutionListener.class, SystemJobsRegistry.class})
public class AuthServiceApp {
public static void main(String[] args) {
SpringApplication.run(AuthServiceApp.class, args);
......
package ee.sm.ti.teis.authorization.keycloak;
import ee.sm.ti.teis.exceptions.TeisResourceNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@Slf4j
public class KeycloakOfficialsService {
private static final int LIST_QUERY_BATCH_SIZE = 100;
private final Keycloak keycloakTemplate;
private final TiadKeycloakProperties tiadKeycloakProperties;
@Value("${teis.group-prefix}")
private String teisGroupPrefix;
public KeycloakOfficialsService(Keycloak keycloakTemplate, TiadKeycloakProperties tiadKeycloakProperties) {
this.keycloakTemplate = keycloakTemplate;
this.tiadKeycloakProperties = tiadKeycloakProperties;
......@@ -40,4 +52,46 @@ public class KeycloakOfficialsService {
.groups();
}
public Set<UserRepresentation> getKeycloakTeisGroupUsers() {
Set<UserRepresentation> keycloakUsers = new LinkedHashSet<>();
Set<GroupRepresentation> keycloakTeisGroups = new LinkedHashSet<>();
Map<String, Long> countGroups = keycloakTemplate
.realm(tiadKeycloakProperties.getRealm())
.groups()
.count();
int firstResult = 0;
while (firstResult < countGroups.get("count")) {
List<GroupRepresentation> batchGroups = keycloakTemplate
.realm(tiadKeycloakProperties.getRealm())
.groups().groups(firstResult, LIST_QUERY_BATCH_SIZE);
for (GroupRepresentation group : batchGroups) {
if (group.getName().startsWith(teisGroupPrefix)) {
keycloakTeisGroups.add(group);
List<GroupRepresentation> subGroups = group.getSubGroups();
keycloakTeisGroups.addAll(subGroups);
}
}
firstResult += LIST_QUERY_BATCH_SIZE;
}
for (GroupRepresentation group : keycloakTeisGroups) {
log.info("Start querying group members for {}.", group.getName());
int countMembers = keycloakTemplate
.realm(tiadKeycloakProperties.getRealm())
.groups().group(group.getId()).members().size();
log.info("Get group members ({}) for {}.", countMembers, group.getName());
firstResult = 0;
while (firstResult < countMembers) {
List<UserRepresentation> members = keycloakTemplate
.realm(tiadKeycloakProperties.getRealm())
.groups().group(group.getId()).members(firstResult, LIST_QUERY_BATCH_SIZE, true);
keycloakUsers.addAll(members.stream()
.filter(UserRepresentation::isEnabled)
.collect(Collectors.toSet()));
firstResult += LIST_QUERY_BATCH_SIZE;
}
}
return keycloakUsers;
}
}
package ee.sm.ti.teis.authorization.officials;
import ee.sm.ti.teis.ErrorDTO;
import ee.sm.ti.teis.domain.officials.OfficialUser;
import ee.sm.ti.teis.domainrequest.DomainQueryResponseDto;
import ee.sm.ti.teis.domainrequest.DomainResponseDTO;
import ee.sm.ti.teis.domainrequest.DomainUpdateDTO;
import ee.sm.ti.teis.domainrequest.rabbit.TeisRabbitClient;
import ee.sm.ti.teis.errors.CommonErrorCode;
import ee.sm.ti.teis.exceptions.TeisBusinessException;
import ee.sm.ti.teis.servicerequest.RequestMetaDTO;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;
import java.util.List;
import static ee.sm.ti.teis.types.enums.ObjectStatus.CURRENT;
@Service
public class OfficialUsersService extends TeisRabbitClient<OfficialUser> {
public OfficialUsersService(RabbitTemplate msRabbitTemplate) {
super(msRabbitTemplate);
}
List<OfficialUser> getCurrentOfficialUsers(RequestMetaDTO requestMetaDTO) {
OfficialUser item = new OfficialUser();
item.setObjectStatus(CURRENT);
return getItems(item, OfficialUsersQueryResponse.class, requestMetaDTO);
}
OfficialUser deleteOfficialUser(OfficialUser officialUser, RequestMetaDTO requestMetaDTO) {
return deleteItem(officialUser, OfficialUserDomainResponse.class, requestMetaDTO);
}
OfficialUser createOfficialUser(OfficialUser officialUser, RequestMetaDTO requestMetaDTO) {
return createItem(officialUser, OfficialUserDomainResponse.class, requestMetaDTO);
}
public OfficialUser updateOfficialUser(OfficialUser officialUser, RequestMetaDTO requestMetaDTO) {
UpdateOfficialUserDomainRequest requestDTO = new UpdateOfficialUserDomainRequest();
requestDTO.setPayload(officialUser, requestMetaDTO);
ParameterizedTypeReference<OfficialUserDomainResponse> responseType = ParameterizedTypeReference.forType(OfficialUserDomainResponse.class);
OfficialUserDomainResponse resultDTO = msRabbitTemplate.convertSendAndReceiveAsType(requestDTO.routingKey(), requestDTO, responseType);
if (resultDTO != null) {
return resultDTO.getPayload();
}
throw new TeisBusinessException(CommonErrorCode.SYSTEM_ERROR, "No response from officials service.");
}
public static class OfficialUsersQueryResponse extends DomainQueryResponseDto<List<OfficialUser>, ErrorDTO> {
}
public static class OfficialUserDomainResponse extends DomainResponseDTO<OfficialUser, ErrorDTO> {
}
public static class UpdateOfficialUserDomainRequest extends DomainUpdateDTO<OfficialUser, ErrorDTO> {
}
}
package ee.sm.ti.teis.authorization.officials;
import ee.sm.ti.teis.scheduler.domain.SystemJob;
import ee.sm.ti.teis.scheduler.job.executor.SystemJobExecutor;
import ee.sm.ti.teis.servicerequest.RequestMetaDTO;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
@Service
public class OfficialUsersSyncJob extends SystemJobExecutor {
private final OfficialUsersSyncService officialUsersSyncService;
public OfficialUsersSyncJob(RabbitTemplate msRabbitTemplate, OfficialUsersSyncService officialUsersSyncService) {
super(msRabbitTemplate);
this.officialUsersSyncService = officialUsersSyncService;
}
@Override
protected String getJobId() {
return "auth.syncOfficialUsersFromKeycloak";
}
@Override
public String executeInternal(SystemJob payload, RequestMetaDTO requestMetaDto) {
return officialUsersSyncService.syncOfficialUsers(requestMetaDto);
}
}
package ee.sm.ti.teis.authorization.officials;
import ee.sm.ti.teis.authorization.keycloak.KeycloakOfficialsService;
import ee.sm.ti.teis.domain.officials.OfficialUser;
import ee.sm.ti.teis.servicerequest.RequestMetaDTO;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import static ee.sm.ti.teis.servicecommon.util.ProfilesUtil.isTestProfile;
import static java.text.MessageFormat.format;
@Service
@Slf4j
public class OfficialUsersSyncService {
private final OfficialUsersService officialUsersService;
private final KeycloakOfficialsService keycloakService;
private boolean isTestProfile = true;
public OfficialUsersSyncService(OfficialUsersService officialUsersService, KeycloakOfficialsService keycloakService,
Environment env) {
this.officialUsersService = officialUsersService;
this.keycloakService = keycloakService;
if (env != null) {
this.isTestProfile = isTestProfile(env);
}
}
String syncOfficialUsers(RequestMetaDTO requestMetaDTO) {
int createdUsers = 0;
int updatedUsers = 0;
int deletedUsers = 0;
Set<UserRepresentation> keycloakUsers = keycloakService.getKeycloakTeisGroupUsers();
List<OfficialUser> currentOfficialUsers = officialUsersService.getCurrentOfficialUsers(requestMetaDTO);
log.info("Found {} AD users from Keycloak and {} CURRENT TeIS official users.",
keycloakUsers.size(), currentOfficialUsers.size());
if (keycloakUsers.isEmpty()) {
return "No users found from Keycloak. Skip sync.";
}
for (UserRepresentation keycloakUser : keycloakUsers) {
Optional<OfficialUser> currentOfficialUserOptional = currentOfficialUsers.stream()
.filter(officialUser -> officialUser.getIdCode().equals(keycloakUser.getUsername())).findFirst();
if (currentOfficialUserOptional.isPresent()) {
OfficialUser currentOfficialUser = currentOfficialUserOptional.get();
if (!keycloakUser.getFirstName().trim().equals(currentOfficialUser.getFirstName().trim()) ||
!keycloakUser.getLastName().trim().equals(currentOfficialUser.getLastName().trim())) {
// update existing user name
currentOfficialUser.setFirstName(currentOfficialUser.getFirstName().trim());
currentOfficialUser.setLastName(currentOfficialUser.getLastName().trim());
officialUsersService.updateOfficialUser(currentOfficialUser, requestMetaDTO);
updatedUsers++;
}
} else {
// add new users
officialUsersService.createOfficialUser(mapOfficialUser(keycloakUser), requestMetaDTO);
createdUsers++;
}
}
// remove users
for (OfficialUser officialUser : currentOfficialUsers) {
if (keycloakUsers.stream().noneMatch(keycloakUser ->
keycloakUser.getUsername().equals(officialUser.getIdCode()))
&& !isTestUser(officialUser)) {
officialUsersService.deleteOfficialUser(officialUser, requestMetaDTO);
deletedUsers++;
}
}
return format("Official service users were synchronized with Keycloak users. created={0}, updated={1}, deleted={2}",
createdUsers, updatedUsers, deletedUsers);
}
private OfficialUser mapOfficialUser(UserRepresentation keycloakUser) {
return OfficialUser.builder()
.idCode(keycloakUser.getUsername())
.firstName(keycloakUser.getFirstName())
.lastName(keycloakUser.getLastName())
.build();
}
boolean isTestUser(OfficialUser officialUser) {
// FIXME test should be test-, but this removes Test3 user from TEIS official users.
return officialUser.getLastName().toLowerCase().startsWith("test") && isTestProfile;
}
}
package ee.sm.ti.teis.authorization.rabbit;
import ee.sm.ti.teis.configuration.QueueList;
import ee.sm.ti.teis.configuration.TeisQueue;
import ee.sm.ti.teis.scheduler.message.ExecuteSystemJobStartRequest;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import static ee.sm.ti.teis.scheduler.job.executor.JobExecutorQueueBaseConfig.EXECUTE_SYSTEM_JOB_QUEUE_NAME;
@Configuration
public class JobExecutorQueueConfig implements QueueList {
public static final String MS_EXECUTE_SYSTEM_JOB_QUEUE = "auth-service" + EXECUTE_SYSTEM_JOB_QUEUE_NAME;
@Override
public void updateQueues(ArrayList<TeisQueue> queues) {
addMsQueue(queues, "msExecuteSystemJobsQueue", new ExecuteSystemJobStartRequest().routingKey(), MS_EXECUTE_SYSTEM_JOB_QUEUE);
}
}
package ee.sm.ti.teis.authorization.officials;
import ee.sm.ti.teis.authorization.keycloak.KeycloakOfficialsService;
import ee.sm.ti.teis.domain.officials.OfficialUser;
import ee.sm.ti.teis.servicerequest.RequestMetaDTO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.keycloak.representations.idm.UserRepresentation;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static ee.sm.ti.teis.servicerequest.UserType.OFFICIAL_USER;
import static ee.sm.ti.teis.utils.TestUtils.createRequestMeta;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.UUID.randomUUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class OfficialUsersSyncServiceTest {
@Mock
private OfficialUsersService officialUsersService;
@Mock
private KeycloakOfficialsService keycloakService;
@InjectMocks
private OfficialUsersSyncService officialUsersSyncService;
private RequestMetaDTO requestMetaDTO;
@BeforeEach
void setUp() {
requestMetaDTO = createRequestMeta(randomUUID().toString(), OFFICIAL_USER, randomUUID().toString(),
emptyList());
}
@Test
void syncShouldNotStart_when_noUsersFromKeycloak() {
when(keycloakService.getKeycloakTeisGroupUsers()).thenReturn(emptySet());
when(officialUsersService.getCurrentOfficialUsers(any())).thenReturn(emptyList());
String result = officialUsersSyncService.syncOfficialUsers(requestMetaDTO);
assertThat(result).contains("No users found ");
}
@Test
void sync_should_create_new_users() {
when(keycloakService.getKeycloakTeisGroupUsers()).thenReturn(getNewKeycloakUsers());
when(officialUsersService.getCurrentOfficialUsers(any())).thenReturn(emptyList());
String result = officialUsersSyncService.syncOfficialUsers(requestMetaDTO);
assertThat(result).contains("created=3");
verify(officialUsersService, times(3)).createOfficialUser(any(), any());
verify(officialUsersService, times(0)).updateOfficialUser(any(), any());
verify(officialUsersService, times(0)).deleteOfficialUser(any(), any());
}
@Test
void sync_should_update_existing_users_if_name_changed() {
when(keycloakService.getKeycloakTeisGroupUsers()).thenReturn(getNewKeycloakUsers());
when(officialUsersService.getCurrentOfficialUsers(any())).thenReturn(getOfficialUsers());
String result = officialUsersSyncService.syncOfficialUsers(requestMetaDTO);
assertThat(result).contains("created=1");
assertThat(result).contains("updated=1");
assertThat(result).contains("deleted=0");
verify(officialUsersService, times(1)).createOfficialUser(any(), any());
verify(officialUsersService, times(1)).updateOfficialUser(any(), any());
verify(officialUsersService, times(0)).deleteOfficialUser(any(), any());
}
@Test
void sync_should_delete_users_removed_from_keycloak() {
when(keycloakService.getKeycloakTeisGroupUsers()).thenReturn(getNewKeycloakUsers());
when(officialUsersService.getCurrentOfficialUsers(any())).thenReturn(getOfficialUsersToDelete());
String result = officialUsersSyncService.syncOfficialUsers(requestMetaDTO);
assertThat(result).contains("created=2");
assertThat(result).contains("updated=0");
assertThat(result).contains("deleted=1");
verify(officialUsersService, times(2)).createOfficialUser(any(), any());
verify(officialUsersService, times(0)).updateOfficialUser(any(), any());
verify(officialUsersService, times(1)).deleteOfficialUser(any(), any());
}
Set<UserRepresentation> getNewKeycloakUsers() {
Set<UserRepresentation> newUsers = new LinkedHashSet();
UserRepresentation user1 = new UserRepresentation();
user1.setFirstName("firstName1");
user1.setLastName("lastName1");
user1.setUsername("11111");
newUsers.add(user1);
UserRepresentation user2 = new UserRepresentation();
user2.setFirstName("firstName2");
user2.setLastName("lastName2");
user2.setUsername("22222");
newUsers.add(user2);
UserRepresentation user3 = new UserRepresentation();
user3.setFirstName("firstName3");
user3.setLastName("lastName3");
user3.setUsername("33333");
newUsers.add(user3);
return newUsers;
}
List<OfficialUser> getOfficialUsers() {
List<OfficialUser> newUsers = new ArrayList<>();
OfficialUser user1 = OfficialUser.builder().firstName("firstName1").lastName("lastName1").idCode("11111").build();
newUsers.add(user1);
OfficialUser user2 = OfficialUser.builder().firstName("firstName2-NEW").lastName("lastName2-NEW").idCode("22222").build();
newUsers.add(user2);
return newUsers;
}
List<OfficialUser> getOfficialUsersToDelete() {
List<OfficialUser> newUsers = new ArrayList<>();
OfficialUser user1 = OfficialUser.builder().firstName("firstName1").lastName("lastName1").idCode("11111").build();
newUsers.add(user1);
OfficialUser user2 = OfficialUser.builder().firstName("firstName5").lastName("lastName5").idCode("55555").build();
newUsers.add(user2);
return newUsers;
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment