From 2584fe0c038c689a8410ece4f2fab20248bfb19a Mon Sep 17 00:00:00 2001 From: Arpitha Date: Wed, 5 Nov 2025 18:23:10 +0530 Subject: [PATCH 1/2] X-auth-token added and added userId in logs. (#132) * X-auth-token added and added userId in logs. * X-auth-token added and added userId in logs. * X-auth-token added and added userId in logs. * X-auth-token added and added userId in logs. * X-auth-token added and added userId in logs. --- .../java/org/sunbird/common/Constants.java | 9 + .../sunbird/telemetry/TelemetryGenerator.java | 14 +- .../telemetry/logger/TelemetryManager.java | 54 ++- .../logger/TelemetryRequestContext.java | 24 ++ .../sunbird/search/util/SearchConstants.java | 8 + .../controllers/SearchBaseController.scala | 22 +- .../app/utils/AccessTokenValidator.java | 85 ++++ .../search-service/app/utils/Base64Util.java | 404 ++++++++++++++++++ .../search-service/app/utils/CryptoUtil.java | 42 ++ .../search-service/app/utils/KeyData.java | 29 ++ .../search-service/app/utils/KeyManager.java | 100 +++++ .../search-service/conf/application.conf | 9 +- search-api/search-service/pom.xml | 5 + 13 files changed, 789 insertions(+), 16 deletions(-) create mode 100644 platform-core/platform-common/src/main/java/org/sunbird/common/Constants.java create mode 100644 platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryRequestContext.java create mode 100755 search-api/search-service/app/utils/AccessTokenValidator.java create mode 100644 search-api/search-service/app/utils/Base64Util.java create mode 100755 search-api/search-service/app/utils/CryptoUtil.java create mode 100644 search-api/search-service/app/utils/KeyData.java create mode 100644 search-api/search-service/app/utils/KeyManager.java diff --git a/platform-core/platform-common/src/main/java/org/sunbird/common/Constants.java b/platform-core/platform-common/src/main/java/org/sunbird/common/Constants.java new file mode 100644 index 000000000..21191cb17 --- /dev/null +++ b/platform-core/platform-common/src/main/java/org/sunbird/common/Constants.java @@ -0,0 +1,9 @@ +package org.sunbird.common; + +public class Constants { + public static final String USER_ID = "userId"; + public static final String SYSTEM = "system"; + public static final String ACCESS_PUBLIC_KEY_PATH = "access.token.publickey.basepath"; + public static final String READING_PUBLIC_KEY_EXCEPTION = "KeyManager:init: exception reading public key file:"; + public static final String LOADING_PUBLIC_KEY_EXCEPTION = "KeyManager:init: exception loading public keys"; +} diff --git a/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/TelemetryGenerator.java b/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/TelemetryGenerator.java index 66ad4dbbc..6df67a5a4 100644 --- a/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/TelemetryGenerator.java +++ b/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/TelemetryGenerator.java @@ -2,12 +2,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; +import org.sunbird.common.Constants; import org.sunbird.common.Platform; import org.sunbird.telemetry.dto.Actor; import org.sunbird.telemetry.dto.Context; import org.sunbird.telemetry.dto.Producer; import org.sunbird.telemetry.dto.Target; import org.sunbird.telemetry.dto.Telemetry; +import org.sunbird.telemetry.logger.TelemetryRequestContext; import java.util.ArrayList; import java.util.HashMap; @@ -77,6 +79,10 @@ public static String log(Map context, String type, String level, edata.put("pageid", pageid); if (null != params && !params.isEmpty()) edata.put("params", getParamsList(params)); + Object userIdFromContext = context.get(Constants.USER_ID); + if (userIdFromContext != null && StringUtils.isNotBlank(userIdFromContext.toString())) { + edata.put(Constants.USER_ID, userIdFromContext.toString()); + } Telemetry telemetry = new Telemetry("LOG", actor, eventContext, edata); return getTelemetry(telemetry); } @@ -117,6 +123,10 @@ public static String error(Map context, String code, String type edata.put("pageid", pageid); if (null != object) edata.put("object", object); + String uid = context != null ? context.get(Constants.USER_ID) : null; + if (StringUtils.isNotBlank(uid)) { + edata.put(Constants.USER_ID, uid); + } Telemetry telemetry = new Telemetry("ERROR", actor, eventContext, edata); return getTelemetry(telemetry); @@ -233,7 +243,9 @@ private static Context getContext(Map context) { String did = context.get("did"); if (StringUtils.isNotBlank(did)) eventContext.setDid(did); - + if (StringUtils.isNotBlank(TelemetryRequestContext.getUserId())) { + context.put(Constants.USER_ID, TelemetryRequestContext.getUserId()); + } return eventContext; } diff --git a/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryManager.java b/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryManager.java index 211cc2e46..d248cb5b8 100644 --- a/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryManager.java +++ b/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryManager.java @@ -2,6 +2,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.sunbird.common.Constants; import org.sunbird.common.Platform; import org.sunbird.common.exception.MiddlewareException; import org.sunbird.common.exception.ResponseCode; @@ -37,10 +38,12 @@ public class TelemetryManager { * @param params */ - public static void access(Map context, Map params) { - String event = TelemetryGenerator.access(context, params); - telemetryHandler.send(event, Level.INFO, true); - } + public static void access(Map context, Map params) { + Map ctx = enrichContextWithUid(context); + Map enrichedParams = ensureParamsWithUid(params); + String event = TelemetryGenerator.access(ctx, enrichedParams); + telemetryHandler.send(event, Level.INFO, true); + } /** * To log only message as a telemetry event. @@ -197,7 +200,14 @@ public static void search(Map context, String query, Object filt } } - String event = TelemetryGenerator.search(reqContext, query, filters, sort, null, size, topN, type); + String uid = TelemetryRequestContext.getUserId(); + if (StringUtils.isNotBlank(uid)) { + if (reqContext == null) reqContext = new HashMap<>(); + reqContext.put(TelemetryParams.ACTOR.name(), uid); + reqContext.put(Constants.USER_ID, uid); + } + + String event = TelemetryGenerator.search(reqContext, query, filters, sort, null, size, topN, type); telemetryHandler.send(event, Level.INFO, true); } @@ -210,12 +220,19 @@ public static void search(Map context, String query, Object filt */ private static void log(String message, Map params, String logLevel) { Map context = getContext(); - String event = TelemetryGenerator.log(context, "system", logLevel, message, null, params); + Map enrichedParams = ensureParamsWithUid(params); + String event = TelemetryGenerator.log(context, Constants.SYSTEM, logLevel, message, null, enrichedParams); telemetryHandler.send(event, Level.getLevel(logLevel)); } private static Map getContext() { Map context = new HashMap(); + String uid = TelemetryRequestContext.getUserId(); + + if (StringUtils.isNotBlank(uid)) { + context.put(TelemetryParams.ACTOR.name(), uid); + context.put(Constants.USER_ID, uid); + } context.put(TelemetryParams.ACTOR.name(), "org.sunbird.learning.platform"); context.put(TelemetryParams.CHANNEL.name(), getContextValue("CHANNEL_ID", DEFAULT_CHANNEL_ID)); context.put(TelemetryParams.ENV.name(), getContextValue(TelemetryParams.ENV.name(), "system")); @@ -232,4 +249,29 @@ public static void logRequestBody(String message) { String event = TelemetryGenerator.log(context, "payload", Level.INFO.name(), message, null, null); telemetryHandler.send(event, Level.INFO, true); } + + private static Map ensureParamsWithUid(Map params) { + String uid = TelemetryRequestContext.getUserId(); + if (StringUtils.isNotBlank(uid)) { + if (params == null) params = new HashMap<>(); + params.put(Constants.USER_ID, uid); + } + return params; + } + + private static Map enrichContextWithUid(Map context) { + String uid = TelemetryRequestContext.getUserId(); + Map ctx = (context == null) ? new HashMap<>() : new HashMap<>(context); + if (StringUtils.isNotBlank(uid)) { + ctx.put(TelemetryParams.ACTOR.name(), uid); + ctx.put(Constants.USER_ID, uid); + } + if (!ctx.containsKey(TelemetryParams.CHANNEL.name())) { + ctx.put(TelemetryParams.CHANNEL.name(), DEFAULT_CHANNEL_ID); + } + if (!ctx.containsKey(TelemetryParams.ENV.name())) { + ctx.put(TelemetryParams.ENV.name(), Constants.SYSTEM); + } + return ctx; + } } diff --git a/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryRequestContext.java b/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryRequestContext.java new file mode 100644 index 000000000..f3e336230 --- /dev/null +++ b/platform-core/platform-telemetry/src/main/java/org/sunbird/telemetry/logger/TelemetryRequestContext.java @@ -0,0 +1,24 @@ +// Java +package org.sunbird.telemetry.logger; + +import org.apache.commons.lang3.StringUtils; +import java.util.HashMap; +import java.util.Map; + +public final class TelemetryRequestContext { + private static final ThreadLocal> CTX = ThreadLocal.withInitial(HashMap::new); + + private TelemetryRequestContext() { } + + public static void setUserId(String userId) { + if (StringUtils.isNotBlank(userId)) CTX.get().put("userId", userId); + } + + public static String getUserId() { + return CTX.get().get("userId"); + } + + public static void clear() { + CTX.remove(); + } +} \ No newline at end of file diff --git a/search-api/search-core/src/main/java/org/sunbird/search/util/SearchConstants.java b/search-api/search-core/src/main/java/org/sunbird/search/util/SearchConstants.java index dd2da9776..12179ac16 100644 --- a/search-api/search-core/src/main/java/org/sunbird/search/util/SearchConstants.java +++ b/search-api/search-core/src/main/java/org/sunbird/search/util/SearchConstants.java @@ -124,4 +124,12 @@ public class SearchConstants { public static final String OPERATION = "operation"; public static final String PROPERTY_NAME = "propertyName"; public static final String VALUES = "values"; + public static final String SEARCH = "search"; + public static final String X_AUTH_TOKEN = "x-authenticated-user-token"; + public static final String CHANNEL_ID = "CHANNEL_ID"; + public static final String CHANNEL_DEFAULT = "channel.default"; + public static final String CONSUMER_ID = "CONSUMER_ID"; + public static final String LEARNING_PLATFORM = "learning.platform"; + + } diff --git a/search-api/search-service/app/controllers/SearchBaseController.scala b/search-api/search-service/app/controllers/SearchBaseController.scala index 5938c93ac..9fd94b007 100644 --- a/search-api/search-service/app/controllers/SearchBaseController.scala +++ b/search-api/search-service/app/controllers/SearchBaseController.scala @@ -4,15 +4,18 @@ import akka.actor.ActorRef import akka.pattern.Patterns import org.apache.commons.lang3.StringUtils import org.sunbird.common.dto.{RequestParams, Response, ResponseHandler} -import org.sunbird.common.exception.ResponseCode +import org.sunbird.common.exception.{ClientException, ResponseCode} import org.sunbird.common.{DateUtils, JsonUtils, Platform} +import org.sunbird.search.util.SearchConstants import org.sunbird.telemetry.TelemetryParams +import org.sunbird.telemetry.logger.TelemetryRequestContext import play.api.mvc._ import java.util import java.util.UUID import scala.collection.JavaConversions._ import scala.concurrent.{ExecutionContext, Future} +import utils.AccessTokenValidator abstract class SearchBaseController(protected val cc: ControllerComponents)(implicit exec: ExecutionContext) extends AbstractController(cc) { @@ -110,17 +113,20 @@ abstract class SearchBaseController(protected val cc: ControllerComponents)(impl request } - protected def setHeaderContext(searchRequest: org.sunbird.common.dto.Request)(implicit playRequest: play.api.mvc.Request[AnyContent]) : Unit = { + protected def setHeaderContext(searchRequest: org.sunbird.common.dto.Request)(implicit playRequest: play.api.mvc.Request[AnyContent]): Unit = { searchRequest.setContext(new util.HashMap[String, AnyRef]()) - searchRequest.getContext.put(TelemetryParams.ENV.name, "search") + searchRequest.getContext.put(TelemetryParams.ENV.name, SearchConstants.SEARCH) searchRequest.getContext.putAll(commonHeaders()) - if (StringUtils.isBlank(searchRequest.getContext.getOrDefault("CHANNEL_ID", "").asInstanceOf[String])) { - searchRequest.getContext.put("CHANNEL_ID", Platform.config.getString("channel.default")) + val token = playRequest.headers.get(SearchConstants.X_AUTH_TOKEN).getOrElse("") + val userId = AccessTokenValidator.verifyUserToken(token, searchRequest.getContext) + TelemetryRequestContext.setUserId(userId) + if (StringUtils.isBlank(searchRequest.getContext.getOrDefault(SearchConstants.CHANNEL_ID, "").asInstanceOf[String])) { + searchRequest.getContext.put(SearchConstants.CHANNEL_ID, Platform.config.getString(SearchConstants.CHANNEL_DEFAULT)) } - if (null != searchRequest.getContext.get("CONSUMER_ID")) searchRequest.put(TelemetryParams.ACTOR.name, searchRequest.getContext.get("CONSUMER_ID")) - else if (null != searchRequest && null != searchRequest.getParams.getCid) searchRequest.put(TelemetryParams.ACTOR.name, searchRequest.getParams.getCid) - else searchRequest.put(TelemetryParams.ACTOR.name, "learning.platform") + if (null != searchRequest.getContext.get(SearchConstants.CONSUMER_ID) && (searchRequest.getContext.get(TelemetryParams.ACTOR.name) == null)) searchRequest.put(TelemetryParams.ACTOR.name, searchRequest.getContext.get("CONSUMER_ID")) + else if (null != searchRequest && null != searchRequest.getParams.getCid && (searchRequest.getContext.get(TelemetryParams.ACTOR.name) == null)) searchRequest.put(TelemetryParams.ACTOR.name, searchRequest.getParams.getCid) + else if (searchRequest.getContext.get(TelemetryParams.ACTOR.name) == null) searchRequest.put(TelemetryParams.ACTOR.name, SearchConstants.LEARNING_PLATFORM) } def getErrorResponse(apiId: String, version: String, errCode: String, errMessage: String): Future[Result] = { diff --git a/search-api/search-service/app/utils/AccessTokenValidator.java b/search-api/search-service/app/utils/AccessTokenValidator.java new file mode 100755 index 000000000..95baab536 --- /dev/null +++ b/search-api/search-service/app/utils/AccessTokenValidator.java @@ -0,0 +1,85 @@ +package utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.keycloak.common.util.Time; +import org.sunbird.common.Platform; +import org.sunbird.telemetry.logger.TelemetryManager; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +public class AccessTokenValidator { + private AccessTokenValidator() { } + private static final ObjectMapper mapper = new ObjectMapper(); + private static final String sso_url = Platform.config.getString("sso.url"); + private static final String realm = Platform.config.getString("sso.realm"); + + private static Map validateToken(String token, Map requestContext) + throws IOException { + String[] tokenElements = token.split("\\."); + String header = tokenElements[0]; + String body = tokenElements[1]; + String signature = tokenElements[2]; + String payLoad = header + "." + body; + Map headerData = + mapper.readValue(new String(decodeFromBase64(header)), Map.class); + String keyId = headerData.get("kid").toString(); + boolean isValid = + CryptoUtil.verifyRSASign( + payLoad, + decodeFromBase64(signature), + KeyManager.getPublicKey(keyId).getPublicKey(), + "SHA256withRSA", + requestContext); + if (isValid) { + Map tokenBody = + mapper.readValue(new String(decodeFromBase64(body)), Map.class); + boolean isExp = isExpired((Integer) tokenBody.get("exp")); + if (isExp) { + TelemetryManager.warn("Token is expired "); + return Collections.EMPTY_MAP; + } + return tokenBody; + } + return Collections.EMPTY_MAP; + } + + public static String verifyUserToken(String token, Map requestContext) { + String userId = "UNAUTHORIZED"; + try { + Map payload = validateToken(token, requestContext); + if (MapUtils.isNotEmpty(payload) && checkIss((String) payload.get("iss"))) { + userId = (String) payload.get("sub"); + if (StringUtils.isNotBlank(userId)) { + int pos = userId.lastIndexOf(":"); + userId = userId.substring(pos + 1); + } + } + } catch (Exception ex) { + TelemetryManager.error( + "Exception in verifyUserAccessToken: Token : ", ex); + } + if ("UNAUTHORIZED".equalsIgnoreCase(userId)) { + TelemetryManager.info( + "verifyUserAccessToken: Invalid User Token"); + } + return userId; + } + + private static boolean checkIss(String iss) { + String realmUrl = sso_url + "realms/" + realm; + return (realmUrl.equalsIgnoreCase(iss)); + } + + + private static boolean isExpired(Integer expiration) { + return (Time.currentTime() > expiration); + } + + private static byte[] decodeFromBase64(String data) { + return Base64Util.decode(data, 11); + } +} diff --git a/search-api/search-service/app/utils/Base64Util.java b/search-api/search-service/app/utils/Base64Util.java new file mode 100644 index 000000000..7d4b7e0b2 --- /dev/null +++ b/search-api/search-service/app/utils/Base64Util.java @@ -0,0 +1,404 @@ +package utils; + +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +import java.io.UnsupportedEncodingException; + +/** + * Utilities for encoding and decoding the Base64 representation of binary data. See RFCs 2045 and 3548. + */ +public class Base64Util { + /** Default values for encoder/decoder flags. */ + public static final int DEFAULT = 0; + + /** Encoder flag bit to omit the padding '=' characters at the end of the output (if any). */ + public static final int NO_PADDING = 1; + + /** Encoder flag bit to omit all line terminators (i.e., the output will be on one long line). */ + public static final int NO_WRAP = 2; + + /** + * Encoder flag bit to indicate lines should be terminated with a CRLF pair instead of just an LF. + * Has no effect if {@code NO_WRAP} is specified as well. + */ + public static final int CRLF = 4; + + /** + * Encoder/decoder flag bit to indicate using the "URL and filename safe" variant of Base64 (see + * RFC 3548 section 4) where {@code -} and {@code _} are used in place of {@code +} and {@code /}. + */ + public static final int URL_SAFE = 8; + + + // -------------------------------------------------------- + // decoding + // -------------------------------------------------------- + + /** + * Decode the Base64-encoded data in input and return the data in a new byte array. + * + *

+ * + *

The padding '=' characters at the end are considered optional, but if any are present, there + * must be the correct number of them. + * + * @param str the input String to decode, which is converted to bytes using the default charset + * @param flags controls certain features of the decoded output. Pass {@code DEFAULT} to decode + * standard Base64. + * @throws IllegalArgumentException if the input contains incorrect padding + */ + public static byte[] decode(String str, int flags) { + return decode(str.getBytes(), flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in a new byte array. + * + *

+ * + *

The padding '=' characters at the end are considered optional, but if any are present, there + * must be the correct number of them. + * + * @param input the input array to decode + * @param flags controls certain features of the decoded output. Pass {@code DEFAULT} to decode + * standard Base64. + * @throws IllegalArgumentException if the input contains incorrect padding + */ + public static byte[] decode(byte[] input, int flags) { + return decode(input, 0, input.length, flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in a new byte array. + * + *

+ * + *

The padding '=' characters at the end are considered optional, but if any are present, there + * must be the correct number of them. + * + * @param input the data to decode + * @param offset the position within the input array at which to start + * @param len the number of bytes of input to decode + * @param flags controls certain features of the decoded output. Pass {@code DEFAULT} to decode + * standard Base64. + * @throws IllegalArgumentException if the input contains incorrect padding + */ + public static byte[] decode(byte[] input, int offset, int len, int flags) { + // Allocate space for the most data the input could represent. + // (It could contain less if it contains whitespace, etc.) + Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]); + + if (!decoder.process(input, offset, len, true)) { + throw new IllegalArgumentException("bad base-64"); + } + + // Maybe we got lucky and allocated exactly enough output space. + if (decoder.op == decoder.output.length) { + return decoder.output; + } + + // Need to shorten the array, so allocate a new one of the + // right size and copy. + byte[] temp = new byte[decoder.op]; + System.arraycopy(decoder.output, 0, temp, 0, decoder.op); + return temp; + } + + + /* package */ abstract static class Coder { + public byte[] output; + public int op; + + /** + * Encode/decode another block of input data. this.output is provided by the caller, and must be + * big enough to hold all the coded data. On exit, this.opwill be set to the length of the coded + * data. + * + * @param finish true if this is the final call to process for this object. Will finalize the + * coder state and include any final bytes in the output. + * @return true if the input so far is good; false if some error has been detected in the input + * stream.. + */ + public abstract boolean process(byte[] input, int offset, int len, boolean finish); + + /** + * @return the maximum number of bytes a call to process() could produce for the given number of + * input bytes. This may be an overestimate. + */ + public abstract int maxOutputSize(int len); + } + + /* package */ static class Decoder extends Coder { + /** Lookup table for turning bytes into their position in the Base64 alphabet. */ + private static final int DECODE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** + * Decode lookup table for the "web safe" variant (RFC 3548 sec. 4) where - and _ replace + and + * /. + */ + private static final int DECODE_WEBSAFE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** Non-data values in the DECODE arrays. */ + private static final int SKIP = -1; + + private static final int EQUALS = -2; + private final int[] alphabet; + /** + * States 0-3 are reading through the next input tuple. State 4 is having read one '=' and + * expecting exactly one more. State 5 is expecting no more data or padding characters in the + * input. State 6 is the error state; an error has been detected in the input and no future + * input can "fix" it. + */ + private int state; // state number (0 to 6) + + private int value; + + public Decoder(int flags, byte[] output) { + this.output = output; + + alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE; + state = 0; + value = 0; + } + + /** @return an overestimate for the number of bytes {@code len} bytes could decode to. */ + public int maxOutputSize(int len) { + return len * 3 / 4 + 10; + } + + /** + * Decode another block of input data. + * + * @return true if the state machine is still healthy. false if bad base-64 data has been + * detected in the input stream. + */ + public boolean process(byte[] input, int offset, int len, boolean finish) { + if (this.state == 6) return false; + + int p = offset; + len += offset; + + // Using local variables makes the decoder about 12% + // faster than if we manipulate the member variables in + // the loop. (Even alphabet makes a measurable + // difference, which is somewhat surprising to me since + // the member variable is final.) + int state = this.state; + int value = this.value; + int op = 0; + final byte[] output = this.output; + final int[] alphabet = this.alphabet; + + while (p < len) { + // Try the fast path: we're starting a new tuple and the + // next four bytes of the input stream are all data + // bytes. This corresponds to going through states + // 0-1-2-3-0. We expect to use this method for most of + // the data. + // + // If any of the next four bytes of input are non-data + // (whitespace, etc.), value will end up negative. (All + // the non-data values in decode are small negative + // numbers, so shifting any of them up and or'ing them + // together will result in a value with its top bit set.) + // + // You can remove this whole block and the output should + // be the same, just slower. + if (state == 0) { + while (p + 4 <= len + && (value = + ((alphabet[input[p] & 0xff] << 18) + | (alphabet[input[p + 1] & 0xff] << 12) + | (alphabet[input[p + 2] & 0xff] << 6) + | (alphabet[input[p + 3] & 0xff]))) + >= 0) { + output[op + 2] = (byte) value; + output[op + 1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + p += 4; + } + if (p >= len) break; + } + + // The fast path isn't available -- either we've read a + // partial tuple, or the next four input bytes aren't all + // data, or whatever. Fall back to the slower state + // machine implementation. + + int d = alphabet[input[p++] & 0xff]; + + switch (state) { + case 0: + if (d >= 0) { + value = d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 1: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 2: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect exactly one more padding character. + output[op++] = (byte) (value >> 4); + state = 4; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 3: + if (d >= 0) { + // Emit the output triple and return to state 0. + value = (value << 6) | d; + output[op + 2] = (byte) value; + output[op + 1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + state = 0; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect no further data or padding characters. + output[op + 1] = (byte) (value >> 2); + output[op] = (byte) (value >> 10); + op += 2; + state = 5; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 4: + if (d == EQUALS) { + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 5: + if (d != SKIP) { + this.state = 6; + return false; + } + break; + } + } + + if (!finish) { + // We're out of input, but a future call could provide + // more. + this.state = state; + this.value = value; + this.op = op; + return true; + } + + // Done reading input. Now figure out where we are left in + // the state machine and finish up. + + switch (state) { + case 0: + // Output length is a multiple of three. Fine. + break; + case 1: + // Read one extra input byte, which isn't enough to + // make another output byte. Illegal. + this.state = 6; + return false; + case 2: + // Read two extra input bytes, enough to emit 1 more + // output byte. Fine. + output[op++] = (byte) (value >> 4); + break; + case 3: + // Read three extra input bytes, enough to emit 2 more + // output bytes. Fine. + output[op++] = (byte) (value >> 10); + output[op++] = (byte) (value >> 2); + break; + case 4: + // Read one padding '=' when we expected 2. Illegal. + this.state = 6; + return false; + case 5: + // Read all the padding '='s we expected and no more. + // Fine. + break; + } + + this.state = state; + this.op = op; + return true; + } + } +} diff --git a/search-api/search-service/app/utils/CryptoUtil.java b/search-api/search-service/app/utils/CryptoUtil.java new file mode 100755 index 000000000..ac82a2eea --- /dev/null +++ b/search-api/search-service/app/utils/CryptoUtil.java @@ -0,0 +1,42 @@ +package utils; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Map; + +public class CryptoUtil { + + private static final String US_ASCII = "US-ASCII"; + + public static boolean verifyRSASign( + String payLoad, + byte[] signature, + PublicKey key, + String algorithm, + Map requestContext) { + try { + Signature sign = Signature.getInstance(algorithm); + sign.initVerify(key); + sign.update(toAsciiBytes(payLoad)); + return sign.verify(signature); + } catch (NoSuchAlgorithmException e) { + return false; + } catch (InvalidKeyException e) { + return false; + } catch (SignatureException e) { + return false; + } + } + + private static byte[] toAsciiBytes(String s) { + try { + return s.getBytes(US_ASCII); + } catch (UnsupportedEncodingException e) { + return s.getBytes(); + } + } +} \ No newline at end of file diff --git a/search-api/search-service/app/utils/KeyData.java b/search-api/search-service/app/utils/KeyData.java new file mode 100644 index 000000000..2b65413ce --- /dev/null +++ b/search-api/search-service/app/utils/KeyData.java @@ -0,0 +1,29 @@ +package utils; + +import java.security.PublicKey; + +public class KeyData { + private String keyId; + private PublicKey publicKey; + + public KeyData(String keyId, PublicKey publicKey) { + this.keyId = keyId; + this.publicKey = publicKey; + } + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public PublicKey getPublicKey() { + return publicKey; + } + + public void setPublicKey(PublicKey publicKey) { + this.publicKey = publicKey; + } +} diff --git a/search-api/search-service/app/utils/KeyManager.java b/search-api/search-service/app/utils/KeyManager.java new file mode 100644 index 000000000..4dbc75380 --- /dev/null +++ b/search-api/search-service/app/utils/KeyManager.java @@ -0,0 +1,100 @@ +package utils; + +import org.sunbird.common.Constants; +import org.sunbird.common.Platform; +import org.sunbird.telemetry.logger.TelemetryManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class KeyManager { + + private static final Map keyMap = new HashMap(); + + public static void init() { + String basePath = Platform.config.getString(Constants.ACCESS_PUBLIC_KEY_PATH); + try { + File baseDir = new File(basePath); + List files = new ArrayList(); + listFilesRecursively(baseDir, files); + + for (int i = 0; i < files.size(); i++) { + File f = files.get(i); + try { + String content = readFileUtf8(f); + KeyData keyData = new KeyData(f.getName(), loadPublicKey(content)); + keyMap.put(f.getName(), keyData); + } catch (Exception e) { + TelemetryManager.error(Constants.READING_PUBLIC_KEY_EXCEPTION, e); + } + } + } catch (Exception e) { + TelemetryManager.error(Constants.LOADING_PUBLIC_KEY_EXCEPTION, e); + } + } + + private static void listFilesRecursively(File dir, List out) { + if (dir == null || !dir.exists()) return; + if (dir.isFile()) { + out.add(dir); + return; + } + File[] children = dir.listFiles(); + if (children == null) return; + for (int i = 0; i < children.length; i++) { + File c = children[i]; + if (c.isDirectory()) { + listFilesRecursively(c, out); + } else if (c.isFile()) { + out.add(c); + } + } + } + + private static String readFileUtf8(File file) throws IOException { + BufferedReader br = null; + try { + br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8")); + StringBuilder sb = new StringBuilder(); + char[] buf = new char[4096]; + int n; + while ((n = br.read(buf)) != -1) { + sb.append(buf, 0, n); + } + return sb.toString(); + } finally { + if (br != null) { + try { br.close(); } catch (IOException ignore) {} + } + } + } + + public static KeyData getPublicKey(String keyId) { + if (keyMap.isEmpty()) { + init(); + } + return keyMap.get(keyId); + } + + public static PublicKey loadPublicKey(String key) throws Exception { + String publicKey = new String(key.getBytes("UTF-8"), "UTF-8"); + publicKey = publicKey.replaceAll("(-+BEGIN PUBLIC KEY-+)", ""); + publicKey = publicKey.replaceAll("(-+END PUBLIC KEY-+)", ""); + publicKey = publicKey.replaceAll("[\\r\\n]+", ""); + byte[] keyBytes = Base64Util.decode(publicKey.getBytes("UTF-8"), Base64Util.DEFAULT); + + X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(X509publicKey); + } +} \ No newline at end of file diff --git a/search-api/search-service/conf/application.conf b/search-api/search-service/conf/application.conf index 9416c5f46..c8d5a3ab4 100644 --- a/search-api/search-service/conf/application.conf +++ b/search-api/search-service/conf/application.conf @@ -321,4 +321,11 @@ search.fields.enable.fuzzy.when.noresult=false search.fields.enable.secureSettings=false non.text.fields=["startDateTimeInEpoch","endDateTimeInEpoch"] -allowed.search.query.length=200 \ No newline at end of file +allowed.search.query.length=200 + +access.token.publickey.basepath = "/publickey" + +sso { + url = "https://portal.dev.karmayogibharat.net/auth/" + realm = "sunbird" +} \ No newline at end of file diff --git a/search-api/search-service/pom.xml b/search-api/search-service/pom.xml index 10a883e4a..5e3d2d2cf 100644 --- a/search-api/search-service/pom.xml +++ b/search-api/search-service/pom.xml @@ -50,6 +50,11 @@ play-guice_${scala.major.version} ${play2.version} + + org.keycloak + keycloak-admin-client + 7.0.1 + com.typesafe.play filters-helpers_${scala.major.version} From f4e5b2f0107f4cf9743b88d235b89340bfdd6af9 Mon Sep 17 00:00:00 2001 From: Arpitha Date: Mon, 10 Nov 2025 14:27:54 +0530 Subject: [PATCH 2/2] KB-11923 nested filter query issue fix (#134) --- .../search/processor/SearchProcessor.java | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/search-api/search-core/src/main/java/org/sunbird/search/processor/SearchProcessor.java b/search-api/search-core/src/main/java/org/sunbird/search/processor/SearchProcessor.java index a3428951c..78f77834d 100644 --- a/search-api/search-core/src/main/java/org/sunbird/search/processor/SearchProcessor.java +++ b/search-api/search-core/src/main/java/org/sunbird/search/processor/SearchProcessor.java @@ -6,6 +6,7 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.StringUtils; +import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.index.query.*; import org.elasticsearch.index.query.MultiMatchQueryBuilder.Type; @@ -548,29 +549,16 @@ private void formQueryImpl(List properties, QueryBuilder queryBuilder, Bool } private QueryBuilder checkNestedProperty(QueryBuilder queryBuilder, String propertyName) { - String cleanProp = propertyName.replaceAll(SearchConstants.RAW_FIELD_EXTENSION, ""); - if (!cleanProp.contains(".")) { + if (StringUtils.isBlank(propertyName)) { return queryBuilder; } - - String[] parts = cleanProp.split("\\."); - if (parts.length == 2) { - return QueryBuilders.nestedQuery( - parts[0], - queryBuilder, - org.apache.lucene.search.join.ScoreMode.None - ).innerHit(new InnerHitBuilder()); + String cleanProp = propertyName.replace(SearchConstants.RAW_FIELD_EXTENSION, ""); + if (!cleanProp.contains(".")) { + return queryBuilder; } + String nestedPath = cleanProp.substring(0, cleanProp.lastIndexOf('.')); - for (int i = parts.length - 2; i >= 0; i--) { - String path = String.join(".", Arrays.copyOfRange(parts, 0, i + 1)); - queryBuilder = QueryBuilders.nestedQuery( - path, - queryBuilder, - org.apache.lucene.search.join.ScoreMode.None - ).innerHit(new InnerHitBuilder()); - } - return queryBuilder; + return QueryBuilders.nestedQuery(nestedPath, queryBuilder, ScoreMode.None); }