Skip to content

Commit

Permalink
feat: add a custom token mapper to get project ids from api-db
Browse files Browse the repository at this point in the history
  • Loading branch information
shreddedbacon committed Dec 6, 2023
1 parent 5cd4871 commit 2835fb4
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions services/keycloak/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"]
78 changes: 78 additions & 0 deletions services/keycloak/custom-mapper/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>net.cake.keycloak.custom</groupId>
<artifactId>custom-protocol-mapper</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<properties>
<keycloak.version>17.0.1</keycloak.version>
</properties>

<dependencies>
<!-- provided keycloak dependencies -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>LATEST</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<forceJavacCompilerUse>true</forceJavacCompilerUse>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<executions>
<!-- Run shade goal on package phase -->
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();

/**
* 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<ProviderConfigProperty> 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<Object> groupsAndRoles = new ArrayList();
Map<String, String> groupProjectIds = new HashMap();
Map<String, String> projectGroupProjectIds = new HashMap();
UserModel user = userSession.getUser();
Set<GroupModel> groups = user.getGroups();
for (GroupModel group : groups) {
if(group.getFirstAttribute("type").equals("role-subgroup")) {
GroupModel parent = group.getParent();
String parentName = parent.getName();
List<String> 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<RoleModel> 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<String, String> config = new HashMap<String, String>();
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;
}


}
20 changes: 20 additions & 0 deletions services/keycloak/custom-mapper/src/main/java/DbUtil.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<jboss-deployment-structure>
<deployment>
<dependencies>
<module name="org.keycloak.keycloak-services" />
</dependencies>
</deployment>
</jboss-deployment-structure>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CustomOIDCProtocolMapper
22 changes: 22 additions & 0 deletions services/keycloak/startup-scripts/00-configure-lagoon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
##################
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 2835fb4

Please sign in to comment.