diff --git a/docker-compose.yaml b/docker-compose.yaml index f622ff31b1..334f094204 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -89,7 +89,7 @@ services: - CONSOLE_LOGGING_LEVEL=trace depends_on: api-lagoon-migrations: - condition: service_completed_successfully # don't start the API until the lagoon migrations are completed + condition: service_started keycloak: condition: service_started ports: diff --git a/services/keycloak/Dockerfile b/services/keycloak/Dockerfile index c5a0bb53e2..58fd58477b 100644 --- a/services/keycloak/Dockerfile +++ b/services/keycloak/Dockerfile @@ -1,3 +1,9 @@ +FROM maven:3.8.2-jdk-11 as builder + +COPY custom-mapper/. . + +RUN mvn clean compile package + ARG UPSTREAM_REPO ARG UPSTREAM_TAG FROM ${UPSTREAM_REPO:-uselagoon}/commons:${UPSTREAM_TAG:-latest} as commons @@ -67,6 +73,11 @@ ENV TMPDIR=/tmp \ KEYCLOAK_API_CLIENT_SECRET=39d5282d-3684-4026-b4ed-04bbc034b61a \ KEYCLOAK_AUTH_SERVER_CLIENT_SECRET=f605b150-7636-4447-abd3-70988786b330 \ KEYCLOAK_SERVICE_API_CLIENT_SECRET=d3724d52-34d1-4967-a802-4d178678564b \ + LAGOON_DB_VENDOR=mariadb \ + LAGOON_DB_DATABASE=infrastructure \ + LAGOON_DB_USER=api \ + LAGOON_DB_PASSWORD=api \ + LAGOON_DB_HOST=api-db \ JAVA_OPTS="-server -Xms2048m -Xmx4096m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true" VOLUME /opt/jboss/keycloak/standalone/data @@ -78,6 +89,7 @@ COPY profile.properties /opt/jboss/keycloak/standalone/configuration/profile.pro COPY configure-ds-pool.cli /opt/jboss/tools/cli/databases/configure-ds-pool.cli COPY themes/lagoon /opt/jboss/keycloak/themes/lagoon COPY --from=commons /tmp/lagoon-scripts.jar /opt/jboss/keycloak/standalone/deployments/lagoon-scripts.jar +COPY --from=builder /target/custom-protocol-mapper-1.0.0.jar /opt/jboss/keycloak/standalone/deployments/custom-protocol-mapper-1.0.0.jar ENTRYPOINT ["/sbin/tini", "--", "/lagoon/entrypoints.bash"] CMD ["-b", "0.0.0.0"] diff --git a/services/keycloak/custom-mapper/pom.xml b/services/keycloak/custom-mapper/pom.xml new file mode 100644 index 0000000000..2b6bcec1f3 --- /dev/null +++ b/services/keycloak/custom-mapper/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + net.cake.keycloak.custom + custom-protocol-mapper + 1.0.0 + jar + + + 17.0.1 + + + + + + org.keycloak + keycloak-core + ${keycloak.version} + provided + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + provided + + + org.keycloak + keycloak-server-spi-private + ${keycloak.version} + provided + + + org.keycloak + keycloak-services + ${keycloak.version} + provided + + + org.mariadb.jdbc + mariadb-java-client + LATEST + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + true + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.0 + + + + package + + shade + + + + + + + \ No newline at end of file diff --git a/services/keycloak/custom-mapper/src/main/java/CustomOIDCProtocolMapper.java b/services/keycloak/custom-mapper/src/main/java/CustomOIDCProtocolMapper.java new file mode 100644 index 0000000000..dc80053237 --- /dev/null +++ b/services/keycloak/custom-mapper/src/main/java/CustomOIDCProtocolMapper.java @@ -0,0 +1,167 @@ +import org.keycloak.models.ClientSessionContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.mappers.*; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.AccessToken; +import org.jboss.logging.Logger; + +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Iterator; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Statement; +import java.sql.PreparedStatement; + +public class CustomOIDCProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { + + public static final String PROVIDER_ID = "lagoon-search-customprotocolmapper"; + + private static final Logger logger = Logger.getLogger(CustomOIDCProtocolMapper.class); + + private static final List configProperties = new ArrayList(); + + /** + * Maybe you want to have config fields for your Mapper + */ + /* + static { + ProviderConfigProperty property; + property = new ProviderConfigProperty(); + property.setName(ProtocolMapperUtils.USER_ATTRIBUTE); + property.setLabel(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_LABEL); + property.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT); + property.setType(ProviderConfigProperty.STRING_TYPE); + configProperties.add(property); + + property = new ProviderConfigProperty(); + property.setName(ProtocolMapperUtils.MULTIVALUED); + property.setLabel(ProtocolMapperUtils.MULTIVALUED_LABEL); + property.setHelpText(ProtocolMapperUtils.MULTIVALUED_HELP_TEXT); + property.setType(ProviderConfigProperty.BOOLEAN_TYPE); + configProperties.add(property); + + } + */ + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getDisplayCategory() { + return TOKEN_MAPPER_CATEGORY; + } + + @Override + public String getDisplayType() { + return "Lagoon Project Group Mapper"; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getHelpText() { + return "A mapper that can retrieve groups and projects from the lagoon API to store in the token"; + } + + public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession keycloakSession, + UserSessionModel userSession, ClientSessionContext clientSessionCtx) { + + List groupsAndRoles = new ArrayList(); + Map groupProjectIds = new HashMap(); + Map projectGroupProjectIds = new HashMap(); + UserModel user = userSession.getUser(); + Set groups = user.getGroups(); + for (GroupModel group : groups) { + if(group.getFirstAttribute("type").equals("role-subgroup")) { + GroupModel parent = group.getParent(); + String parentName = parent.getName(); + List projectIds = new ArrayList(); + try (Connection c = DbUtil.getConnection()) { + PreparedStatement statement = c.prepareStatement("SELECT project_id FROM group_projects WHERE group_id='"+parent.getId()+"'"); + ResultSet resultSet = statement.executeQuery(); + while (resultSet.next()) { + projectIds.add(resultSet.getString(1)); + } + } + catch(Exception ex) { + // don't throw an exception, just log and continue so that the token still generates + logger.tracef("Issue connecting to database to perform query on group id %s: %v", parent.getId(), ex); + } + String temp = parent.getFirstAttribute("type"); + String result = ""; + if(temp != null && !temp.isEmpty()){ + result = temp; + } + if(result.equals("project-default-group")) { + if(projectIds != null) { + for (String projectId : projectIds) { + projectGroupProjectIds.put(projectId, parentName); + }; + } + } else { + if(projectIds != null) { + // add the group so group-tenant association works properly + groupsAndRoles.add(parentName); + // calculate the groupprojectids + for (String projectId : projectIds) { + groupProjectIds.put(projectId, parentName); + }; + } + } + } + }; + // that remains are project ids that are not already associated to an existing group + projectGroupProjectIds.keySet().removeAll(groupProjectIds.keySet()); + + for (Object projectId : projectGroupProjectIds.keySet()) { + groupsAndRoles.add("p"+projectId); + } + + // add all roles the user is part of + Set userRoles = user.getRoleMappings(); + for (RoleModel role : userRoles) { + String roleName = role.getName(); + groupsAndRoles.add(roleName); + }; + + token.getOtherClaims().put("groups", groupsAndRoles); + + setClaim(token, mappingModel, userSession, keycloakSession, clientSessionCtx); + return token; + } + + public static ProtocolMapperModel create(String name, + boolean accessToken, boolean idToken, boolean userInfo) { + ProtocolMapperModel mapper = new ProtocolMapperModel(); + mapper.setName(name); + mapper.setProtocolMapper(PROVIDER_ID); + mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + Map config = new HashMap(); + config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); + config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true"); + config.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true"); + mapper.setConfig(config); + return mapper; + } + + +} diff --git a/services/keycloak/custom-mapper/src/main/java/DbUtil.java b/services/keycloak/custom-mapper/src/main/java/DbUtil.java new file mode 100644 index 0000000000..0a54d3edd6 --- /dev/null +++ b/services/keycloak/custom-mapper/src/main/java/DbUtil.java @@ -0,0 +1,20 @@ +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import org.mariadb.jdbc.Driver; + +import org.keycloak.component.ComponentModel; + +public class DbUtil { + + public static Connection getConnection() throws SQLException{ + String driverClass = System.getenv("LAGOON_DB_VENDOR"); + String username = System.getenv("LAGOON_DB_USER"); + String password = System.getenv("LAGOON_DB_PASSWORD"); + String database = System.getenv("LAGOON_DB_DATABASE"); + String host = System.getenv("LAGOON_DB_HOST"); + + String jdbcUrl = "jdbc:"+driverClass+"://"+host+":3306/"+database+"?user="+username+"&password="+password; + return DriverManager.getConnection(jdbcUrl); + } +} \ No newline at end of file diff --git a/services/keycloak/custom-mapper/src/main/resources/META-INF/jboss-deployment-structure.xml b/services/keycloak/custom-mapper/src/main/resources/META-INF/jboss-deployment-structure.xml new file mode 100644 index 0000000000..47e418d700 --- /dev/null +++ b/services/keycloak/custom-mapper/src/main/resources/META-INF/jboss-deployment-structure.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/services/keycloak/custom-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/keycloak/custom-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper new file mode 100644 index 0000000000..c3f5a9dd43 --- /dev/null +++ b/services/keycloak/custom-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -0,0 +1 @@ +CustomOIDCProtocolMapper \ No newline at end of file diff --git a/services/keycloak/startup-scripts/00-configure-lagoon.sh b/services/keycloak/startup-scripts/00-configure-lagoon.sh index 46e89caa6c..2a65e31c04 100755 --- a/services/keycloak/startup-scripts/00-configure-lagoon.sh +++ b/services/keycloak/startup-scripts/00-configure-lagoon.sh @@ -2460,6 +2460,27 @@ function add_organization_viewall { EOF } +function migrate_to_custom_group_mapper { + local opendistro_security_client_id=$(/opt/jboss/keycloak/bin/kcadm.sh get -r lagoon clients?clientId=lagoon-opendistro-security --config $CONFIG_PATH | jq -r '.[0]["id"]') + local lagoon_opendistro_security_mappers=$(/opt/jboss/keycloak/bin/kcadm.sh get -r lagoon clients/$opendistro_security_client_id/protocol-mappers/models --config $CONFIG_PATH) + local lagoon_opendistro_security_mapper_groups=$(echo $lagoon_opendistro_security_mappers | jq -r '.[] | select(.name=="groups") | .protocolMapper') + if [ "$lagoon_opendistro_security_mapper_groups" == "lagoon-search-customprotocolmapper" ]; then + echo "custom mapper already migrated" + return 0 + fi + + echo Migrating "token mapper for search" to customer token mapper + + ################ + # Update Mapper + ################ + + local old_mapper_id=$(echo $lagoon_opendistro_security_mappers | jq -r '.[] | select(.name=="groups") | .id') + /opt/jboss/keycloak/bin/kcadm.sh delete -r lagoon clients/$opendistro_security_client_id/protocol-mappers/models/$old_mapper_id --config $CONFIG_PATH + echo '{"name":"groups","protocolMapper":"lagoon-search-customprotocolmapper","protocol":"openid-connect","config":{"id.token.claim":"true","access.token.claim":"true","userinfo.token.claim":"true","multivalued":"true","claim.name":"groups","jsonType.label":"String"}}' | /opt/jboss/keycloak/bin/kcadm.sh create -r ${KEYCLOAK_REALM:-master} clients/$opendistro_security_client_id/protocol-mappers/models --config $CONFIG_PATH -f - + +} + ################## # Initialization # ################## @@ -2510,6 +2531,7 @@ function configure_keycloak { add_development_task_cancel add_production_task_cancel add_organization_viewall + migrate_to_custom_group_mapper # always run last