Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add data access token user role filter feature #10315

Merged
merged 4 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,16 @@ public static String parseUrl(String url)
@Value("${download_group:}") // default is empty string
public void setDownloadGroup(String property) { downloadGroup = property; }

public static final String DEFAULT_DAT_METHOD = "none";

private static String dataAccessTokenMethod;
@Value("${dat.method:none}") // default is empty string
public void setDataAccessTokenMethod(String property) { dataAccessTokenMethod = property; }

private static String tokenAccessUserRole;
@Value("${dat.filter_user_role:}") // default is empty string
public void setTokenAccessUserRole(String property) { tokenAccessUserRole = property; }

private static Logger LOG = LoggerFactory.getLogger(GlobalProperties.class);
private static ConfigPropertyResolver portalProperties = new ConfigPropertyResolver();
private static Properties mavenProperties = initializeProperties(MAVEN_PROPERTIES_FILE_NAME);
Expand Down Expand Up @@ -1296,4 +1306,15 @@ public static String getDownloadControl() {
}
}
}

public static String getDataAccessTokenMethod() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication != null &&
StringUtils.isNotEmpty(tokenAccessUserRole)) {
return authentication.getAuthorities().contains(new SimpleGrantedAuthority(tokenAccessUserRole)) ? dataAccessTokenMethod : DEFAULT_DAT_METHOD;
} else {
return dataAccessTokenMethod;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ The following properties must be present in portal.properties in order to allow
* **Permissible Values**: jwt, uuid, oauth2, none
* **Default Value**: none

**Property**: dat.filter\_user\_role (optional)

* **Description**: This property determines users access in token generation. If present, this role will be checked in user roles before generating a token.
* **Permissible Values**: A string value.
* **Default Value**: none

**Property**: dat.unauth\_users (optional, not used for dat.method = oauth2)

* **Description**: A list of users that should not be allowed to download a data access token.
Expand Down
3 changes: 2 additions & 1 deletion portal/src/main/webapp/config_service.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
String[] propNameArray = {
"app.version",
"app.name",
"dat.method",
"oncoprint.custom_driver_annotation.binary.menu_label",
"disabled_tabs",
"civic.url",
Expand Down Expand Up @@ -207,6 +206,8 @@
obj.put("sessionServiceEnabled", !StringUtils.isEmpty(GlobalProperties.getSessionServiceUrl()));

obj.put("skin_hide_download_controls", GlobalProperties.getDownloadControl());

obj.put("dat_method", GlobalProperties.getDataAccessTokenMethod());

out.println(obj.toJSONString());

Expand Down
1 change: 1 addition & 0 deletions src/main/resources/portal.properties.EXAMPLE
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ dat.method=none
dat.ttl_seconds=2592000
dat.uuid.max_number_per_user=1
dat.jwt.secret_key=
dat.filter_user_role=

# OAuth2 token data access settings
#dat.oauth2.clientId=<client-id>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.models.HttpMethod;
import org.apache.commons.lang3.StringUtils;
import org.cbioportal.model.DataAccessToken;
import org.cbioportal.service.DataAccessTokenService;
import org.cbioportal.service.exception.DataAccessTokenNoUserIdentityException;
Expand All @@ -32,6 +32,7 @@
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.HttpClientErrorException;
Expand All @@ -54,6 +55,10 @@ public class DataAccessTokenController {
@Value("${dat.unauth_users:anonymousUser}")
private String[] USERS_WHO_CANNOT_USE_TOKENS;

private static String userRoleToAccessToken;
@Value("${download_group:}") // default is empty string
public void setUserRoleToAccessToken(String property) { userRoleToAccessToken = property; }

@Autowired
private DataAccessTokenService tokenService;
private Set<String> usersWhoCannotUseTokenSet;
Expand Down Expand Up @@ -130,6 +135,10 @@ private String getAuthenticatedUser(Authentication authentication) {
if (usersWhoCannotUseTokenSet.contains(username)) {
throw new DataAccessTokenProhibitedUserException();
}
if(StringUtils.isNotEmpty(userRoleToAccessToken) &&
!authentication.getAuthorities().contains(new SimpleGrantedAuthority(userRoleToAccessToken))) {
throw new DataAccessTokenProhibitedUserException();
}
return username;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.commons.lang3.StringUtils;
import org.cbioportal.model.DataAccessToken;
import org.cbioportal.service.DataAccessTokenService;
import org.cbioportal.service.exception.DataAccessTokenNoUserIdentityException;
import org.cbioportal.service.exception.DataAccessTokenProhibitedUserException;
import org.cbioportal.web.config.annotation.InternalApi;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -46,6 +48,7 @@
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.HttpClientErrorException;
Expand Down Expand Up @@ -76,6 +79,9 @@ public class OAuth2DataAccessTokenController {
@Value("${dat.oauth2.clientId}")
private String clientId;

@Value("${dat.filter_user_role:}") // default is empty string
private String userRoleToAccessToken;

@Autowired
private DataAccessTokenService tokenService;
private String authorizationUrl;
Expand All @@ -98,6 +104,8 @@ public void postConstruct() throws UnsupportedEncodingException {
public ResponseEntity<String> downloadDataAccessToken(Authentication authentication,
HttpServletRequest request, HttpServletResponse response) throws IOException {

isUserAuthorizedToAccess(authentication);

// redirect to authentication endpoint
HttpHeaders headers = new HttpHeaders();
headers.add("Location", authorizationUrl);
Expand Down Expand Up @@ -156,4 +164,15 @@ public void revokeDataAccessToken(@ApiParam(required = true, value = "token") @P
throw new UnsupportedOperationException("this endpoint is does not apply to OAuth2 data access token method.");
}

private void isUserAuthorizedToAccess(Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
throw new DataAccessTokenNoUserIdentityException();
}

if(StringUtils.isNotEmpty(userRoleToAccessToken) &&
!authentication.getAuthorities().contains(new SimpleGrantedAuthority(userRoleToAccessToken))) {
throw new DataAccessTokenProhibitedUserException();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
Expand Down Expand Up @@ -190,6 +191,32 @@ public void createTokenValidUserTest() throws Exception {
.andReturn();
}

@Test
public void createTokenValidUserTestWithUserRole() throws Exception {
ReflectionTestUtils.setField(DataAccessTokenController.class, "userRoleToAccessToken", "PLACEHOLDER_ROLE");
Mockito.when(tokenService.createDataAccessToken(ArgumentMatchers.anyString())).thenReturn(MOCK_TOKEN_INFO);
HttpSession session = getSession(MOCK_USER, MOCK_PASSWORD);
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/data-access-tokens")
.session((MockHttpSession) session)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isCreated())
.andReturn();
}

@Test
public void createTokenUnauthorizedUserTestWithUserRole() throws Exception {
ReflectionTestUtils.setField(DataAccessTokenController.class, "userRoleToAccessToken", "TEST");
Mockito.when(tokenService.createDataAccessToken(ArgumentMatchers.anyString())).thenReturn(MOCK_TOKEN_INFO);
HttpSession session = getSession(MOCK_USER, MOCK_PASSWORD);
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/data-access-tokens")
.session((MockHttpSession) session)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
.andReturn();
}

kalletlak marked this conversation as resolved.
Show resolved Hide resolved
/* Tests mapping for DELETE /data-access-tokens
* Checks response status code is 200 success
* Checks that correct username argument is passed to service class
Expand Down Expand Up @@ -241,4 +268,14 @@ public Void answer(InvocationOnMock getAllTokensInvocation) {
Assert.fail("Unexpected argument passed to service class. Expected argument: " + MOCK_USER + " Received argument: " + receivedArgument);
}
}

@Test
public void createTokenNotLoggedIn() throws Exception {
Mockito.when(tokenService.createDataAccessToken(ArgumentMatchers.anyString())).thenReturn(MOCK_TOKEN_INFO);
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/data-access-tokens")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
.andReturn();
}
}
Loading