From 2da4398d236b7ac0fa27185e56ef622f7c5ea8da Mon Sep 17 00:00:00 2001 From: Mike Neilson Date: Sat, 25 Oct 2025 12:35:38 -0700 Subject: [PATCH 1/3] mimic form login with oauth flow in swagger ui. --- .../opendcs/odcsapi/res/OpenDcsResource.java | 12 +++++++ .../sec/basicauth/BasicAuthResource.java | 15 +++++++-- .../sec/basicauth/CredentalReader.java | 33 +++++++++++++++++++ .../sec/basicauth/LoginProviderResource.java | 14 ++++++++ .../main/webapp/WEB-INF/app_pages/login.jsp | 8 ++--- .../main/webapp/resources/css/login-form.css | 13 +------- .../src/main/webapp/resources/css/main.css | 4 +++ .../src/main/webapp/resources/js/login.js | 13 ++++++++ 8 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/CredentalReader.java create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/LoginProviderResource.java diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java index f1eff93ad..84cfc51a1 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java @@ -23,7 +23,13 @@ import decodes.db.DatabaseIO; import decodes.tsdb.TimeSeriesDb; import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.OAuthFlow; +import io.swagger.v3.oas.annotations.security.OAuthFlows; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.security.SecuritySchemes; + import org.opendcs.database.api.OpenDcsDatabase; import org.opendcs.odcsapi.dao.OpenDcsDatabaseFactory; @@ -37,6 +43,12 @@ version = "0.0.3" ) ) +@SecuritySchemes({ +@SecurityScheme(name = "default", type = SecuritySchemeType.OAUTH2, + flows = @OAuthFlows( + password = @OAuthFlow(tokenUrl = "/odcsapi/credentials") + )) +}) public class OpenDcsResource { private static final String UNSUPPORTED_OPERATION_MESSAGE = "Endpoint is unsupported by the OpenDCS REST API."; diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java index 56ef0096d..4eddce5e7 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java @@ -20,17 +20,20 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.util.Base64; +import java.util.Map; import java.util.Set; import javax.annotation.security.RolesAllowed; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.StringToClassMapItem; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityScheme; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -38,6 +41,7 @@ import javax.sql.DataSource; import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; @@ -62,6 +66,7 @@ @Path("/") @Tag(name = "REST - Authentication and Authorization", description = "Endpoints for authentication and authorization.") + public final class BasicAuthResource extends OpenDcsResource { @@ -75,7 +80,7 @@ public final class BasicAuthResource extends OpenDcsResource @POST @Path("credentials") - @Consumes(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({ApiConstants.ODCS_API_GUEST}) @Operation( @@ -92,7 +97,7 @@ public final class BasicAuthResource extends OpenDcsResource description = "Login Credentials", required = true, content = @Content( - mediaType = MediaType.APPLICATION_JSON, + mediaType = MediaType.APPLICATION_FORM_URLENCODED, schema = @Schema(implementation = Credentials.class) ) ), @@ -131,8 +136,12 @@ public final class BasicAuthResource extends OpenDcsResource ) } ) - public Response postCredentials(Credentials credentials) throws WebAppException + public Response postCredentials(@FormParam("username") String username, + @FormParam("password") String password) throws WebAppException { + Credentials credentials = new Credentials(); + credentials.setUsername(username); + credentials.setPassword(password); TimeSeriesDb db = getLegacyTimeseriesDB(); if(!db.isOpenTSDB()) { diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/CredentalReader.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/CredentalReader.java new file mode 100644 index 000000000..6de053982 --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/CredentalReader.java @@ -0,0 +1,33 @@ +package org.opendcs.odcsapi.sec.basicauth; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; + +@Provider +public class CredentalReader implements MessageBodyReader +{ + +@Override +public boolean isReadable(Class paramClass, Type paramType, + Annotation[] paramArrayOfAnnotation, MediaType mediaType) { + return paramType == Credentials.class && mediaType.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE); +} + +@Override +public Credentials readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'readFrom'"); +} + + +} \ No newline at end of file diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/LoginProviderResource.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/LoginProviderResource.java new file mode 100644 index 000000000..8e957cd65 --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/LoginProviderResource.java @@ -0,0 +1,14 @@ +package org.opendcs.odcsapi.sec.basicauth; + +import javax.ws.rs.Path; + +import org.slf4j.Logger; + +import io.swagger.v3.oas.annotations.tags.Tag; + +@Path("/") +@Tag(name = "REST - Authentication and Authorization", description = "Endpoints for authentication and authorization.") +public class LoginProviderResource +{ + +} diff --git a/opendcs-web-client/src/main/webapp/WEB-INF/app_pages/login.jsp b/opendcs-web-client/src/main/webapp/WEB-INF/app_pages/login.jsp index fb64b0a10..641a15760 100644 --- a/opendcs-web-client/src/main/webapp/WEB-INF/app_pages/login.jsp +++ b/opendcs-web-client/src/main/webapp/WEB-INF/app_pages/login.jsp @@ -37,19 +37,19 @@ -
-
+

Login

- User Icon +
+
<% if (Objects.equals(authType, "sso") && authBasePath != null) { %> <% } else { %> @@ -61,7 +61,7 @@

<% } %> - +
diff --git a/opendcs-web-client/src/main/webapp/resources/css/login-form.css b/opendcs-web-client/src/main/webapp/resources/css/login-form.css index 8db59920f..af72e6463 100644 --- a/opendcs-web-client/src/main/webapp/resources/css/login-form.css +++ b/opendcs-web-client/src/main/webapp/resources/css/login-form.css @@ -1,5 +1,5 @@ /* - * Copyright 2023 OpenDCS Consortium + * Copyright 2023-2025 OpenDCS Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +13,6 @@ * limitations under the License. */ - -/* BASIC */ -/* -html { - background-color: #56baed; -} -*/ - a { color: #92badd; display:inline-block; @@ -292,6 +284,3 @@ input[type=password]:placeholder { outline: none; } -#icon { - width:60%; -} diff --git a/opendcs-web-client/src/main/webapp/resources/css/main.css b/opendcs-web-client/src/main/webapp/resources/css/main.css index 7374ea755..debc9bdab 100644 --- a/opendcs-web-client/src/main/webapp/resources/css/main.css +++ b/opendcs-web-client/src/main/webapp/resources/css/main.css @@ -10,6 +10,10 @@ main { height: 1.5em; } +.bi.large-icon { + font-size: 10em; +} + .mode-icon-active { width: 1.5em; height: 1.5em; diff --git a/opendcs-web-client/src/main/webapp/resources/js/login.js b/opendcs-web-client/src/main/webapp/resources/js/login.js index 794af3c44..8f2af5f55 100644 --- a/opendcs-web-client/src/main/webapp/resources/js/login.js +++ b/opendcs-web-client/src/main/webapp/resources/js/login.js @@ -31,6 +31,12 @@ */ $( document ).ready(function() { console.log("Loaded login.js."); + $.ajax({ + url: `${window.API_URL}/openapi.json`, + type: "GET", + success: (res) => createLogin(res), + error: (p) => console.log(p) + }); $(".dropdown-user").addClass("invisible"); $("#loginButton").on("click", function(e) { login(); @@ -52,6 +58,13 @@ function inputBoxLogin(event) } } +function createLogin(spec) { + console.log(spec); + + const schemes = spec.components.securitySchemes; + console.log(schemes); +} + /** * Attempts to log the user into OpenDCS using the credentials api call in OHydroJson * On success, it will set the username and token into the session storage for future use. It will bring the user to a page in decodes. From 0b06d8c4e1ee0b12a18439874050832970327170 Mon Sep 17 00:00:00 2001 From: Mike Neilson Date: Sat, 25 Oct 2025 17:45:22 -0700 Subject: [PATCH 2/3] remove http basic auth, establish openapi config modification hook. --- .../openapi/OpenDcsOpenApiModifier.java | 31 +++++ .../sec/basicauth/BasicAuthResource.java | 128 ++++++++++-------- .../sec/basicauth/CredentalReader.java | 33 ----- .../sec/basicauth/LoginProviderResource.java | 14 -- .../odcsapi/sec/openid/OidcAuthCheck.java | 1 + .../src/main/webapp/WEB-INF/web.xml | 18 +-- 6 files changed, 106 insertions(+), 119 deletions(-) create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/openapi/OpenDcsOpenApiModifier.java delete mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/CredentalReader.java delete mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/LoginProviderResource.java diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/openapi/OpenDcsOpenApiModifier.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/openapi/OpenDcsOpenApiModifier.java new file mode 100644 index 000000000..564347553 --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/openapi/OpenDcsOpenApiModifier.java @@ -0,0 +1,31 @@ +package org.opendcs.odcsapi.openapi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.swagger.v3.jaxrs2.ReaderListener; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.integration.api.OpenApiReader; +import io.swagger.v3.oas.models.OpenAPI; + +@OpenAPIDefinition +public class OpenDcsOpenApiModifier implements ReaderListener +{ + private static final Logger log = LoggerFactory.getLogger(OpenDcsOpenApiModifier.class); + @Override + public void beforeScan(OpenApiReader reader, OpenAPI openAPI) + { + /* do nothing */ + } + + @Override + public void afterScan(OpenApiReader reader, OpenAPI openAPI) + { + /** + * todo: + * 1 update authcheck provider to return security scheme with runtime determined info + * 2 add SecuritySchemes + */ + } + +} diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java index 4eddce5e7..cbfd7b786 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java @@ -78,6 +78,69 @@ public final class BasicAuthResource extends OpenDcsResource @Context private HttpHeaders httpHeaders; + @POST + @Path("credentials") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @RolesAllowed({ApiConstants.ODCS_API_GUEST}) + @Operation( + summary = "The ‘credentials’ POST method is used to obtain a new token", + description = "The user name and password provided must be a valid login for the underlying database. \n" + + "Also, that user must be assigned either of the roles OTSDB_ADMIN or OTSDB_MGR.\n" + + "--- \n\n\n" + + "Starting in **API Version 0.0.3**, authentication credentials (username and password) " + + "may be passed as shown above in the POST body. \n" + + "They may also be passed in a GET call to the 'credentials' method, " + + "(e.g. '*http://localhost:8080/odcsapi/credentials*') containing an HTTP Authentication Basic " + + "header in the form 'username:password'. \n\nThe returned data to the GET call will be empty.", + requestBody = @RequestBody( + description = "Login Credentials", + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Credentials.class) + ) + ), + responses = { + @ApiResponse( + responseCode = "200", + description = "Successful authentication." + ), + @ApiResponse( + responseCode = "400", + description = "Bad request - null or otherwise invalid credentials.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = "object", implementation = StringToClassMapItem.class), + examples = @ExampleObject(value = "{\"status\":400," + + "\"message\": \"Neither username nor password may be null.\"}")) + ), + @ApiResponse( + responseCode = "403", + description = "Invalid credentials or insufficient role.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = "object", implementation = StringToClassMapItem.class), + examples = @ExampleObject(value = "{\"status\":403," + + "\"message\":\"Failed to authorize user.\"}")) + ), + @ApiResponse( + responseCode = "500", + description = "Internal Server Error" + ), + @ApiResponse( + responseCode = "501", + description = "This authentication method is only supported by the OpenTSDB database.", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = "object", implementation = StringToClassMapItem.class), + examples = @ExampleObject(value = "{\"status\":501," + + "\"message\":\"Basic Auth is not supported.\"}")) + ) + } + ) + public Response postCredentials(Credentials credentials) throws WebAppException + { + return doLogin(credentials); + } + @POST @Path("credentials") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @@ -142,20 +205,18 @@ public Response postCredentials(@FormParam("username") String username, Credentials credentials = new Credentials(); credentials.setUsername(username); credentials.setPassword(password); - TimeSeriesDb db = getLegacyTimeseriesDB(); - if(!db.isOpenTSDB()) - { - throw new ServerErrorException("Basic Auth is not supported", Response.Status.NOT_IMPLEMENTED); - } + return doLogin(credentials); + } + private Response doLogin(Credentials credentials) throws WebAppException + { //If credentials are null, Authorization header will be checked. - if(credentials != null) + if(credentials == null) { - verifyCredentials(credentials); + throw newAuthException(); } - - String authorizationHeader = httpHeaders.getHeaderString(HttpHeaders.AUTHORIZATION); - credentials = getCredentials(credentials, authorizationHeader); + + verifyCredentials(credentials); validateDbCredentials(credentials); Set roles = getUserRoles(credentials.getUsername()); OpenDcsPrincipal principal = new OpenDcsPrincipal(credentials.getUsername(), roles); @@ -196,58 +257,11 @@ private static void verifyCredentials(Credentials credentials) } } - private static Credentials getCredentials(Credentials postBody, String authorizationHeader) - { - if(postBody != null) - { - return postBody; - } - if(authorizationHeader == null || authorizationHeader.isEmpty()) - { - throw newAuthException(); - } - return parseAuthorizationHeader(authorizationHeader); - } - private static BadRequestException newAuthException() { return new BadRequestException("Credentials not provided."); } - private static Credentials parseAuthorizationHeader(String authString) - { - String[] authHeaders = authString.split(","); - for(String header : authHeaders) - { - String trimmedHeader = header.trim(); - log.debug(MODULE + ".makeToken authHdr = {}", trimmedHeader); - if(trimmedHeader.startsWith("Basic")) - { - return extractCredentials(trimmedHeader.substring(6).trim()); - } - } - throw newAuthException(); - } - - private static Credentials extractCredentials(String base64Credentials) - { - String decodedCredentials = new String(Base64.getDecoder().decode(base64Credentials.getBytes())); - String[] parts = decodedCredentials.split(":", 2); - - if(parts.length < 2 || parts[0] == null || parts[1] == null - || parts[0].isEmpty() || parts[1].isEmpty()) - { - throw newAuthException(); - } - - Credentials credentials = new Credentials(); - credentials.setUsername(parts[0]); - credentials.setPassword(parts[1]); - - log.info(MODULE + ".checkToken found tokstr in header."); - return credentials; - } - private void validateDbCredentials(Credentials creds) throws WebAppException { diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/CredentalReader.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/CredentalReader.java deleted file mode 100644 index 6de053982..000000000 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/CredentalReader.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.opendcs.odcsapi.sec.basicauth; - -import java.io.IOException; -import java.io.InputStream; -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.ext.MessageBodyReader; -import javax.ws.rs.ext.Provider; - -@Provider -public class CredentalReader implements MessageBodyReader -{ - -@Override -public boolean isReadable(Class paramClass, Type paramType, - Annotation[] paramArrayOfAnnotation, MediaType mediaType) { - return paramType == Credentials.class && mediaType.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE); -} - -@Override -public Credentials readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, - MultivaluedMap httpHeaders, InputStream entityStream) - throws IOException, WebApplicationException { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'readFrom'"); -} - - -} \ No newline at end of file diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/LoginProviderResource.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/LoginProviderResource.java deleted file mode 100644 index 8e957cd65..000000000 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/LoginProviderResource.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.opendcs.odcsapi.sec.basicauth; - -import javax.ws.rs.Path; - -import org.slf4j.Logger; - -import io.swagger.v3.oas.annotations.tags.Tag; - -@Path("/") -@Tag(name = "REST - Authentication and Authorization", description = "Endpoints for authentication and authorization.") -public class LoginProviderResource -{ - -} diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java index 66d0c6981..c7258ac38 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java @@ -33,6 +33,7 @@ import com.nimbusds.jose.proc.BadJOSEException; import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.JWTClaimsSet; + import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; import org.opendcs.odcsapi.hydrojson.DbInterface; import org.opendcs.odcsapi.sec.AuthorizationCheck; diff --git a/opendcs-rest-api/src/main/webapp/WEB-INF/web.xml b/opendcs-rest-api/src/main/webapp/WEB-INF/web.xml index e2818f2f1..0448a3827 100644 --- a/opendcs-rest-api/src/main/webapp/WEB-INF/web.xml +++ b/opendcs-rest-api/src/main/webapp/WEB-INF/web.xml @@ -34,7 +34,9 @@ jersey.config.server.provider.packages io.swagger.jaxrs.listing, - org.opendcs.odcsapi.res + org.opendcs.odcsapi.res, + org.opendcs.odcsapi.sec, + org.opendcs.odcsapi.openapi 1 @@ -50,20 +52,6 @@ opendcs.rest.api.authorization.expiration.duration PT15M - jersey-servlet /* From 344f435aab6648a86351dd32ef46364ba828b7c0 Mon Sep 17 00:00:00 2001 From: Mike Neilson Date: Tue, 28 Oct 2025 05:04:11 -0700 Subject: [PATCH 3/3] Initial hook for providing scheme. --- .../sec/AbstractAuthorizationCheck.java | 66 +++++++++++++++++++ .../odcsapi/sec/AuthorizationCheck.java | 40 +++-------- .../odcsapi/sec/basicauth/BasicAuthCheck.java | 12 +++- .../odcsapi/sec/cwms/ServletSsoAuthCheck.java | 13 +++- .../odcsapi/sec/openid/OidcAuthCheck.java | 12 +++- 5 files changed, 109 insertions(+), 34 deletions(-) create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AbstractAuthorizationCheck.java diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AbstractAuthorizationCheck.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AbstractAuthorizationCheck.java new file mode 100644 index 000000000..b0c81d292 --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AbstractAuthorizationCheck.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 OpenDCS Consortium and its Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opendcs.odcsapi.sec; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.sql.DataSource; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.SecurityContext; + +import decodes.cwms.CwmsTimeSeriesDb; +import decodes.tsdb.TimeSeriesDb; +import opendcs.opentsdb.OpenTsdb; +import org.opendcs.database.api.OpenDcsDatabase; +import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; +import org.opendcs.odcsapi.dao.OpenDcsDatabaseFactory; +import org.opendcs.odcsapi.sec.basicauth.OpenTsdbAuthorizationDAO; +import org.opendcs.odcsapi.sec.cwms.CwmsAuthorizationDAO; + +import static org.opendcs.odcsapi.res.DataSourceContextCreator.DATA_SOURCE_ATTRIBUTE_KEY; + +public abstract class AbstractAuthorizationCheck implements AuthorizationCheck +{ + + /** + * Authorizes the current session returning the SecurityContext that will check user roles. + * + * @param requestContext context for the current session. + * @param httpServletRequest context for the current request. + */ + public abstract SecurityContext authorize(ContainerRequestContext requestContext, + HttpServletRequest httpServletRequest, ServletContext servletContext); + + public abstract boolean supports(String type, ContainerRequestContext requestContext, ServletContext servletContext); + + protected final ApiAuthorizationDAI getAuthDao(ServletContext servletContext) + { + DataSource dataSource = (DataSource) servletContext.getAttribute(DATA_SOURCE_ATTRIBUTE_KEY); + OpenDcsDatabase db = OpenDcsDatabaseFactory.createDb(dataSource); + TimeSeriesDb timeSeriesDb = db.getLegacyDatabase(TimeSeriesDb.class) + .orElseThrow(() -> new UnsupportedOperationException("Endpoint is unsupported by the OpenDCS REST API.")); + //Need to figure out a better way to extend the toolkit API to be able to add dao's within the REST API + if(timeSeriesDb instanceof CwmsTimeSeriesDb) + { + return new CwmsAuthorizationDAO(timeSeriesDb); + } + else if(timeSeriesDb instanceof OpenTsdb) + { + return new OpenTsdbAuthorizationDAO(timeSeriesDb); + } + throw new UnsupportedOperationException("Endpoint is unsupported by the OpenDCS REST API."); + } +} diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java index 66edf5f1b..e27248aef 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java @@ -17,22 +17,13 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; -import javax.sql.DataSource; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.core.SecurityContext; -import decodes.cwms.CwmsTimeSeriesDb; -import decodes.tsdb.TimeSeriesDb; -import opendcs.opentsdb.OpenTsdb; -import org.opendcs.database.api.OpenDcsDatabase; -import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; -import org.opendcs.odcsapi.dao.OpenDcsDatabaseFactory; -import org.opendcs.odcsapi.sec.basicauth.OpenTsdbAuthorizationDAO; -import org.opendcs.odcsapi.sec.cwms.CwmsAuthorizationDAO; +import io.swagger.v3.oas.models.security.SecurityScheme; -import static org.opendcs.odcsapi.res.DataSourceContextCreator.DATA_SOURCE_ATTRIBUTE_KEY; -public abstract class AuthorizationCheck +public interface AuthorizationCheck { /** @@ -41,27 +32,14 @@ public abstract class AuthorizationCheck * @param requestContext context for the current session. * @param httpServletRequest context for the current request. */ - public abstract SecurityContext authorize(ContainerRequestContext requestContext, + SecurityContext authorize(ContainerRequestContext requestContext, HttpServletRequest httpServletRequest, ServletContext servletContext); - public abstract boolean supports(String type, ContainerRequestContext requestContext, ServletContext servletContext); + boolean supports(String type, ContainerRequestContext requestContext, ServletContext servletContext); - - protected final ApiAuthorizationDAI getAuthDao(ServletContext servletContext) - { - DataSource dataSource = (DataSource) servletContext.getAttribute(DATA_SOURCE_ATTRIBUTE_KEY); - OpenDcsDatabase db = OpenDcsDatabaseFactory.createDb(dataSource); - TimeSeriesDb timeSeriesDb = db.getLegacyDatabase(TimeSeriesDb.class) - .orElseThrow(() -> new UnsupportedOperationException("Endpoint is unsupported by the OpenDCS REST API.")); - //Need to figure out a better way to extend the toolkit API to be able to add dao's within the REST API - if(timeSeriesDb instanceof CwmsTimeSeriesDb) - { - return new CwmsAuthorizationDAO(timeSeriesDb); - } - else if(timeSeriesDb instanceof OpenTsdb) - { - return new OpenTsdbAuthorizationDAO(timeSeriesDb); - } - throw new UnsupportedOperationException("Endpoint is unsupported by the OpenDCS REST API."); - } + /** + * build the OpenApi SecurityScheme to render into the runtime generated spec. + * @return + */ + SecurityScheme getOaSecurityScheme(); } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthCheck.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthCheck.java index c456c0508..7f06260c2 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthCheck.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthCheck.java @@ -26,9 +26,12 @@ import com.google.auto.service.AutoService; import decodes.tsdb.TimeSeriesDb; +import io.swagger.v3.oas.models.security.SecurityScheme; + import org.opendcs.database.api.OpenDcsDatabase; import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; import org.opendcs.odcsapi.dao.OpenDcsDatabaseFactory; +import org.opendcs.odcsapi.sec.AbstractAuthorizationCheck; import org.opendcs.odcsapi.sec.AuthorizationCheck; import org.opendcs.odcsapi.sec.OpenDcsApiRoles; import org.opendcs.odcsapi.sec.OpenDcsPrincipal; @@ -37,7 +40,7 @@ import static org.opendcs.odcsapi.res.DataSourceContextCreator.DATA_SOURCE_ATTRIBUTE_KEY; @AutoService(AuthorizationCheck.class) -public final class BasicAuthCheck extends AuthorizationCheck +public final class BasicAuthCheck extends AbstractAuthorizationCheck { @Override @@ -81,4 +84,11 @@ private Set getUserRoles(String username, ServletContext servle throw new IllegalStateException("Unable to query the database for user authorization", ex); } } + + @Override + public SecurityScheme getOaSecurityScheme() + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getOaSecurityScheme'"); + } } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/ServletSsoAuthCheck.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/ServletSsoAuthCheck.java index 79030050b..75ce4f65f 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/ServletSsoAuthCheck.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/ServletSsoAuthCheck.java @@ -27,14 +27,18 @@ import javax.ws.rs.core.Response; import com.google.auto.service.AutoService; + +import io.swagger.v3.oas.models.security.SecurityScheme; + import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; +import org.opendcs.odcsapi.sec.AbstractAuthorizationCheck; import org.opendcs.odcsapi.sec.AuthorizationCheck; import org.opendcs.odcsapi.sec.OpenDcsApiRoles; import org.opendcs.odcsapi.sec.OpenDcsPrincipal; import org.opendcs.odcsapi.sec.OpenDcsSecurityContext; @AutoService(AuthorizationCheck.class) -public final class ServletSsoAuthCheck extends AuthorizationCheck +public final class ServletSsoAuthCheck extends AbstractAuthorizationCheck { static final String SESSION_COOKIE_NAME = "JSESSIONIDSSO"; @@ -73,4 +77,11 @@ private boolean hasSessionCookie(ContainerRequestContext request) Cookie cookie = cookies.get(SESSION_COOKIE_NAME); return cookie != null; } + + @Override + public SecurityScheme getOaSecurityScheme() + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getOaSecurityScheme'"); + } } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java index c7258ac38..9a4e3d467 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java @@ -34,9 +34,12 @@ import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.JWTClaimsSet; +import io.swagger.v3.oas.models.security.SecurityScheme; + import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; import org.opendcs.odcsapi.hydrojson.DbInterface; import org.opendcs.odcsapi.sec.AuthorizationCheck; +import org.opendcs.odcsapi.sec.AbstractAuthorizationCheck; import org.opendcs.odcsapi.sec.OpenDcsApiRoles; import org.opendcs.odcsapi.sec.OpenDcsPrincipal; import org.opendcs.odcsapi.sec.OpenDcsSecurityContext; @@ -44,7 +47,7 @@ import org.opendcs.utils.logging.OpenDcsLoggerFactory; @AutoService(AuthorizationCheck.class) -public final class OidcAuthCheck extends AuthorizationCheck +public final class OidcAuthCheck extends AbstractAuthorizationCheck { private static final Logger log = OpenDcsLoggerFactory.getLogger(); @@ -128,4 +131,11 @@ public boolean supports(String type, ContainerRequestContext requestContext, Ser String authorizationHeader = requestContext.getHeaderString(AUTHORIZATION_HEADER); return keySource != null && "openid".equals(type) && authorizationHeader != null && authorizationHeader.startsWith(BEARER_PREFIX); } + + @Override + public SecurityScheme getOaSecurityScheme() + { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getOaSecurityScheme'"); + } }