Skip to content

Commit

Permalink
Merge pull request #28 from virtualidentityag/VIC-1918_Tenant_data_in…
Browse files Browse the repository at this point in the history
…_authorised_context

VIC-1918: Tenant data in authorised context
  • Loading branch information
Soarecos authored Nov 3, 2022
2 parents 59a75ac + 21439bc commit af2428e
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 9 deletions.
6 changes: 6 additions & 0 deletions api/tenantservice.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ paths:
required: true
schema:
type: string
- name: tenantId
in: query
description: Tenant Id
required: false
schema:
type: long
responses:
200:
description: Successful operation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.vi.tenantservice.api.exception.TenantAuthorisationException;
import com.vi.tenantservice.api.exception.TenantValidationException;
import javax.ws.rs.BadRequestException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
Expand All @@ -25,7 +26,7 @@ protected ResponseEntity<Object> handle(TenantAuthorisationException ex, WebRequ
return handleExceptionInternal(ex, "", ex.getCustomHttpHeaders(), HttpStatus.FORBIDDEN, request);
}

@ExceptionHandler(value = {IllegalStateException.class})
@ExceptionHandler(value = {IllegalStateException.class, BadRequestException.class})
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
protected void handleIllegalStateException() {
// status code is set with ResponseStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ public ResponseEntity<TenantDTO> updateTenant(Long id, @Valid TenantDTO tenantDT
}

@Override
public ResponseEntity<RestrictedTenantDTO> getRestrictedTenantDataBySubdomain(String subdomain) {
var tenantById = tenantServiceFacade.findTenantBySubdomain(subdomain);
public ResponseEntity<RestrictedTenantDTO> getRestrictedTenantDataBySubdomain(String subdomain, Long tenantId) {
var tenantById = tenantServiceFacade.findTenantBySubdomain(subdomain, tenantId);
return tenantById.isEmpty() ? new ResponseEntity<>(HttpStatus.NOT_FOUND)
: new ResponseEntity<>(tenantById.get(), HttpStatus.OK);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
import com.vi.tenantservice.api.model.TenantEntity;
import com.vi.tenantservice.api.service.TenantService;
import com.vi.tenantservice.api.validation.TenantInputSanitizer;
import com.vi.tenantservice.config.security.AuthorisationService;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.ws.rs.BadRequestException;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

/**
Expand All @@ -29,6 +32,10 @@ public class TenantServiceFacade {
private final @NonNull TenantConverter tenantConverter;
private final @NonNull TenantInputSanitizer tenantInputSanitizer;
private final @NonNull TenantFacadeAuthorisationService tenantFacadeAuthorisationService;
private final @NonNull AuthorisationService authorisationService;

@Value("${feature.multitenancy.with.single.domain.enabled}")
private boolean multitenancyWithSingleDomain;

public TenantDTO createTenant(TenantDTO tenantDTO) {
log.info("Creating new tenant");
Expand Down Expand Up @@ -74,17 +81,35 @@ public Optional<RestrictedTenantDTO> findRestrictedTenantById(Long id) {
return tenantById.isEmpty() ? Optional.empty()
: Optional.of(tenantConverter.toRestrictedTenantDTO(tenantById.get()));
}

public List<BasicTenantLicensingDTO> getAllTenants() {
var tenantEntities = tenantService.getAllTenants();
return tenantEntities.stream().map(tenantConverter::toBasicLicensingTenantDTO).collect(
Collectors.toList());
}

public Optional<RestrictedTenantDTO> findTenantBySubdomain(String subdomain) {
var tenantById = tenantService.findTenantBySubdomain(subdomain);
return tenantById.isEmpty() ? Optional.empty()
: Optional.of(tenantConverter.toRestrictedTenantDTO(tenantById.get()));
public Optional<RestrictedTenantDTO> findTenantBySubdomain(String subdomain, Long optionalTenantIdOverride) {
var tenantBySubdomain = tenantService.findTenantBySubdomain(subdomain);

Optional<Long> tenantIdFromRequestOrCookie = authorisationService.resolveTenantFromRequest(optionalTenantIdOverride);
if (multitenancyWithSingleDomain && tenantIdFromRequestOrCookie.isPresent()) {
return getSingleDomainSpecificTenantData(tenantBySubdomain, tenantIdFromRequestOrCookie.get());
}

return tenantBySubdomain.isEmpty() ? Optional.empty()
: Optional.of(tenantConverter.toRestrictedTenantDTO(tenantBySubdomain.get()));
}

public Optional<RestrictedTenantDTO> getSingleDomainSpecificTenantData(
Optional<TenantEntity> mainTenantForSingleDomainMultitenancy, Long resolvedTenantId) {

Optional<TenantEntity> resolvedTenant = tenantService.findTenantById(resolvedTenantId);
if (resolvedTenant.isEmpty()) {
throw new BadRequestException("Tenant not found for id " + resolvedTenantId);
}
RestrictedTenantDTO restrictedTenantDTO = tenantConverter.toRestrictedTenantDTO(mainTenantForSingleDomainMultitenancy.get());
restrictedTenantDTO.getContent().setPrivacy(resolvedTenant.get().getContentPrivacy());
return Optional.of(restrictedTenantDTO);
}

public Optional<RestrictedTenantDTO> getSingleTenant() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
package com.vi.tenantservice.config.security;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Base64;
import java.util.Map;
import java.util.Optional;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.keycloak.KeycloakPrincipal;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.WebUtils;

@Service
public class AuthorisationService {

@Value("${feature.multitenancy.with.single.domain.enabled}")
private boolean multitenancyWithSingleDomain;

public boolean hasAuthority(String authorityName) {
return getAuthentication().getAuthorities().stream()
.anyMatch(role -> authorityName.equals(role.getAuthority()));
}


public Optional<Long> findTenantIdInAccessToken() {
Integer tenantId = (Integer) getPrincipal().getKeycloakSecurityContext().getToken()
.getOtherClaims().get("tenantId");
Expand All @@ -36,4 +49,38 @@ private Authentication getAuthentication() {
private KeycloakPrincipal getPrincipal() {
return (KeycloakPrincipal) getAuthentication().getPrincipal();
}

public Optional<Long> resolveTenantFromRequest(Long tenantId) {

if(!multitenancyWithSingleDomain){
return Optional.empty();
}

if (tenantId != null) {
return Optional.of(tenantId);
}
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
Cookie token = WebUtils.getCookie(request, "keycloak");

if (token == null) {
return Optional.empty();
}

String[] chunks = token.getValue().split("\\.");
Base64.Decoder decoder = Base64.getUrlDecoder();
String payload = new String(decoder.decode(chunks[1]));
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try {
Map<String, Object> map = objectMapper.readValue(payload, Map.class);
Integer tenantIdFromCookie = (Integer) map.get("tenantId");
return tenantIdFromCookie == null ? Optional.empty()
: Optional.of(Long.valueOf(tenantIdFromCookie));
} catch (JsonProcessingException e) {
return Optional.empty();
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.vi.tenantservice.api.model.TenantEntity;
import com.vi.tenantservice.api.service.TenantService;
import com.vi.tenantservice.api.validation.TenantInputSanitizer;
import com.vi.tenantservice.config.security.AuthorisationService;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
Expand All @@ -23,6 +24,7 @@
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.test.util.ReflectionTestUtils;

@ExtendWith(MockitoExtension.class)
class TenantServiceFacadeTest {
Expand All @@ -45,6 +47,9 @@ class TenantServiceFacadeTest {
@Mock
private TenantFacadeAuthorisationService tenantFacadeAuthorisationService;

@Mock
private AuthorisationService authorisationService;

@InjectMocks
private TenantServiceFacade tenantServiceFacade;

Expand Down Expand Up @@ -193,4 +198,28 @@ void getSingleTenant_Should_shouldThrowIllegalStateException_When_moreTenantsAre
verify(tenantService).getAllTenants();
verifyNoInteractions(converter);
}

@Test
void findTenantBySubdomain_Should_returnTenantAwareData_When_RequestIsTenantAware(){
String subdomain = "app";
ReflectionTestUtils.setField(tenantServiceFacade,"multitenancyWithSingleDomain",true);
ReflectionTestUtils.setField(tenantServiceFacade,"tenantConverter",new TenantConverter());

TenantEntity defaultTenantEntity = new TenantEntity();
defaultTenantEntity.setContentPrivacy("content1");
Optional<TenantEntity> defaultTenant = Optional.of(defaultTenantEntity);

TenantEntity accessTokenTenant = new TenantEntity();
accessTokenTenant.setContentPrivacy("content2");
Optional<TenantEntity> accessTokenTenantData = Optional.of(accessTokenTenant);

when(tenantService.findTenantBySubdomain(subdomain)).thenReturn(defaultTenant);
when(authorisationService.resolveTenantFromRequest(null)).thenReturn(Optional.of(2L));
when(tenantService.findTenantById(2L)).thenReturn(accessTokenTenantData);

Optional<RestrictedTenantDTO> tenantDTO = tenantServiceFacade.findTenantBySubdomain(subdomain, null);
assertThat(tenantDTO.get().getContent().getPrivacy()).contains("content2");

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.vi.tenantservice.api.service;

import static org.assertj.core.api.Assertions.assertThat;

import com.vi.tenantservice.config.security.AuthorisationService;
import java.util.Optional;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@ExtendWith(MockitoExtension.class)
public class AuthorisationServiceTest {

@InjectMocks
AuthorisationService authorisationService;

@Test
public void resolve_Should_returnTenantId_When_tenantIdInCookie() {
ReflectionTestUtils.setField(authorisationService,"multitenancyWithSingleDomain",true);
String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9."
+ "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTY2NzM0NzI0MiwiZXhwIjoxNjY3MzUwODQyLCJ0ZW5hbnRJZCI6NH0."
+ "SjDBi7mbXwpUuZmUl7BEqptxsrd2aEJ6VMSIfTQx4sk";
givenRequestContextIsSet(jwt);
Optional<Long> tenantId = authorisationService.resolveTenantFromRequest(null);
assertThat(tenantId.get()).isNotNull();
resetRequestAttributes();
}

@Test
public void resolve_Should_returnNull_When_tenantIdNotInCookie() {
ReflectionTestUtils.setField(authorisationService,"multitenancyWithSingleDomain",true);
String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9."
+ "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTY2NzM0NzI0MiwiZXhwIjoxNjY3MzUwODQyfQ."
+ "rKjznZb8k-IMylStd_1qs8Qwk7-pKPq33Nax-Fj_M6w";
givenRequestContextIsSet(jwt);
Optional<Long> tenantId = authorisationService.resolveTenantFromRequest(null);
assertThat(tenantId.isEmpty()).isEqualTo(true);
resetRequestAttributes();
}

@Test
public void resolve_Should_returnTenantId_When_tenantIdGivenByArgument() {
ReflectionTestUtils.setField(authorisationService,"multitenancyWithSingleDomain",true);
givenRequestContextIsSet(null);
Optional<Long> tenantId = authorisationService.resolveTenantFromRequest(1L);
assertThat(tenantId.get()).isEqualTo(1L);
resetRequestAttributes();
}

private void givenRequestContextIsSet(String jwt) {
MockHttpServletRequest request = new MockHttpServletRequest();
Cookie cookie = new Cookie("keycloak", jwt);
request.setCookies(cookie);
ServletRequestAttributes servletRequestAttributes = new ServletRequestAttributes(request);
RequestContextHolder.setRequestAttributes(servletRequestAttributes);
}

private void resetRequestAttributes() {
RequestContextHolder.setRequestAttributes(null);
}

}

0 comments on commit af2428e

Please sign in to comment.