Skip to content

Commit

Permalink
feat: prototype for extracting teamProject from initial request
Browse files Browse the repository at this point in the history
...and setting that as a user role.

This prototype is still missing
a method to validate the teamProject
against Arborist.

feat: introduce custom configuration option
  • Loading branch information
pieterlukasse committed Oct 11, 2023
1 parent 723eb4c commit c2069e3
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 15 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/image_build_push.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Build Image and Push to Quay

on: push

jobs:
ci:
name: Build Image and Push to Quay
uses: uc-cdis/.github/.github/workflows/image_build_push.yaml@master
with:
OVERRIDE_REPO_NAME: "ohdsi-webapi"
BUILD_PLATFORMS: "linux/amd64"
secrets:
ECR_AWS_ACCESS_KEY_ID: ${{ secrets.ECR_AWS_ACCESS_KEY_ID }}
ECR_AWS_SECRET_ACCESS_KEY: ${{ secrets.ECR_AWS_SECRET_ACCESS_KEY }}
QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }}
QUAY_ROBOT_TOKEN: ${{ secrets.QUAY_ROBOT_TOKEN }}
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
<spring.batch.repository.isolationLevelForCreate>ISOLATION_READ_COMMITTED</spring.batch.repository.isolationLevelForCreate>
<spring.profiles.active>default</spring.profiles.active>

<security.ohdsi.custom.authorization.mode>teamproject</security.ohdsi.custom.authorization.mode>
<security.provider>DisabledSecurity</security.provider>
<security.token.expiration>43200</security.token.expiration>
<security.origin>http://localhost</security.origin>
Expand Down Expand Up @@ -229,7 +230,7 @@
<spring.jpa.properties.hibernate.jdbc.batch_size>200</spring.jpa.properties.hibernate.jdbc.batch_size>
<spring.jpa.properties.hibernate.order_inserts>true</spring.jpa.properties.hibernate.order_inserts>
<logging.level.root>info</logging.level.root>
<logging.level.org.ohdsi>info</logging.level.org.ohdsi>
<logging.level.org.ohdsi>debug</logging.level.org.ohdsi>
<logging.level.org.springframework.orm>info</logging.level.org.springframework.orm>
<logging.level.org.springframework.jdbc>info</logging.level.org.springframework.jdbc>
<logging.level.org.springframework.web>info</logging.level.org.springframework.web>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,9 @@ public UserOrigin getOrigin() {
public void setOrigin(UserOrigin origin) {
this.origin = origin;
}

public String toString() {
role = this.getRole();
return (role != null ? role.getName() : "");
}
}
99 changes: 90 additions & 9 deletions src/main/java/org/ohdsi/webapi/shiro/PermissionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import org.ohdsi.webapi.shiro.Entities.UserRepository;
import org.ohdsi.webapi.shiro.Entities.UserRoleEntity;
import org.ohdsi.webapi.shiro.Entities.UserRoleRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
Expand All @@ -37,6 +39,7 @@
@Component
@Transactional
public class PermissionManager {
private final Logger logger = LoggerFactory.getLogger(PermissionManager.class);

@Autowired
private UserRepository userRepository;
Expand All @@ -59,6 +62,8 @@ public class PermissionManager {
private ThreadLocal<ConcurrentHashMap<String, UserSimpleAuthorizationInfo>> authorizationInfoCache = ThreadLocal.withInitial(ConcurrentHashMap::new);

public RoleEntity addRole(String roleName, boolean isSystem) {
logger.debug("Called addRole: {}", roleName);

Guard.checkNotEmpty(roleName);

checkRoleIsAbsent(roleName, isSystem, "Can't create role - it already exists");
Expand Down Expand Up @@ -96,8 +101,32 @@ public void removeUserFromRole(String roleName, String login, UserOrigin origin)
UserEntity user = this.getUserByLogin(login);

UserRoleEntity userRole = this.userRoleRepository.findByUserAndRole(user, role);
if (userRole != null && (origin == null || origin.equals(userRole.getOrigin())))
if (userRole != null && (origin == null || origin.equals(userRole.getOrigin()))) {
logger.debug("Removing user from role: {}, {}, {}", user.getLogin(), role.getName(), userRole.getOrigin());
this.userRoleRepository.delete(userRole);
}
}

public void removeUserFromUserRole(String roleName, String login) {
Guard.checkNotEmpty(roleName);
Guard.checkNotEmpty(login);

if (roleName.equalsIgnoreCase(login))
throw new RuntimeException("Can't remove user from personal role");

logger.debug("Checking if role exists: {}", roleName);
RoleEntity role = this.roleRepository.findByNameAndSystemRole(roleName, false);
if (role != null) {
UserEntity user = this.getUserByLogin(login);

UserRoleEntity userRole = this.userRoleRepository.findByUserAndRole(user, role);
if (userRole != null) {
logger.debug("Removing user from USER role: {}, {}", user.getLogin(), roleName);
this.userRoleRepository.delete(userRole);
}
} else {
logger.debug("Role {} not found", roleName);
}
}

public Iterable<RoleEntity> getRoles(boolean includePersonalRoles) {
Expand Down Expand Up @@ -141,14 +170,29 @@ public void clearAuthorizationInfoCache() {
this.authorizationInfoCache.set(new ConcurrentHashMap<>());
}


@Transactional
public void registerUser(String login, String name, Set<String> defaultRoles, Set<String> newUserRoles,
boolean resetRoles) {
registerUser(login, name, UserOrigin.SYSTEM, defaultRoles, newUserRoles, resetRoles);
}

@Transactional
public UserEntity registerUser(final String login, final String name, final Set<String> defaultRoles) {
return registerUser(login, name, UserOrigin.SYSTEM, defaultRoles);
return registerUser(login, name, UserOrigin.SYSTEM, defaultRoles, null, false);
}

@Transactional
public UserEntity registerUser(final String login, final String name, final UserOrigin userOrigin,
final Set<String> defaultRoles) {
return registerUser(login, name, userOrigin, defaultRoles, null, false);
}

@Transactional
public UserEntity registerUser(final String login, final String name, final UserOrigin userOrigin,
final Set<String> defaultRoles, final Set<String> newUserRoles, boolean resetRoles) {
logger.debug("Called registerUser with resetRoles: login={}, reset roles={}, default roles={}, new user roles={}",
login, resetRoles, defaultRoles, newUserRoles);
Guard.checkNotEmpty(login);

UserEntity user = userRepository.findByLogin(login);
Expand All @@ -162,6 +206,14 @@ public UserEntity registerUser(final String login, final String name, final User
user.setOrigin(userOrigin);
user = userRepository.save(user);
}
if (resetRoles) {
// remove all user roles:
removeAllUserRolesFromUser(login, user);
// add back just the given newUserRoles:
addRolesForUser(login, userOrigin, user, newUserRoles, false);
}
// get user again, fresh from db with all new roles:
user = userRepository.findOne(user.getId());
return user;
}

Expand All @@ -176,18 +228,46 @@ public UserEntity registerUser(final String login, final String name, final User

RoleEntity personalRole = this.addRole(login, false);
this.addUser(user, personalRole, userOrigin, null);
addRolesForUser(login, userOrigin, user, newUserRoles, false);
addDefaultRolesForUser(login, userOrigin, user, defaultRoles);
// // get user again, fresh from db with all new roles:
user = userRepository.findOne(user.getId());
return user;
}

if (defaultRoles != null) {
for (String roleName: defaultRoles) {
RoleEntity defaultRole = this.getSystemRoleByName(roleName);
if (defaultRole != null) {
this.addUser(user, defaultRole, userOrigin, null);
private void addRolesForUser(String login, UserOrigin userOrigin, UserEntity user, Set<String> roles, boolean isSystemRole) {
if (roles != null) {
for (String roleName: roles) {
// Temporary patch/workaround (in reality the role should have been added by sysadmin?):
pocAddUserRole(roleName);
// end temporary patch
RoleEntity role = this.getRoleByName(roleName, isSystemRole);
if (role != null) {
this.addUser(user, role, userOrigin, null);
}
}
}
}

user = userRepository.findOne(user.getId());
return user;
private void addDefaultRolesForUser(String login, UserOrigin userOrigin, UserEntity user, Set<String> roles) {
addRolesForUser(login, userOrigin, user, roles,true);
}

private void removeAllUserRolesFromUser(String login, UserEntity user) {
Set<RoleEntity> userRoles = this.getUserRoles(user);
// remove all roles except the personal role:
userRoles.stream().filter(role -> !role.getName().equalsIgnoreCase(login)).forEach(userRole -> {
this.removeUserFromUserRole(userRole.getName(), login);
});
}

private RoleEntity pocAddUserRole(String roleName) {
RoleEntity role = this.roleRepository.findByNameAndSystemRole(roleName, false);
if (role != null) {
return role;
} else {
return addRole(roleName, false);
}
}

public Iterable<UserEntity> getUsers() {
Expand Down Expand Up @@ -322,6 +402,7 @@ private Set<PermissionEntity> getRolePermissions(RoleEntity role) {

private Set<RoleEntity> getUserRoles(UserEntity user) {
Set<UserRoleEntity> userRoles = user.getUserRoles();
logger.debug("Called getUserRoles. Found: {}", userRoles);
Set<RoleEntity> roles = new LinkedHashSet<>();
for (UserRoleEntity userRole : userRoles) {
if (isRelationAllowed(userRole.getStatus())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.ws.rs.core.UriBuilder;
Expand All @@ -31,27 +34,34 @@
import org.ohdsi.webapi.shiro.TokenManager;
import org.ohdsi.webapi.util.UserUtils;
import org.pac4j.core.profile.CommonProfile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
*
* @author gennadiy.anisimov
*/
public class UpdateAccessTokenFilter extends AdviceFilter {

private final Logger logger = LoggerFactory.getLogger(UpdateAccessTokenFilter.class);

private final PermissionManager authorizer;
private final int tokenExpirationIntervalInSeconds;
private final Set<String> defaultRoles;
private final String onFailRedirectUrl;
private final String authorizationMode;

public UpdateAccessTokenFilter(
PermissionManager authorizer,
Set<String> defaultRoles,
int tokenExpirationIntervalInSeconds,
String onFailRedirectUrl) {
String onFailRedirectUrl,
String authorizationMode) {
this.authorizer = authorizer;
this.tokenExpirationIntervalInSeconds = tokenExpirationIntervalInSeconds;
this.defaultRoles = defaultRoles;
this.onFailRedirectUrl = onFailRedirectUrl;
this.authorizationMode = authorizationMode;
logger.debug("AUTHORIZATION_MODE in UpdateAccessTokenFilter constructor == '{}'", this.authorizationMode);
}

@Override
Expand Down Expand Up @@ -121,12 +131,29 @@ protected boolean preHandle(ServletRequest request, ServletResponse response) th
session.stop();
}

if (jwt == null) {
if (jwt == null) { // dead check...jwt is always null...
if (name == null) {
name = login;
}
try {
this.authorizer.registerUser(login, name, defaultRoles);
logger.debug("AUTHORIZATION_MODE in UpdateAccessTokenFilter == '{}'", this.authorizationMode);
boolean resetRoles = false;
Set<String> newUserRoles = new HashSet<String>();
if (this.authorizationMode.equals("teamproject")) {
// in case of "teamproject" mode, we want all roles to be reset always, and
// set to only the one requested/found in the request parameters (following lines below):
resetRoles = true;
// check if a teamproject parameter is found in the request:
String teamProjectRole = extractTeamProjectFromRequestParameters(request);
// if found, add teamproject as a role in the newUserRoles list:
if (teamProjectRole != null) {
newUserRoles.add(teamProjectRole);
// double check with Arborist if this role has really been granted to the user....
// TODO
}
}
this.authorizer.registerUser(login, name, defaultRoles, newUserRoles, resetRoles);

} catch (Exception e) {
WebUtils.toHttp(response).setHeader("x-auth-error", e.getMessage());
throw new Exception(e);
Expand Down Expand Up @@ -174,4 +201,61 @@ private Date getExpirationDate(final int expirationIntervalInSeconds) {
calendar.add(Calendar.SECOND, expirationIntervalInSeconds);
return calendar.getTime();
}

private String extractTeamProjectFromRequestParameters(ServletRequest request) {
// Get the url
HttpServletRequest httpRequest = (HttpServletRequest) request;
String url = httpRequest.getRequestURL().toString();

// try to find it in the redirectUrl parameter:
logger.debug("Looking for redirectUrl in request: {}....", url);
String[] redirectUrlParams = getParameterValues(request, "redirectUrl");
if (redirectUrlParams != null) {
logger.debug("Parameter redirectUrl found. Checking if it contains teamproject....");
// teamProject will be in first one in this case...as only parameter:
String firstParameter = redirectUrlParams[0].toLowerCase();
if (firstParameter.contains("teamproject=")) {
String teamProject = firstParameter.split("teamproject=")[1];
logger.debug("Found teamproject: {}", teamProject);
return teamProject;
}
}

// try to find "teamproject" param in url itself (there will be no redirectUrl if user session is still valid):
logger.debug("Fallback1: Looking for teamproject in request: {}....", url);
String[] teamProjectParams = getParameterValues(request, "teamproject");
if (teamProjectParams != null) {
logger.debug("Parameter teamproject found. Parsing....");
String teamProject = teamProjectParams[0].toLowerCase();
logger.debug("Found teamproject: {}", teamProject);
return teamProject;
}

logger.debug("Fallback2: Looking for teamproject in Action-Location header of request: {}....", url);
String actionLocationUrl = httpRequest.getHeader("Action-Location");
if (actionLocationUrl != null && actionLocationUrl.contains("teamproject=")) {
String teamProject = actionLocationUrl.split("teamproject=")[1];
logger.debug("Found teamproject: {}", teamProject);
return teamProject;
}

logger.debug("Found NO teamproject.");
return null;
}

private String[] getParameterValues(ServletRequest request, String parameterName) {
// Get the parameters
logger.debug("Looking for parameter with name: {} ...", parameterName);
Enumeration<String> paramNames = request.getParameterNames();
while(paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
logger.debug("Parameter name: {}", paramName);
if (paramName.equals(parameterName)) {
String[] paramValues = request.getParameterValues(paramName);
return paramValues;
}
}
logger.debug("Found NO parameter with name: {}", parameterName);
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ public class AtlasRegularSecurity extends AtlasSecurity {
@Value("${security.auth.google.enabled}")
private boolean googleAuthEnabled;

@Value("${security.ohdsi.custom.authorization.mode}")
private String authorizationMode;

private RestTemplate restTemplate = new RestTemplate();

@Autowired
Expand All @@ -271,8 +274,9 @@ public Map<FilterTemplates, Filter> getFilters() {
Map<FilterTemplates, Filter> filters = super.getFilters();

filters.put(LOGOUT, new LogoutFilter(eventPublisher));
logger.debug("Initializing UpdateAccessTokenFilter with AUTHORIZATION_MODE === '{}'", this.authorizationMode);
filters.put(UPDATE_TOKEN, new UpdateAccessTokenFilter(this.authorizer, this.defaultRoles, this.tokenExpirationIntervalInSeconds,
this.redirectUrl));
this.redirectUrl, this.authorizationMode));

filters.put(ACCESS_AUTHC, new GoogleAccessTokenFilter(restTemplate, permissionManager, Collections.emptySet()));
filters.put(JWT_AUTHC, new AtlasJwtAuthFilter());
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ security.auth.ldap.enabled=${security.auth.ldap.enabled}
security.auth.ad.enabled=${security.auth.ad.enabled}
security.auth.cas.enabled=${security.auth.cas.enabled}

#Authorization config
security.ohdsi.custom.authorization.mode=${security.ohdsi.custom.authorization.mode}

#Execution engine
executionengine.updateStatusCallback=${executionengine.updateStatusCallback}
executionengine.resultCallback=${executionengine.resultCallback}
Expand Down

0 comments on commit c2069e3

Please sign in to comment.