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

Feature/354 openid auth #1007

Merged
merged 14 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
FROM gradle:8.5-jdk8 as builder
FROM gradle:8.5-jdk8 AS builder
USER $USER
RUN --mount=type=cache,target=/home/gradle/.gradle
WORKDIR /builddir
COPY . /builddir/
RUN gradle clean prepareDockerBuild --info --no-daemon
RUN gradle prepareDockerBuild --info --no-daemon

FROM alpine:3.21.0 as tomcat_base
FROM alpine:3.21.0 AS tomcat_base
RUN apk --no-cache upgrade && \
apk --no-cache add \
openjdk8-jre \
Expand Down Expand Up @@ -44,7 +44,7 @@ ENV cwms.dataapi.access.providers "KeyAccessManager,OpenID"
ENV cwms.dataapi.access.openid.wellKnownUrl "https://identity-test.cwbi.us/auth/realms/cwbi/.well-known/openid-configuration"
ENV cwms.dataapi.access.openid.issuer "https://identity-test.cwbi.us/auth/realms/cwbi"
ENV cwms.dataapi.access.openid.timeout "604800"
ENV cwms.dataapi.access.openid.altAuthUrl "https://identityc-test.cwbi.us/auth/realms/cwbi"
#ENV cwms.dataapi.access.openid.altAuthUrl "https://identityc-test.cwbi.us/auth/realms/cwbi"

# used to simplify redeploy in certain contexts. Update to match -<marker> in image label
ENV IMAGE_MARKER="a"
Expand Down
25 changes: 22 additions & 3 deletions compose_files/keycloak/realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"oauth2DeviceCodeLifespan": 600,
"oauth2DevicePollingInterval": 5,
"enabled": true,
"sslRequired": "external",
"sslRequired": "none",
"registrationAllowed": false,
"registrationEmailAsUsername": false,
"rememberMe": false,
Expand Down Expand Up @@ -662,7 +662,8 @@
"alwaysDisplayInConsole": true,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"cwms-data.test"
"https://cwms-data.test:8444/*",
"https://localhost:5010/*"
],
"webOrigins": [
"*"
Expand All @@ -671,7 +672,7 @@
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"implicitFlowEnabled": true,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
Expand Down Expand Up @@ -2291,6 +2292,8 @@
{
"username": "m5hectest",
"enabled": true,
"email": "[email protected]",
"emailVerified": true,
"credentials": [
{
"type": "password",
Expand All @@ -2300,6 +2303,22 @@
"realmRoles": [
"cwms_user"
]
},
{
"username": "q0hecoidc",
"enabled": true,
"email": "[email protected]",
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "q0hecoidc"
}
],
"realmRoles": [
"cwms_user",
"new_user"
]
}
]
}
1 change: 1 addition & 0 deletions compose_files/sql/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ begin
cwms_sec.add_cwms_user('m5hectest',NULL,'SWT');
cwms_sec.add_user_to_group('m5hectest','All Users', 'SWT');
cwms_sec.add_user_to_group('m5hectest','CWMS Users', 'SWT');
grant excecute on cwms_upass to web_user;
end;
/
quit;
2 changes: 2 additions & 0 deletions cwms-data-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ dependencies {
testImplementation(libs.cwms.tomcat.auth)
testImplementation(libs.apache.freemarker)

testRuntimeOnly("org.slf4j:slf4j-jdk14:2.0.16")

webjars(libs.swagger.ui) {
transitive = false
}
Expand Down
2 changes: 1 addition & 1 deletion cwms-data-api/src/main/java/cwms/cda/ApiServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ public void init() {
})
.routes(this::configureRoutes)
.javalinServlet();
logger.atInfo().log("Javalin initialized.");
}

private String obtainFullVersion(ServletConfig servletConfig) throws ServletException {
Expand All @@ -477,7 +478,6 @@ private CdaAccessManager buildAccessManager(String provider) {
} catch (ServiceNotFoundException err) {
throw new RuntimeException("Unable to initialize access manager",err);
}

}

protected void configureRoutes() {
Expand Down
82 changes: 82 additions & 0 deletions cwms-data-api/src/main/java/cwms/cda/data/dao/AuthDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
import io.javalin.core.security.RouteRole;
import io.javalin.http.Context;
import io.javalin.http.HttpCode;
import usace.cwms.db.jooq.codegen.packages.cwms_sec.UPDATE_USER_DATA;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
Expand Down Expand Up @@ -66,6 +69,13 @@ public class AuthDao extends Dao<DataApiPrincipal> {
private static final String USER_FOR_EDIPI =
"select userid from cwms_20.at_sec_cwms_users where edipi = ?";

// NOTE: the column name *should* be principal_name. It was spelled incorrectly and never changed for the life of the schema.
private static final String USER_EXISTS =
"select userid from cwms_20.at_sec_cwms_users where principle_name = ?";

private static final String ADD_CWMS_USER = "CALL cwms_20.cwms_sec.create_user(?,?,?,?)";
private static final String UPDATE_INFO = "CALL cwms_20.cwms_upass.update_user_data(?,?,null,null,null,?,?)";

public static final String CREATE_API_KEY = "insert into cwms_20.at_api_keys"
+ "(userid, key_name, apikey, created, expires) values(UPPER(?),?,?,?,?)";
public static final String REMOVE_API_KEY = "delete from cwms_20.at_api_keys "
Expand Down Expand Up @@ -243,6 +253,27 @@ private String userForEdipi(long edipi) throws CwmsAuthException {
}
}

private String userForPrincipal(String principal) throws CwmsAuthException {
try {
return dsl.connectionResult(c -> {
setSessionForAuthCheck(c);
try (PreparedStatement userForEdipi = c.prepareStatement(USER_EXISTS)) {
userForEdipi.setString(1, principal);
try (ResultSet rs = userForEdipi.executeQuery()) {
if (rs.next()) {
return rs.getString(1);
} else {
return null;
}
}
}
});
} catch (DataAccessException ex) {
logger.atInfo().withCause(ex).log("Unable to lookup user.");
throw new CwmsAuthException("Unable to lookup user.", ex);
}
}

/**
* Build a DataApiPrincipal from a given EDIPI value.
* @param edipi the Edipi value to look up.
Expand Down Expand Up @@ -493,4 +524,55 @@ public DataApiPrincipal getDataApiPrincipal(Context ctx) {
public void resetContext(DSLContext dslContext) {
this.dsl = dslContext;
}

/**
* Returns a principal from user if that user exists. otherwise empty optional
* @param principal provider + subject principal to lookup.
* @return
* @throws CwmsAuthException if anything goes wrong with the database query.
*/
public Optional<DataApiPrincipal> getPrincipalFromPrincipal(String principal) throws CwmsAuthException {
String user = userForPrincipal(principal);
if (user != null) {
Set<RouteRole> roles = this.getRolesForUser(user);
// In this case "cac_auth" just means the user is an actually user verify by some sort of
// identify management system. E.g. "not an apikey"
roles.add(new Role("cac_auth"));
return Optional.of(new DataApiPrincipal(user, roles));
} else {
return Optional.empty();
}
}


public DataApiPrincipal createUser(String username, String principal, String fullname, String email) throws CwmsAuthException {
try {
dsl.connection(c -> {
setSessionForAuthCheck(c);
try (PreparedStatement createUser = c.prepareStatement(ADD_CWMS_USER);
PreparedStatement updateData = c.prepareStatement(UPDATE_INFO)) {
createUser.setString(1, username);
createUser.setNull(2, Types.VARCHAR);
createUser.setNull(3, Types.ARRAY, "CWMS_T_CHAR_32_ARRAY");
createUser.setNull(4, Types.VARCHAR);
createUser.execute();

updateData.setString(1, username);
updateData.setString(2,fullname);
updateData.setString(3, email);
updateData.setString(4, principal);
updateData.execute();
}
});
Optional<DataApiPrincipal> apiPrincipal = getPrincipalFromPrincipal(principal);
if (apiPrincipal.isPresent()) {
return apiPrincipal.get();
} else {
throw new CwmsAuthException("User " + username + " was created, however no principal object could be created.");
}
} catch (DataAccessException ex) {
logger.atInfo().withCause(ex).log("Unable to create user " + username);
throw new CwmsAuthException("Unable to create user " + username, ex);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.util.Base64.Decoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
Expand All @@ -42,6 +43,8 @@
public class OpenIDAccessManager extends CdaAccessManager {
private static final FluentLogger log = FluentLogger.forEnclosingClass();
public static final String AUTHORIZATION = "Authorization";
public static final String CREATE_USERS_KEY = "cda.openid.create_users";
private static final boolean createUsers = true;// Boolean.parseBoolean(System.getProperty(CREATE_USERS_KEY,"false"));
private JwtParser jwtParser = null;
private OpenIDConfig config = null;

Expand Down Expand Up @@ -69,15 +72,26 @@ public void manage(Handler handler, @NotNull Context ctx, @NotNull Set<RouteRole
private DataApiPrincipal getUserFromToken(Context ctx) throws CwmsAuthException {
try {
Jws<Claims> token = jwtParser.parseClaimsJws(getToken(ctx));
String username = token.getBody().get("preferred_username",String.class);
Claims claims = token.getBody();
final String issuer = claims.getIssuer();
final String subject = claims.getSubject();
final String username = issuer + "::" + subject;
AuthDao dao = AuthDao.getInstance(JooqDao.getDslContext(ctx),ctx.attribute(ApiServlet.OFFICE_ID));
String edipiStr = username.substring(username.lastIndexOf(".") + 1);
long edipi = Long.parseLong(edipiStr);
return dao.getPrincipalFromEdipi(edipi);
Optional<DataApiPrincipal> principal = dao.getPrincipalFromPrincipal(username);
if (principal.isPresent()) {
return principal.get();
} else if (createUsers) {
final String preferredUserName = claims.get("preferred_username", String.class);
final String givenName = claims.get("given_name", String.class);
final String email = claims.get("email", String.class);
return dao.createUser(preferredUserName,username,givenName, email);
} else {
throw new CwmsAuthException("Not Authorized",HttpServletResponse.SC_UNAUTHORIZED);
}
} catch (NumberFormatException | JwtException ex) {
throw new CwmsAuthException("JWT not valid",ex,HttpServletResponse.SC_UNAUTHORIZED);
}
}
}

private String getToken(Context ctx) {
String header = ctx.header(AUTHORIZATION);
Expand Down
2 changes: 2 additions & 0 deletions cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import cwms.cda.data.dto.LocationGroup;
import fixtures.CwmsDataApiSetupCallback;
import fixtures.IntegrationTestNameGenerator;
import fixtures.KeyCloakExtension;
import fixtures.TestAccounts;
import fixtures.users.MockCwmsUserPrincipalImpl;
import java.io.File;
Expand Down Expand Up @@ -74,6 +75,7 @@
*/
@DisplayNameGeneration(IntegrationTestNameGenerator.class)
@Tag("integration")
@ExtendWith(KeyCloakExtension.class)
@ExtendWith(CwmsDataApiSetupCallback.class)
public class DataApiTestIT {
private static FluentLogger logger = FluentLogger.forEnclosingClass();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cwms.cda.api.auth;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Optional;
import java.util.logging.Logger;


import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import cwms.cda.api.DataApiTestIT;
import fixtures.KeyCloakExtension;
import io.javalin.http.HttpCode;
import io.restassured.filter.log.LogDetail;
import io.restassured.http.ContentType;

@Tag("integration")
@ExtendWith(KeyCloakExtension.class)
public class OpenIdConnectTestIT extends DataApiTestIT {
private static final Logger logger = Logger.getLogger(OpenIdConnectTestIT.class.getName());


@Test
void test_keycloak_user_is_created() {

Optional<String> token = KeyCloakExtension.tokenForUser("q0hecoidc", "q0hecoidc");
assertTrue(token.isPresent());

given()
.log().ifValidationFails(LogDetail.ALL, true)
.header("Authorization", "Bearer " + token.get())
.queryParam("name-mask","asdf")
.when()
.get("/properties")
.then()
.log().ifValidationFails(LogDetail.ALL,true)
// 403 here means the user was created, but has no privileges as the user was just created.
.statusCode(is(HttpCode.FORBIDDEN.getStatus()));
}

@Test
void test_keycloak_user_can_operate() {
Optional<String> token = KeyCloakExtension.tokenForUser("m5hectest", "m5hectest");
assertTrue(token.isPresent());

given()
.log().ifValidationFails(LogDetail.ALL, true)
.header("Authorization", "Bearer " + token.get())
.queryParam("name-mask","asdf")
.when()
.get("/properties")
.then()
.log().ifValidationFails(LogDetail.ALL,true)
// 403 here means the user was created, but has no privileges as the user was just created.
.statusCode(is(HttpCode.OK.getStatus()));
}
}
Loading
Loading