Skip to content

Commit

Permalink
PO-327: Add custom annotation for authorisation (#342)
Browse files Browse the repository at this point in the history
* PO-327: Add custom annotation for authorisation

* PO-327: Add custom annotation for authorisation

* Fix typos
  • Loading branch information
sabahirfan authored May 3, 2024
1 parent eaab572 commit 431a759
Show file tree
Hide file tree
Showing 11 changed files with 400 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import uk.gov.hmcts.opal.authentication.exception.MissingRequestHeaderException;
import uk.gov.hmcts.opal.authorisation.model.Role;
import uk.gov.hmcts.opal.authorisation.model.UserState;
import uk.gov.hmcts.opal.service.opal.UserStateService;

import java.util.function.Supplier;

import static uk.gov.hmcts.opal.authorisation.aspect.AuthorizationAspectService.AUTHORIZATION;
import static uk.gov.hmcts.opal.util.PermissionUtil.checkAnyRoleHasPermission;
import static uk.gov.hmcts.opal.util.PermissionUtil.checkRoleHasPermission;

@Aspect
@Component
Expand All @@ -26,14 +30,47 @@ public Object checkAuthorization(ProceedingJoinPoint joinPoint,
) throws Throwable {

Object[] args = joinPoint.getArgs();
String authHeaderValue = authorizationAspectService.getRequestHeaderValue(args);
String bearerToken = authorizationAspectService.getAuthorization(authHeaderValue)
.orElseThrow(() -> new MissingRequestHeaderException(AUTHORIZATION));
UserState userState = userStateService.getUserStateUsingAuthToken(bearerToken);
UserState userState = getUserState(args);
if (checkAnyRoleHasPermission(userState, authorizedAnyRoleHasPermission.value())) {
return joinPoint.proceed();
}
throw new PermissionNotAllowedException(authorizedAnyRoleHasPermission.value());
}

@Around("execution(* *(*)) && @annotation(authorizedRoleHasPermission)")
public Object checkAuthorization(ProceedingJoinPoint joinPoint,
AuthorizedRoleHasPermission authorizedRoleHasPermission
) throws Throwable {

Object[] args = joinPoint.getArgs();
UserState userState = getUserState(args);

Role role = authorizationAspectService.getRole(args, userState);
if (checkRoleHasPermission(role, authorizedRoleHasPermission.value())) {
return joinPoint.proceed();
}
throw new PermissionNotAllowedException(authorizedRoleHasPermission.value(), role);
}

/**
* This will infer the UserState from the arguments of the annotated method if present.
* If no argument of USerState then it will fetch based on the bearer token of the current user.
*
* @param args arguments of the annotated method
* @return UserState object for the user
*/
private UserState getUserState(Object[] args) {
return authorizationAspectService.getUserState(args)
.orElseGet(getUserStateSupplier(args));
}

private Supplier<UserState> getUserStateSupplier(Object[] args) {
return () -> {
String authHeaderValue = authorizationAspectService.getRequestHeaderValue(args);
String bearerToken = authorizationAspectService.getAuthorization(authHeaderValue)
.orElseThrow(() -> new MissingRequestHeaderException(AUTHORIZATION));
return userStateService.getUserStateUsingAuthToken(bearerToken);
};
}
}

Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package uk.gov.hmcts.opal.authorisation.aspect;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import uk.gov.hmcts.opal.authorisation.model.Role;
import uk.gov.hmcts.opal.authorisation.model.UserState;
import uk.gov.hmcts.opal.dto.AddNoteDto;
import uk.gov.hmcts.opal.dto.NoteDto;

import java.util.Arrays;
import java.util.Optional;

import static java.lang.String.format;
import static uk.gov.hmcts.opal.util.PermissionUtil.getRequiredRole;

@Slf4j
@Component
public class AuthorizationAspectService {

Expand All @@ -31,4 +41,32 @@ public Optional<String> getAuthorization(String authHeaderValue) {
}
return Optional.empty();
}

public Role getRole(Object[] args, UserState userState) {
for (Object arg : args) {
if (arg instanceof Role) {
return (Role) arg;
} else if (arg instanceof AddNoteDto addNoteDto) {
return getRequiredRole(userState, addNoteDto.getBusinessUnitId());
} else if (arg instanceof NoteDto noteDto) {
return getRequiredRole(userState, noteDto.getBusinessUnitId());
}
}
throw new RoleNotFoundException(format(
"Can't infer the role for user %s. "
+ "Annotated method needs to have arguments of types (Role, AddNoteDto, NoteDto).",
userState.getUserName()
));
}

public Optional<UserState> getUserState(Object[] args) {
return getArgument(args, UserState.class);
}

public <T> Optional<T> getArgument(Object[] args, Class<T> clazz) {
return Arrays.stream(args)
.filter(clazz::isInstance)
.map(clazz::cast)
.findFirst();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package uk.gov.hmcts.opal.authorisation.aspect;

import uk.gov.hmcts.opal.authorisation.model.Permissions;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* The <code>AuthorizedRoleHasPermission</code> annotation is used to authorise or deny execution of a business method
* based on the role.
* If the given role has the permission then only execution will be allowed, otherwise PermissionNotAllowedException
* will be thrown.
* For example:
* The role can be one of the argument of the annotated method.
* <pre>
* &#064;AuthorizedRoleHasPermission(Permissions.ACCOUNT_ENQUIRY)
* public void businessMethod(Role role) { ... }
* </pre>
* The role can be inferred if one of the argument is of type NoteDto, the role will be picked by matching
* businessUnitId of NoteDto argument within the userState roles.
* If this role has the permission then only execution will be allowed, otherwise PermissionNotAllowedException
* will be thrown.
* For example:
* <pre>
* &#064;AuthorizedRoleHasPermission(Permissions.ACCOUNT_ENQUIRY_NOTES)
* public NoteDto saveNote(NoteDto noteDto) { .. }
* </pre>
* The role can be inferred if one of the argument is of type NoteDto, the role will be picked by matching
* businessUnitId of AddNoteDto argument within the userState roles.
* If this role has the permission then only execution will be allowed, otherwise PermissionNotAllowedException
* will be thrown.
* For example:
* <pre>
* &#064;AuthorizedRoleHasPermission(Permissions.ACCOUNT_ENQUIRY_NOTES)
* public NoteDto saveNote(AddNoteDto addNoteDto) { .. }
* </pre>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorizedRoleHasPermission {
Permissions value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@

import lombok.Getter;
import uk.gov.hmcts.opal.authorisation.model.Permissions;
import uk.gov.hmcts.opal.authorisation.model.Role;

@Getter
public class PermissionNotAllowedException extends RuntimeException {

private final Permissions permission;
private final Role role;

public PermissionNotAllowedException(Permissions value) {
super(value + " permission is not allowed for the user");
this.permission = value;
this.role = null;
}

public PermissionNotAllowedException(Permissions permission, Role role) {
super(permission + " permission is not allowed for the role " + role);
this.permission = permission;
this.role = role;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package uk.gov.hmcts.opal.authorisation.aspect;

public class RoleNotFoundException extends RuntimeException {

public RoleNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import uk.gov.hmcts.opal.authorisation.model.Permissions;
import uk.gov.hmcts.opal.authorisation.model.Role;
import uk.gov.hmcts.opal.authorisation.model.UserState;
import uk.gov.hmcts.opal.dto.AccountDetailsDto;
Expand All @@ -35,7 +34,6 @@

import static uk.gov.hmcts.opal.util.HttpUtil.buildCreatedResponse;
import static uk.gov.hmcts.opal.util.HttpUtil.buildResponse;
import static uk.gov.hmcts.opal.util.PermissionUtil.checkRoleHasPermission;
import static uk.gov.hmcts.opal.util.PermissionUtil.getRequiredRole;

@RestController
Expand Down Expand Up @@ -120,7 +118,6 @@ public ResponseEntity<NoteDto> addNote(

UserState userState = userStateService.getUserStateUsingAuthToken(authHeaderValue);
Role role = getRequiredRole(userState, addNote.getBusinessUnitId());
checkRoleHasPermission(role, Permissions.ACCOUNT_ENQUIRY_NOTES);

NoteDto noteDto = NoteDto.builder()
.associatedRecordId(addNote.getAssociatedRecordId())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.slf4j.Logger;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import uk.gov.hmcts.opal.authorisation.aspect.AuthorizedRoleHasPermission;
import uk.gov.hmcts.opal.config.properties.LegacyGatewayProperties;
import uk.gov.hmcts.opal.dto.NoteDto;
import uk.gov.hmcts.opal.dto.legacy.LegacySaveNoteRequestDto;
Expand All @@ -13,6 +14,8 @@

import java.util.List;

import static uk.gov.hmcts.opal.authorisation.model.Permissions.ACCOUNT_ENQUIRY_NOTES;

@Service
@Slf4j(topic = "LegacyNoteService")
public class LegacyNoteService extends LegacyService implements NoteServiceInterface {
Expand All @@ -30,6 +33,7 @@ protected Logger getLog() {
}

@Override
@AuthorizedRoleHasPermission(ACCOUNT_ENQUIRY_NOTES)
public NoteDto saveNote(NoteDto noteDto) {
log.info("Saving Note: {}", noteDto);

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/uk/gov/hmcts/opal/service/opal/NoteService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import uk.gov.hmcts.opal.authorisation.aspect.AuthorizedAnyRoleHasPermission;
import uk.gov.hmcts.opal.authorisation.aspect.AuthorizedRoleHasPermission;
import uk.gov.hmcts.opal.authorisation.model.Permissions;
import uk.gov.hmcts.opal.dto.NoteDto;
import uk.gov.hmcts.opal.dto.search.NoteSearchDto;
Expand All @@ -24,6 +25,8 @@
import java.util.Optional;
import java.util.stream.Collectors;

import static uk.gov.hmcts.opal.authorisation.model.Permissions.ACCOUNT_ENQUIRY_NOTES;

@Service
@RequiredArgsConstructor
@Slf4j(topic = "NoteService")
Expand All @@ -35,6 +38,7 @@ public class NoteService implements NoteServiceInterface {

@Override
@FeatureToggle(feature = "add-note", value = true)
@AuthorizedRoleHasPermission(ACCOUNT_ENQUIRY_NOTES)
public NoteDto saveNote(NoteDto noteDto) {
// Restrict the 'postedBy' to 20 characters length
String postedBy = Optional.ofNullable(noteDto.getPostedBy())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import uk.gov.hmcts.opal.authorisation.aspect.AuthorizedRoleHasPermission;
import uk.gov.hmcts.opal.dto.NoteDto;
import uk.gov.hmcts.opal.dto.search.NoteSearchDto;
import uk.gov.hmcts.opal.launchdarkly.FeatureToggle;
Expand All @@ -13,6 +14,8 @@

import java.util.List;

import static uk.gov.hmcts.opal.authorisation.model.Permissions.ACCOUNT_ENQUIRY_NOTES;

@Service
@RequiredArgsConstructor
@Qualifier("noteServiceProxy")
Expand All @@ -28,6 +31,7 @@ private NoteServiceInterface getCurrentModeService() {

@Override
@FeatureToggle(feature = "add-note", value = true)
@AuthorizedRoleHasPermission(ACCOUNT_ENQUIRY_NOTES)
public NoteDto saveNote(NoteDto noteDto) {
return getCurrentModeService().saveNote(noteDto);
}
Expand Down
Loading

0 comments on commit 431a759

Please sign in to comment.