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

feat: implement custom resolvers for custom auth over oauth2 #11053

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
18 changes: 13 additions & 5 deletions src/main/java/org/cbioportal/persistence/SecurityRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,36 @@
import org.cbioportal.model.UserAuthorities;

/**
* Interface to use to retrieve
* portal user information.
* The resolver class implementing SecurityRepository interface
* can define how users (and their rights) are evaluated.
* Depending on the resolver type (template), it can be used in different
* contexts. For example, FullAccessResolver implements SecurityRepository<Object>
* can be used anywhere since it implements object. If you need to access specific properties
* of the user authentication context, you have to implement for example
* SecurityRepository<OidcUser> interface, but then your resolver is usable only
* with authentication type of oauth2.
*/
public interface SecurityRepository {
public interface SecurityRepository<AuthUserContext> {

/**
* Given a user id, returns a user instance.
* If username does not exist in db, returns null.
*
* @param username String
* @param user object that has necessary user information
* @return User
*/
User getPortalUser(String username);
User getPortalUser(String username, AuthUserContext user);

/**
* Given a user id, returns a UserAuthorities instance.
* If username does not exist in db, returns null.
*
* @param username String
* @param user object that has necessary user information
* @return UserAuthorities
*/
UserAuthorities getPortalUserAuthorities(String username);
UserAuthorities getPortalUserAuthorities(String username, AuthUserContext user);

void addPortalUser(User user);
void addPortalUserAuthorities(UserAuthorities userAuthorities);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,4 @@ public interface SecurityMapper {

void addPortalUser(User user);
void addPortalUserAuthority(@Param("email") String email, @Param("authority") String authority);

/**
* Given an internal cancer study id, returns groups string.
* Returns null if cancer study does not exist.
*
* @param internalCancerStudyId Integer
* @return String groups
*/
String getCancerStudyGroups(Integer internalCancerStudyId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,37 @@
import org.cbioportal.model.UserAuthorities;
import org.cbioportal.persistence.SecurityRepository;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

/**
* Security resolver usable for any authentication scheme.
* Requires presence of user entries & roles in the local database.
* Even a successful authentication over a third-party provider will not
* be accepted if the user does not exist in the database.
*/
@Repository
public class SecurityMyBatisRepository implements SecurityRepository {
@ConditionalOnProperty(name = "security.repository.type", havingValue = "cbioportal", matchIfMissing = true)
public class SecurityMyBatisRepository implements SecurityRepository<Object> {

private static final Logger log = LoggerFactory.getLogger(SecurityMyBatisRepository.class);

@Autowired
private SecurityMapper securityMapper;

@Autowired
private StudyGroupMapper studyGroupMapper;

/**
* Given a user id, returns a user instance.
* If username does not exist in db, returns null.
*
* @param username String
* @param _unused
* @return User
*/
@Override
public User getPortalUser(String username) {
public User getPortalUser(String username, Object _unused) {
User user = securityMapper.getPortalUser(username);
if (user != null) {
log.debug("User " + username + " was found in the users table, email is " + user.getEmail());
Expand All @@ -79,10 +90,11 @@ public User getPortalUser(String username) {
* If username does not exist in db, returns null.
*
* @param username String
* @param _unused
* @return UserAuthorities
*/
@Override
public UserAuthorities getPortalUserAuthorities(String username) {
public UserAuthorities getPortalUserAuthorities(String username, Object _unused) {
return securityMapper.getPortalUserAuthorities(username);
}

Expand All @@ -107,7 +119,7 @@ public void addPortalUserAuthorities(UserAuthorities userAuthorities) {
*/
@Override
public Set<String> getCancerStudyGroups(Integer internalCancerStudyId) {
String groups = securityMapper.getCancerStudyGroups(internalCancerStudyId);
String groups = studyGroupMapper.getCancerStudyGroups(internalCancerStudyId);
if (groups == null) {
return Collections.emptySet();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2015 Memorial Sloan-Kettering Cancer Center.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS
* FOR A PARTICULAR PURPOSE. The software and documentation provided hereunder
* is on an "as is" basis, and Memorial Sloan-Kettering Cancer Center has no
* obligations to provide maintenance, support, updates, enhancements or
* modifications. In no event shall Memorial Sloan-Kettering Cancer Center be
* liable to any party for direct, indirect, special, incidental or
* consequential damages, including lost profits, arising out of the use of this
* software and its documentation, even if Memorial Sloan-Kettering Cancer
* Center has been advised of the possibility of such damage.
*/

/*
* This file is part of cBioPortal.
*
* cBioPortal is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.cbioportal.persistence.mybatis;

// imports
import org.apache.ibatis.annotations.Param;

/**
* Interface to use to retrieve
* portal user information.
*/
public interface StudyGroupMapper {
/**
* Given an internal cancer study id, returns groups string.
* Returns null if cancer study does not exist.
*
* @param internalCancerStudyId Integer
* @return String groups
*/
String getCancerStudyGroups(Integer internalCancerStudyId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;

import java.util.HashSet;
import java.util.Objects;
Expand All @@ -29,12 +30,12 @@
public class CustomOAuth2AuthorizationConfig {
Logger log = LoggerFactory.getLogger(CustomOAuth2AuthorizationConfig.class);

private final SecurityRepository securityRepository;
private final SecurityRepository<OidcUser> securityRepository;

private static final String NAME_ATTRIBUTE_KEY = "email";

@Autowired
public CustomOAuth2AuthorizationConfig(SecurityRepository securityRepository) {
public CustomOAuth2AuthorizationConfig(SecurityRepository<OidcUser> securityRepository) {
this.securityRepository = securityRepository;
}

Expand All @@ -48,9 +49,9 @@ public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);

var authenticatedPortalUser = loadPortalUser(oidcUser.getEmail());
var authenticatedPortalUser = loadPortalUser(oidcUser.getEmail(), oidcUser);
if (Objects.isNull(authenticatedPortalUser.cbioUser) || !authenticatedPortalUser.cbioUser.isEnabled()) {
log.debug("User: {} either not in db or not authorized", oidcUser.getEmail());
log.error("User: {} either not in db or not authorized", oidcUser.getEmail());
throw new OAuth2AuthenticationException("user not authorized");
}
Set<GrantedAuthority> mappedAuthorities = authenticatedPortalUser.authorities;
Expand All @@ -59,11 +60,11 @@ public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
};
}

private AuthenticatedPortalUser loadPortalUser(String email) {
private AuthenticatedPortalUser loadPortalUser(String username, OidcUser user) {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
User cbioUser = securityRepository.getPortalUser(email);
User cbioUser = securityRepository.getPortalUser(username, user);
if (!Objects.isNull(cbioUser)) {
UserAuthorities authorities = securityRepository.getPortalUserAuthorities(email);
UserAuthorities authorities = securityRepository.getPortalUserAuthorities(username, user);
if (!Objects.isNull(authorities)) {
mappedAuthorities.addAll(AuthorityUtils.createAuthorityList(authorities.getAuthorities()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.cbioportal.security.config;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;

import java.util.Map;
import java.util.HashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component
public class OAuth2AuthRequestCustomParamsResolver implements OAuth2AuthorizationRequestResolver {
private static final Logger log = LoggerFactory.getLogger(OAuth2AuthRequestCustomParamsResolver.class);

private final DefaultOAuth2AuthorizationRequestResolver defaultResolver;

// @Autowired
// private OAuthCustomRequestParams authParams;
//TODO Try making it dynamic by using the above (auto-expanded config map key:value)

@Value("${security.custom.oauth.request.acr:}")
private String acr_value;

public OAuth2AuthRequestCustomParamsResolver(ClientRegistrationRepository repo) {
this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, "/oauth2/authorization");
}

@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest authorizationRequest = defaultResolver.resolve(request, clientRegistrationId);
return authorizationRequest != null ? customizeAuthorizationRequest(authorizationRequest) : null;
}

@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest authorizationRequest = defaultResolver.resolve(request);
return authorizationRequest != null ? customizeAuthorizationRequest(authorizationRequest) : null;
}

private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest) {
OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.from(authorizationRequest);

// TODO: more flexible way
// Map<String, String> params = authParams.getParams();
// // Add custom parameters from authParams
// if (params != null) {
// Map<String, Object> additionalParams = new HashMap<>(authorizationRequest.getAdditionalParameters());
// additionalParams.putAll(params);
// builder.additionalParameters(additionalParams);
// } else

if (acr_value != null && !acr_value.equals("")) {
Map<String, Object> additionalParams = new HashMap<>(authorizationRequest.getAdditionalParameters());
additionalParams.put("acr_value", acr_value);
builder.additionalParameters(additionalParams);
}

return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.web.SecurityFilterChain;
Expand Down Expand Up @@ -47,6 +48,12 @@ public class OAuth2SecurityConfig {

private static final String LOGIN_URL = "/login";

@Bean
public OAuth2AuthorizationRequestResolver customAuthorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository) {
return new OAuth2AuthRequestCustomParamsResolver(clientRegistrationRepository);
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
Expand All @@ -58,6 +65,9 @@ public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepo
)
.oauth2Login(login ->
login
.authorizationEndpoint(authorization -> authorization
.authorizationRequestResolver(customAuthorizationRequestResolver(clientRegistrationRepository))
)
.loginPage(LOGIN_URL)
.userInfoEndpoint(userInfo ->
userInfo.userAuthoritiesMapper(userAuthoritiesMapper())
Expand Down
Loading