From 2f116f7b73aa6f92efd4d77e6fc012e5dbf204da Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Mon, 17 Nov 2025 21:35:08 +0200 Subject: [PATCH 1/3] adding 1.1 --- .classpath | 35 +++++--- .project | 11 +++ .settings/org.eclipse.core.resources.prefs | 4 +- .settings/org.eclipse.jdt.core.prefs | 14 +++- pom.xml | 2 +- .../mcp/server/OSLMCPServerApplication.java | 10 +-- .../osl/mcp/server/ServiceCatalogQClient.java | 2 +- .../osl/mcp/server/ServiceCatalogTools.java | 84 ++++++++++--------- src/main/resources/application.yaml | 1 + 9 files changed, 104 insertions(+), 59 deletions(-) diff --git a/.classpath b/.classpath index e7c4f49..3261bfe 100644 --- a/.classpath +++ b/.classpath @@ -1,37 +1,50 @@ - + + + + + + + + - + + - + - - + - + + + + + + - + - + - + - + - + + diff --git a/.project b/.project index ec91655..7ae1829 100644 --- a/.project +++ b/.project @@ -20,4 +20,15 @@ org.eclipse.jdt.core.javanature org.eclipse.m2e.core.maven2Nature + + + 1747415794116 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs index 29abf99..742ce1f 100644 --- a/.settings/org.eclipse.core.resources.prefs +++ b/.settings/org.eclipse.core.resources.prefs @@ -1,6 +1,6 @@ eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 +encoding//src/main/java=utf-8 encoding//src/main/resources=UTF-8 -encoding//src/test/java=UTF-8 +encoding//src/test/java=utf-8 encoding//src/test/resources=UTF-8 encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 3328195..1c94aa9 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,9 @@ eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.springframework.lang.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.springframework.lang.NonNullApi +org.eclipse.jdt.core.compiler.annotation.nullable=org.springframework.lang.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.methodParameters=generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 @@ -11,6 +16,13 @@ org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning -org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=17 diff --git a/pom.xml b/pom.xml index 07b9123..8bed96d 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ UTF-8 UTF-8 3.4.5 - 1.0.0-M7 + 1.1.0 1.18.28 2.1.0 1.5.3.Final diff --git a/src/main/java/org/etsi/osl/mcp/server/OSLMCPServerApplication.java b/src/main/java/org/etsi/osl/mcp/server/OSLMCPServerApplication.java index 7d75775..5808501 100644 --- a/src/main/java/org/etsi/osl/mcp/server/OSLMCPServerApplication.java +++ b/src/main/java/org/etsi/osl/mcp/server/OSLMCPServerApplication.java @@ -35,11 +35,11 @@ public class OSLMCPServerApplication { SpringApplication.run(OSLMCPServerApplication.class, args); } - @Bean - public ToolCallbackProvider serviceTools( ServiceCatalogTools oslServices) { - return MethodToolCallbackProvider.builder().toolObjects( oslServices ).build(); - } - +// @Bean +// public ToolCallbackProvider serviceTools( ServiceCatalogTools oslServices) { +// return MethodToolCallbackProvider.builder().toolObjects( oslServices ).build(); +// } +// @Bean diff --git a/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogQClient.java b/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogQClient.java index a9756af..a08fd4b 100644 --- a/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogQClient.java +++ b/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogQClient.java @@ -107,7 +107,7 @@ public class ServiceCatalogQClient extends RouteBuilder { * @throws IOException */ public ServiceSpecification retrieveServiceSpec(String specid) { - logger.info("will retrieve Service Specification from catalog orderid=" + specid ); + logger.info("will retrieve Service Specification from catalog specid=" + specid ); try { Object response = template. diff --git a/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java b/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java index 0508f40..ccd60f8 100644 --- a/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java +++ b/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java @@ -2,14 +2,8 @@ package org.etsi.osl.mcp.server; import java.io.IOException; import java.time.OffsetDateTime; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.etsi.osl.tmf.common.model.Any; import org.etsi.osl.tmf.common.model.service.Characteristic; import org.etsi.osl.tmf.common.model.service.Note; @@ -17,10 +11,8 @@ import org.etsi.osl.tmf.common.model.service.ServiceSpecificationRef; import org.etsi.osl.tmf.prm669.model.RelatedParty; import org.etsi.osl.tmf.rcm634.model.LogicalResourceSpecification; import org.etsi.osl.tmf.ri639.model.Resource; -import org.etsi.osl.tmf.scm633.model.ServiceCandidate; import org.etsi.osl.tmf.scm633.model.ServiceCatalog; import org.etsi.osl.tmf.scm633.model.ServiceCategory; -import org.etsi.osl.tmf.scm633.model.ServiceCategoryRef; import org.etsi.osl.tmf.scm633.model.ServiceSpecification; import org.etsi.osl.tmf.sim638.model.Service; import org.etsi.osl.tmf.sim638.model.ServiceUpdate; @@ -31,14 +23,12 @@ import org.etsi.osl.tmf.so641.model.ServiceOrderStateType; import org.etsi.osl.tmf.so641.model.ServiceRestriction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.chat.model.ToolContext; -import org.springframework.ai.tool.annotation.Tool; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.context.McpSyncRequestContext; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.ParameterizedTypeReference; -//import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.client.RestClient; -import jakarta.validation.Valid; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; /** * @@ -57,14 +47,14 @@ public class ServiceCatalogTools { ServiceCatalogQClient aCatalogClient; -// @Tool(description="Get your name") +// @McpTool(description="Get your name") // public String getYourName(ToolContext context) { // logger.info("ToolContext: {}", McpRequestHolder.get(context).headers()); // return SecurityContextHolder.getContext().getAuthentication().getName(); // } - @Tool(description = "Get a list of all published OSL OpenSlice service catalogs." + @McpTool (description = "Get a list of all published OSL OpenSlice service catalogs." + "Each catalog contains service categories, that we can search individually to get the details and contents of each category.") public JsonNode getOSLServiceCatalogs() { @@ -89,8 +79,8 @@ public class ServiceCatalogTools { } - @Tool(description = "Get OSL categories in catalog providing a catalog name") - public JsonNode getOSLServiceCategories(String catalogName) { + @McpTool(description = "Get OSL categories in catalog providing a catalog name") + public JsonNode getOSLServiceCategories(@McpToolParam(description = "The service catalog name", required = true) String catalogName) { logger.info("getOSLServiceCategories {}", catalogName); @@ -113,8 +103,8 @@ public class ServiceCatalogTools { } - @Tool(description = "Get a list of OSL service specification references in a service category, given a category ID") - public JsonNode getOSLServiceSpecsInCategory(String categoryId) { + @McpTool(description = "Get a list of OSL service specification references in a service category, given a category ID") + public JsonNode getOSLServiceSpecsInCategory(@McpToolParam(description = "The categoryId needed to search service specification references", required = true) String categoryId) { logger.info("getOSLServiceSpecsInCategory {}", categoryId); @@ -141,8 +131,9 @@ public class ServiceCatalogTools { } - @Tool(description = "Get all the details of an OSL service specification given a service Specification Id") - public JsonNode getOSLServiceSpecificationByServiceSpecificationId(String serviceSpecId) { + @McpTool(description = "Get all the details of an OSL service specification given a service Specification Id") + public JsonNode getOSLServiceSpecificationByServiceSpecificationId( + @McpToolParam(description = "The service Specification Id needed to get service specification details", required = true) String serviceSpecId) { logger.info("getOSLServiceByServiceSpecificationId {}", serviceSpecId); @@ -166,8 +157,9 @@ public class ServiceCatalogTools { - @Tool(description = "Get all the details of an OSL resource specification given a resource Specification Id") - public JsonNode getOSLResourceSpecificationByResourceSpecificationId(String resourceSpecId) { + @McpTool(description = "Get all the details of an OSL resource specification given a resource Specification Id") + public JsonNode getOSLResourceSpecificationByResourceSpecificationId( + @McpToolParam(description = "The resource Specification Id needed to get resource specification details", required = true) String resourceSpecId) { logger.info("getOSLResourceSpecificationByResourceSpecificationId {}", resourceSpecId); @@ -190,13 +182,22 @@ public class ServiceCatalogTools { - @Tool(description = "Search for OSL service specifications that are published and available for service ordering in all categories") - public JsonNode searchOSLServiceSpecifications( List searchStrings) { - + @McpTool(description = "Search for OSL service specifications that are published and available for service ordering in all categories") + public JsonNode searchOSLServiceSpecifications( + McpSyncRequestContext context, + @McpToolParam(description = "A list of search strings", required = true) List searchStrings) { + + // Send logging notification + context.info("Processing data: " + searchStrings); + // Send progress notification (using convenient method) + context.progress(p -> p.progress(0.5).total(1.0).message("Processing...")); logger.info("searchOSLServiceSpecifications containing words: {}", searchStrings); List spec = aCatalogClient.searchServiceSpecs( searchStrings ); + // Ping the client + context.ping(); + // Filter and get result as JSON string try { @@ -213,10 +214,14 @@ public class ServiceCatalogTools { return rootNode; } - @Tool(description = "Create a service order given a Service Specification id, the Start date an end date of the order. " + @McpTool(description = "Create a service order given a Service Specification id, the Start date an end date of the order. " + "The user can provide also characteristics of service in the map with format key, value" + "Date Time has the format YYYY-MM-DDTHH:mm:ss+00:00") - public String createServiceOrder(String serviceSpecId, String startDate, String endDate, Map characteristics) { + public String createServiceOrder( + @McpToolParam(description = "The Service Specification id", required = true) String serviceSpecId, + @McpToolParam(description = "The start date", required = true) String startDate, + @McpToolParam(description = "Then end date", required = true) String endDate, + @McpToolParam(description = "A list of characteristics, key=characteristic name, value=characterisitc value", required = true) Map characteristics) { logger.info("createServiceOrder {} {} {} {}", serviceSpecId, startDate, endDate, characteristics.toString()); @@ -295,12 +300,13 @@ public class ServiceCatalogTools { } - @Tool(description = "Provide details for a service order given a Service Order id. " + @McpTool(description = "Provide details for a service order given a Service Order id. " + "Focus attention to:" + "- the state of the service order" + "- and each order item. Especially for each order item focus to the service and especially: status, characteristics and supporting services." + "- For each supporting service we can retrieve more information by using the service id.") - public JsonNode getServiceOrder(String serviceOrderId) { + public JsonNode getServiceOrder( + @McpToolParam(description = "The Service Order id", required = true) String serviceOrderId) { logger.info("serviceOrderId {} {} {} {}", serviceOrderId); ServiceOrder so = aCatalogClient.retrieveServiceOrder(serviceOrderId); @@ -324,10 +330,10 @@ public class ServiceCatalogTools { } - @Tool(description = "Provide details for a service given a Service id. Especially for name, state, characteristics, supporting Services and supporting Resources." + @McpTool(description = "Provide details for a service given a Service id. Especially for name, state, characteristics, supporting Services and supporting Resources." + "We can get details for each supporting service using the service id." + "and for each supporting resource using the resource id.") - public JsonNode getService(String serviceId) { + public JsonNode getService(@McpToolParam(description = "The Service id", required = true) String serviceId) { logger.info("getService {} {} {} {}", serviceId); Service s = aCatalogClient.retrieveService(serviceId); // Filter and get result as JSON string @@ -348,9 +354,9 @@ public class ServiceCatalogTools { } - @Tool(description = "Provide details for a resource given a Resourceid. Especially for name, status, characteristics." + @McpTool(description = "Provide details for a resource given a Resourceid. Especially for name, status, characteristics." + "We can get details for each supporting resource using the resource id.") - public JsonNode getResource(String resourceId) { + public JsonNode getResource(@McpToolParam(description = "The Resource id", required = true) String resourceId) { logger.info("getResource {} {} {} {}", resourceId); Resource r = aCatalogClient.retrieveResource ( resourceId); // Filter and get result as JSON string @@ -371,9 +377,11 @@ public class ServiceCatalogTools { } - @Tool(description = "Update and change a service given a Service id." + @McpTool(description = "Update and change a service given a Service id." + "We provide also characteristics of service in the map with format key, value") - public String updateService(String serviceId, Map characteristics) { + public String updateService( + @McpToolParam(description = "The Service id", required = true) String serviceId, + @McpToolParam(description = "A list of characteristics, key=characteristic name, value=characterisitc value", required = true) Map characteristics) { logger.info("updateService {} {} {} {}", serviceId); ServiceUpdate su = new ServiceUpdate(); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d31c35d..5b82158 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -12,6 +12,7 @@ spring: name: org.etsi.osl.mcp.server version: 0.0.1 type: SYNC + protocol: STREAMABLE sse-message-endpoint: /mcp/messages stdio: false resource-change-notification: true -- GitLab From aeebf541e88b75f78bd594a77f158256428cfcb6 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Mon, 17 Nov 2025 23:03:40 +0200 Subject: [PATCH 2/3] fix search --- .../osl/mcp/server/ProductCatalogTools.java | 33 ++++++++++++++++--- .../osl/mcp/server/ServiceCatalogTools.java | 24 ++++++++++++-- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/etsi/osl/mcp/server/ProductCatalogTools.java b/src/main/java/org/etsi/osl/mcp/server/ProductCatalogTools.java index 8d96645..d3ae320 100644 --- a/src/main/java/org/etsi/osl/mcp/server/ProductCatalogTools.java +++ b/src/main/java/org/etsi/osl/mcp/server/ProductCatalogTools.java @@ -26,6 +26,8 @@ import org.etsi.osl.tmf.prm669.model.RelatedParty; import org.etsi.osl.tmf.ri639.model.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springaicommunity.mcp.context.McpSyncRequestContext; import org.springframework.ai.tool.annotation.Tool; import org.springframework.beans.factory.annotation.Autowired; @@ -167,12 +169,35 @@ public class ProductCatalogTools { } - @Tool(description = "Search for OSL product Offerings that are published and available for product ordering in all categories") - public JsonNode searchOSLProductOfferings( List searchStrings) { + @McpTool(description = "Search for OSL product Offerings that are published and available for product ordering in all categories") + public JsonNode searchOSLProductOfferings( + McpSyncRequestContext context,List searchStrings) { - logger.info("searchOSLProductOfferings containing workds: {}", searchStrings); + // Send logging notification + context.info("Processing data: " + searchStrings); + // Send progress notification (using convenient method) + context.progress(p -> p.progress(0.5).total(1.0).message("Processing...")); + logger.info("searchOSLServiceSpecifications containing words: {}", searchStrings); + + // Split strings that contain multiple words separated by comma or space + List expandedSearchStrings = new java.util.ArrayList<>(); + for (String searchString : searchStrings) { + if (searchString.contains(",") || searchString.contains(" ")) { + // Split by comma or space and add each word separately + String[] words = searchString.split("[,\\s]+"); + for (String word : words) { + String trimmedWord = word.trim(); + if (!trimmedWord.isEmpty()) { + expandedSearchStrings.add(trimmedWord); + } + } + } else { + expandedSearchStrings.add(searchString); + } + } + logger.info("Expanded search strings: {}", expandedSearchStrings); - List spec = aCatalogClient.searchProductOfferings( searchStrings ); + List spec = aCatalogClient.searchProductOfferings( expandedSearchStrings ); // Filter and get result as JSON string try { diff --git a/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java b/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java index ccd60f8..9786627 100644 --- a/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java +++ b/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java @@ -183,17 +183,35 @@ public class ServiceCatalogTools { @McpTool(description = "Search for OSL service specifications that are published and available for service ordering in all categories") - public JsonNode searchOSLServiceSpecifications( + public JsonNode searchOSLServiceSpecifications( McpSyncRequestContext context, @McpToolParam(description = "A list of search strings", required = true) List searchStrings) { - + // Send logging notification context.info("Processing data: " + searchStrings); // Send progress notification (using convenient method) context.progress(p -> p.progress(0.5).total(1.0).message("Processing...")); logger.info("searchOSLServiceSpecifications containing words: {}", searchStrings); - List spec = aCatalogClient.searchServiceSpecs( searchStrings ); + // Split strings that contain multiple words separated by comma or space + List expandedSearchStrings = new java.util.ArrayList<>(); + for (String searchString : searchStrings) { + if (searchString.contains(",") || searchString.contains(" ")) { + // Split by comma or space and add each word separately + String[] words = searchString.split("[,\\s]+"); + for (String word : words) { + String trimmedWord = word.trim(); + if (!trimmedWord.isEmpty()) { + expandedSearchStrings.add(trimmedWord); + } + } + } else { + expandedSearchStrings.add(searchString); + } + } + logger.info("Expanded search strings: {}", expandedSearchStrings); + + List spec = aCatalogClient.searchServiceSpecs( expandedSearchStrings ); // Ping the client context.ping(); -- GitLab From 58c06fb1ef3ad13f235ff799c47574d8b6299820 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Mon, 17 Nov 2025 23:59:07 +0200 Subject: [PATCH 3/3] adding oauth2.0 security --- pom.xml | 25 +- .../osl/mcp/server/ServiceCatalogTools.java | 19 +- .../mcp/server/WebSecurityConfigKeycloak.java | 335 ++++++++++++++++++ src/main/resources/application.yaml | 10 +- 4 files changed, 377 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/etsi/osl/mcp/server/WebSecurityConfigKeycloak.java diff --git a/pom.xml b/pom.xml index 8bed96d..97bd890 100644 --- a/pom.xml +++ b/pom.xml @@ -98,15 +98,32 @@ org.springframework.ai - spring-ai-starter-mcp-server-webflux + spring-ai-starter-mcp-server-webmvc + + org.springaicommunity + mcp-server-security + 0.0.4 + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + - - + + com.jayway.jsonpath + json-path + org.projectlombok @@ -133,8 +150,6 @@ - - diff --git a/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java b/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java index 9786627..10b27d9 100644 --- a/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java +++ b/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java @@ -27,6 +27,8 @@ import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpToolParam; import org.springaicommunity.mcp.context.McpSyncRequestContext; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -61,7 +63,10 @@ public class ServiceCatalogTools { logger.info("getOSLServiceCatalogs"); List serviceCatalogs = aCatalogClient.retrieveServiceCatalogs(); - + var authentication = SecurityContextHolder.getContext().getAuthentication(); + var name = authentication.getName(); + + logger.info("getOSLServiceCatalogs {}", name); // Filter and get result as JSON string try { @@ -232,6 +237,7 @@ public class ServiceCatalogTools { return rootNode; } + @PreAuthorize("isAuthenticated()") @McpTool(description = "Create a service order given a Service Specification id, the Start date an end date of the order. " + "The user can provide also characteristics of service in the map with format key, value" + "Date Time has the format YYYY-MM-DDTHH:mm:ss+00:00") @@ -241,7 +247,10 @@ public class ServiceCatalogTools { @McpToolParam(description = "Then end date", required = true) String endDate, @McpToolParam(description = "A list of characteristics, key=characteristic name, value=characterisitc value", required = true) Map characteristics) { - logger.info("createServiceOrder {} {} {} {}", serviceSpecId, startDate, endDate, characteristics.toString()); + var authentication = SecurityContextHolder.getContext().getAuthentication(); + var username = authentication.getName(); + + logger.info("createServiceOrder {} {} {} {} {}", username, serviceSpecId, startDate, endDate, characteristics.toString()); ServiceOrderCreate sonew = new ServiceOrderCreate(); @@ -267,16 +276,14 @@ public class ServiceCatalogTools { if (sonew.getRelatedParty() == null) { RelatedParty rp = new RelatedParty(); - rp.setName("MCP"); + rp.setName(username); rp.setRole("REQUESTER"); sonew.addRelatedPartyItem(rp); } if (sonew.getNote() == null) { Note n = new Note(); - - n.setText( "Order created by MCP"); - + n.setText( "Order created by MCP for user: " + username); sonew.addNoteItem(n); } diff --git a/src/main/java/org/etsi/osl/mcp/server/WebSecurityConfigKeycloak.java b/src/main/java/org/etsi/osl/mcp/server/WebSecurityConfigKeycloak.java new file mode 100644 index 0000000..5865553 --- /dev/null +++ b/src/main/java/org/etsi/osl/mcp/server/WebSecurityConfigKeycloak.java @@ -0,0 +1,335 @@ +package org.etsi.osl.mcp.server; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.PathNotFoundException; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationManagerResolver; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoders; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.stereotype.Component; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@Profile("!testing") +public class WebSecurityConfigKeycloak { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http, ServerProperties serverProperties, + @Value("${origins:[]}") String[] origins, @Value("${permit-all:[]}") String[] permitAll, + AuthenticationManagerResolver authenticationManagerResolver) throws Exception { + + http.oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver)); + + // Enable and configure CORS + http.cors(cors -> cors.configurationSource(corsConfigurationSource(origins))); + + // State-less session (state in access-token only) + http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + // Disable CSRF because of state-less session-management + http.csrf(csrf -> csrf.disable()); + + // Return 401 (unauthorized) instead of 302 (redirect to login) when + // authorization is missing or invalid + http.exceptionHandling(eh -> eh.authenticationEntryPoint((request, response, authException) -> { + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\""); + response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); + })); + + // If SSL enabled, disable http (https only) + if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) { + http.requiresChannel(channel -> channel.anyRequest().requiresSecure()); + } + + // @formatter:off + http.authorizeHttpRequests(requests -> requests + //.requestMatchers(permitAll).permitAll() + .anyRequest().permitAll()); + // @formatter:on + + return http.build(); + } + + private UrlBasedCorsConfigurationSource corsConfigurationSource(String[] origins) { + final var configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList(origins)); + configuration.setAllowedMethods(List.of("*")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setExposedHeaders(List.of("*")); + configuration.setAllowCredentials(true); + + final var source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Data + @Configuration + @ConfigurationProperties(prefix = "spring-addons") + static class SpringAddonsProperties { + private IssuerProperties[] issuers = {}; + + @Data + static class IssuerProperties { + private URL uri; + + @NestedConfigurationProperty + private ClaimMappingProperties[] claims; + + private String usernameJsonPath = JwtClaimNames.SUB; + + @Data + static class ClaimMappingProperties { + private String jsonPath; + private CaseProcessing caseProcessing = CaseProcessing.UNCHANGED; + private String prefix = ""; + + static enum CaseProcessing { + UNCHANGED, TO_LOWER, TO_UPPER + } + } + } + + public IssuerProperties get(URL issuerUri) throws MisconfigurationException { + final var issuerProperties = Stream.of(issuers).filter(iss -> issuerUri.toString().equals(iss.getUri().toString())).toList(); + if (issuerProperties.size() == 0) { + throw new MisconfigurationException( + "Missing authorities mapping properties for %s".formatted(issuerUri.toString())); + } + if (issuerProperties.size() > 1) { + throw new MisconfigurationException( + "Too many authorities mapping properties for %s".formatted(issuerUri.toString())); + } + return issuerProperties.get(0); + } + + static class MisconfigurationException extends RuntimeException { + private static final long serialVersionUID = 5887967904749547431L; + + public MisconfigurationException(String msg) { + super(msg); + } + } + } + + @RequiredArgsConstructor + static class JwtGrantedAuthoritiesConverter implements Converter> { + private final SpringAddonsProperties.IssuerProperties properties; + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Collection convert(Jwt jwt) { + + + return Stream.of(properties.claims).flatMap(claimProperties -> { + Object claim; + try { + claim = JsonPath.read(jwt.getClaims(), claimProperties.jsonPath); + } catch (PathNotFoundException e) { + claim = null; + } + if (claim == null) { + return Stream.empty(); + } + if (claim instanceof String claimStr) { + return Stream.of(claimStr.split(",")); + } + if (claim instanceof String[] claimArr) { + return Stream.of(claimArr); + } + if (Collection.class.isAssignableFrom(claim.getClass())) { + final var iter = ((Collection) claim).iterator(); + if (!iter.hasNext()) { + return Stream.empty(); + } + final var firstItem = iter.next(); + if (firstItem instanceof String) { + return (Stream) ((Collection) claim).stream(); + } + if (Collection.class.isAssignableFrom(firstItem.getClass())) { + return (Stream) ((Collection) claim).stream() + .flatMap(colItem -> ((Collection) colItem).stream()).map(String.class::cast); + } + } + return Stream.empty(); + }) /* Insert some transformation here if you want to add a prefix like "ROLE_" or force upper-case authorities */ + + .map(s -> "ROLE_" + s) + .map(SimpleGrantedAuthority::new) + .map(GrantedAuthority.class::cast).toList(); + } + } + + @Component + @RequiredArgsConstructor + static class SpringAddonsJwtAuthenticationConverter implements Converter { + private final SpringAddonsProperties springAddonsProperties; + + @Override + public AbstractAuthenticationToken convert(Jwt jwt) { + final var issuerProperties = springAddonsProperties.get(jwt.getIssuer()); + final var authorities = new JwtGrantedAuthoritiesConverter(issuerProperties).convert(jwt); + final String username = JsonPath.read(jwt.getClaims(), issuerProperties.getUsernameJsonPath()); + return new JwtAuthenticationToken(jwt, authorities, username); + } + } + + @Bean + AuthenticationManagerResolver authenticationManagerResolver( + SpringAddonsProperties addonsProperties, SpringAddonsJwtAuthenticationConverter authenticationConverter) { + final Map authenticationProviders = Stream.of(addonsProperties.getIssuers()) + .map(SpringAddonsProperties.IssuerProperties::getUri).map(URL::toString) + .collect(Collectors.toMap(issuer -> issuer, + issuer -> authenticationProvider(issuer, authenticationConverter)::authenticate)); + return new JwtIssuerAuthenticationManagerResolver( + (AuthenticationManagerResolver) authenticationProviders::get); + } + + JwtAuthenticationProvider authenticationProvider(String issuer, + SpringAddonsJwtAuthenticationConverter authenticationConverter) { + JwtDecoder decoder = JwtDecoders.fromIssuerLocation(issuer); + var provider = new JwtAuthenticationProvider(decoder); + provider.setJwtAuthenticationConverter(authenticationConverter); + return provider; + } +} + +//@Configuration +//@EnableWebSecurity +//@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class) +//@Profile("!testing") +//public class WebSecurityConfigKeycloak extends KeycloakWebSecurityConfigurerAdapter { +// +// +// +// @Autowired +// private RestAuthenticationEntryPoint restAuthenticationEntryPoint; +// +// @Autowired +// public void configureGlobal( +// AuthenticationManagerBuilder auth) throws Exception { +// +// KeycloakAuthenticationProvider keycloakAuthenticationProvider +// = keycloakAuthenticationProvider(); +// keycloakAuthenticationProvider.setGrantedAuthoritiesMapper( +// new SimpleAuthorityMapper()); +// auth.authenticationProvider(keycloakAuthenticationProvider); +// } +// +// @Bean +// public KeycloakSpringBootConfigResolver KeycloakConfigResolver() { +// return new KeycloakSpringBootConfigResolver(); +// } +// +// @Bean +// @Override +// protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { +// return new RegisterSessionAuthenticationStrategy( +// new SessionRegistryImpl()); +// } +// +//// +//// @Override +//// @Bean("authenticationManager") +//// public AuthenticationManager authenticationManagerBean() throws Exception { +//// return super.authenticationManagerBean(); +//// } +// +// //see also https://www.baeldung.com/securing-a-restful-web-service-with-spring-security +// @Override +// protected void configure(final HttpSecurity http) throws Exception { +// // @formatter:off +// http.authorizeRequests() +//// .antMatchers("/sessions/**").permitAll() +//// .antMatchers("/register/**").permitAll() +//// //.antMatchers("/sessions/logout").permitAll() +//// .antMatchers("/categories/**").permitAll() +//// .antMatchers("/experiments/**").permitAll() +//// .antMatchers("/vxfs/**").permitAll() +//// .antMatchers("/login").permitAll() +//// .antMatchers("/images/**").permitAll() +//// .antMatchers("/packages/**").permitAll() +//// .antMatchers("/testweb/**").permitAll() +//// .antMatchers("/oauth/token/revokeById/**").permitAll() +//// .antMatchers("/tokens/**").permitAll() +//// .antMatchers("/actuator/**").permitAll() +//// .antMatchers("/swagger/**").permitAll() +//// .antMatchers("/v2/**").permitAll() +//// .antMatchers("/swagger-ui.html").permitAll() +//// .antMatchers("/webjars/**").permitAll() +//// .antMatchers("/swagger-resources/**").permitAll() +// //.antMatchers("/admin/**").permitAll()//.hasAnyRole("admin","user","ROLE_admin","ROLE_user") +// +// .anyRequest().permitAll() +// //.and().formLogin().permitAll() +// .and().csrf().disable() +// //.cors().and().csrf().disable() // we use the filter..see below +// .exceptionHandling() +// .authenticationEntryPoint(restAuthenticationEntryPoint) +// .and() +// .logout(); +// // @formatter:on +// } +// +// +// @Bean +// public FilterRegistrationBean corsFilter() { +// +// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); +// CorsConfiguration config = new CorsConfiguration(); +// config.setAllowCredentials(true); +// config.setAllowedOriginPatterns(Collections.singletonList("*")); +// config.addAllowedHeader("*"); +// config.addAllowedMethod("*"); +// config.addAllowedOriginPattern( "*" ); +// source.registerCorsConfiguration("/**", config); +// FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); +// +// bean.setOrder(0); +// +// return bean; +// +// } +// +// +// +//} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 5b82158..41655a5 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -33,7 +33,15 @@ spring: jwt: issuer-uri: http://keycloak:8080/auth/realms/openslice jwk-set-uri: http://keycloak:8080/auth/realms/openslice/.well-known/openid-configuration - + + +spring-addons: + issuers: + - uri: http://keycloak:8080/auth/realms/openslice + username-json-path: $.preferred_username + claims: + - jsonPath: $.realm_access.roles + - jsonPath: $.resource_access.*.roles logging: level: -- GitLab