Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f27a949
Added validation for secureSettings in content READ API
karthik-tarento May 18, 2023
aa43c38
Added hierarchy API changes
karthik-tarento May 25, 2023
2d85c1a
Adding prefix _rc for secure contents
karthik-tarento May 25, 2023
351714d
Updated content security validation properly
karthik-tarento May 26, 2023
3a4ef1d
Added csJwtToken in hierarchy response
karthik-tarento May 29, 2023
3e675bc
default search is modified for contentSecurity (#55)
wilkysingh-tarento May 29, 2023
f67f5f3
Updated the attribute name for adding token
karthik-tarento May 29, 2023
bd2be19
Using proper header for reading user's orgId
karthik-tarento May 29, 2023
1b1e331
Content security search service enhancement (#56)
sreeragksgh May 29, 2023
f9e8e16
Using user channel id header only for read
karthik-tarento May 29, 2023
f763deb
Merge branch '4.8.0-contentSecurity' of https://github.com/sunbird-cb…
karthik-tarento May 29, 2023
821fe63
Added logic to generate jwt token
karthik-tarento May 30, 2023
2237289
Content Security search API enhancement
sreeragksgh May 30, 2023
7a1e826
Modifications
sreeragksgh May 30, 2023
0b82f06
Modifications on default condition
sreeragksgh May 30, 2023
4f49ecf
method name change to formQueryImpl
sreeragksgh May 30, 2023
707a34a
Enhanced code to read private key from config
karthik-tarento May 30, 2023
2355c01
Merge pull request #59 from sreeragksgh/ContentSecuritySearchEnhancement
pkranga May 30, 2023
db937dd
Using HS256 algorithm to generate token
karthik-tarento May 30, 2023
15581ae
Merge branch '4.8.0-contentSecurity' of https://github.com/sunbird-cb…
karthik-tarento May 30, 2023
eb8884b
Added version details in parent pom file
karthik-tarento May 30, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import java.util.concurrent.CompletionException
import java.io.File
import org.apache.commons.io.FilenameUtils
import javax.inject.Inject
import org.apache.commons.lang3.ObjectUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.collections4.{CollectionUtils, MapUtils}
import org.sunbird.`object`.importer.{ImportConfig, ImportManager}
import org.sunbird.actor.core.BaseActor
import org.sunbird.cache.impl.RedisCache
import org.sunbird.content.util.{AcceptFlagManager, ContentConstants, CopyManager, DiscardManager, FlagManager, RetireManager}
import org.sunbird.cloudstore.StorageService
import org.sunbird.common.{ContentParams, Platform, Slug}
import org.sunbird.common.{ContentParams, JsonUtils, Platform, Slug}
import org.sunbird.common.dto.{Request, Response, ResponseHandler}
import org.sunbird.common.exception.ClientException
import org.sunbird.content.dial.DIALManager
Expand Down Expand Up @@ -73,19 +75,35 @@ class ContentActor @Inject() (implicit oec: OntologyEngineContext, ss: StorageSe
DataNode.read(request).map(node => {
val metadata: util.Map[String, AnyRef] = NodeUtil.serialize(node, fields, node.getObjectType.toLowerCase.replace("image", ""), request.getContext.get("version").asInstanceOf[String])
metadata.put("identifier", node.getIdentifier.replace(".img", ""))
val response: Response = ResponseHandler.OK
if (responseSchemaName.isEmpty) {
response.put("content", metadata)
}
else {
response.put(responseSchemaName, metadata)
}
if(!StringUtils.equalsIgnoreCase(metadata.get("visibility").asInstanceOf[String],"Private")) {
response
}
else {
if (StringUtils.equalsIgnoreCase(metadata.get("visibility").asInstanceOf[String],"Private")) {
throw new ClientException("ERR_ACCESS_DENIED", "content visibility is private, hence access denied")
}
var sa = metadata.get("secureSettings")
var securityAttribute : util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]
if(sa.isInstanceOf[String]) {
securityAttribute = JsonUtils.deserialize(sa.asInstanceOf[String], classOf[java.util.Map[String, AnyRef]])
metadata.put("secureSettings", securityAttribute)
} else if (sa.isInstanceOf[util.Map[String, AnyRef]]) {
securityAttribute = metadata.getOrDefault("secureSettings", new util.HashMap[String, AnyRef]).asInstanceOf[util.Map[String, AnyRef]]
}
//var securityAttribute : util.Map[String, AnyRef] = metadata.getOrDefault("secureSettings", new util.HashMap[String, AnyRef]).asInstanceOf[util.Map[String, AnyRef]]
if (MapUtils.isNotEmpty(securityAttribute)) {
var orgList : util.ArrayList[String] = securityAttribute.getOrDefault("organisation", new util.ArrayList[String]).asInstanceOf[util.ArrayList[String]]
if (!CollectionUtils.isEmpty(orgList)) {
//Content should be read by unique org users only.
var userChannelId : String = request.getRequest.getOrDefault("x-user-channel-id", "").asInstanceOf[String]
if (!orgList.contains(userChannelId)) {
throw new ClientException("ERR_ACCESS_DENIED", "User is not allowed to read this content.")
}
}
}
val response: Response = ResponseHandler.OK
if (responseSchemaName.isEmpty) {
response.put("content", metadata)
} else {
response.put(responseSchemaName, metadata)
}
response
})
}

Expand Down
15 changes: 15 additions & 0 deletions content-api/content-service/app/controllers/BaseController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,19 @@ abstract class BaseController(protected val cc: ControllerComponents)(implicit e
Future(BadRequest(JavaJsonUtils.serialize(result)).as("application/json"))
}

def commonReadHeaders(ignoreHeaders: Option[List[String]] = Option(List()))(implicit request: Request[AnyContent]): java.util.Map[String, Object] = {
val customHeaders = Map("x-authenticated-user-orgid" -> "x-user-channel-id", "x-channel-id" -> "channel", "X-Consumer-ID" -> "consumerId", "X-App-Id" -> "appId").filterKeys(key => !ignoreHeaders.getOrElse(List()).contains(key))
customHeaders.map(ch => {
val value = request.headers.get(ch._1)
if (value.isDefined && !value.isEmpty) {
collection.mutable.HashMap[String, Object](ch._2 -> value.get).asJava
} else {
collection.mutable.HashMap[String, Object]().asJava
}
}).reduce((a, b) => {
a.putAll(b)
return a
})
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ContentController @Inject()(@Named(ActorNames.CONTENT_ACTOR) contentActor:
* @return
*/
def read(identifier: String, mode: Option[String], fields: Option[String]) = Action.async { implicit request =>
val headers = commonHeaders()
val headers = commonReadHeaders()
val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]]
content.putAll(headers)
content.putAll(Map("identifier" -> identifier, "mode" -> mode.getOrElse("read"), "fields" -> fields.getOrElse("")).asJava)
Expand Down Expand Up @@ -94,7 +94,7 @@ class ContentController @Inject()(@Named(ActorNames.CONTENT_ACTOR) contentActor:
}

def getHierarchy(identifier: String, mode: Option[String]) = Action.async { implicit request =>
val headers = commonHeaders()
val headers = commonReadHeaders()
val content = new java.util.HashMap().asInstanceOf[java.util.Map[String, Object]]
content.putAll(headers)
content.putAll(Map("rootId" -> identifier, "mode" -> mode.getOrElse("")).asJava)
Expand Down
5 changes: 5 additions & 0 deletions content-api/hierarchy-manager/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
<artifactId>hierarchy-manager</artifactId>

<dependencies>
<dependency>
<groupId>org.sunbird</groupId>
<artifactId>auth-verifier</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.sunbird</groupId>
<artifactId>graph-engine_2.11</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.util
import java.util.concurrent.CompletionException
import com.fasterxml.jackson.databind.ObjectMapper
import org.apache.commons.lang3.StringUtils
import org.sunbird.auth.verifier.JWTUtil
import org.sunbird.cache.impl.RedisCache
import org.sunbird.common.dto.{Request, Response, ResponseHandler, ResponseParams}
import org.sunbird.common.exception.{ClientException, ErrorCodes, ResourceNotFoundException, ResponseCode, ServerException}
Expand Down Expand Up @@ -132,7 +133,9 @@ object HierarchyManager {
}
val bookmarkId = request.get("bookmarkId").asInstanceOf[String]
var metadata: util.Map[String, AnyRef] = NodeUtil.serialize(rootNode, new util.ArrayList[String](), request.getContext.get("schemaName").asInstanceOf[String], request.getContext.get("version").asInstanceOf[String])

if (!validateContentSecurity(request, metadata)) {
Future(ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "User can't read content with Id: " + request.get("rootId")))
}
fetchRelationalMetadata(request, rootNode.getIdentifier).map(collRelationalMetadata => {
val hierarchy = fetchHierarchy(request, rootNode.getIdentifier)

Expand Down Expand Up @@ -211,15 +214,24 @@ object HierarchyManager {
if (!result.isEmpty) {
val bookmarkId = request.get("bookmarkId").asInstanceOf[String]
val rootHierarchy = result.get("content").asInstanceOf[util.Map[String, AnyRef]]
if (StringUtils.isEmpty(bookmarkId)) {
ResponseHandler.OK.put("content", rootHierarchy)
if (!validateContentSecurity(request, rootHierarchy)) {
ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "User can't read content with Id: " + request.get("rootId"))
} else {
val children = rootHierarchy.getOrElse("children", new util.ArrayList[util.Map[String, AnyRef]]()).asInstanceOf[util.List[util.Map[String, AnyRef]]]
val bookmarkHierarchy = filterBookmarkHierarchy(children, bookmarkId)
if (MapUtils.isEmpty(bookmarkHierarchy)) {
ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "bookmarkId " + bookmarkId + " does not exist")
if (isSecureContent(rootHierarchy)) {
val csToken = generateCSToken(rootHierarchy.get("childNodes").asInstanceOf[util.List[String]])
rootHierarchy.put("cstoken", csToken)
}

if (StringUtils.isEmpty(bookmarkId)) {
ResponseHandler.OK.put("content", rootHierarchy)
} else {
ResponseHandler.OK.put("content", bookmarkHierarchy)
val children = rootHierarchy.getOrElse("children", new util.ArrayList[util.Map[String, AnyRef]]()).asInstanceOf[util.List[util.Map[String, AnyRef]]]
val bookmarkHierarchy = filterBookmarkHierarchy(children, bookmarkId)
if (MapUtils.isEmpty(bookmarkHierarchy)) {
ResponseHandler.ERROR(ResponseCode.RESOURCE_NOT_FOUND, ResponseCode.RESOURCE_NOT_FOUND.name(), "bookmarkId " + bookmarkId + " does not exist")
} else {
ResponseHandler.OK.put("content", bookmarkHierarchy)
}
}
}
} else
Expand Down Expand Up @@ -713,4 +725,40 @@ object HierarchyManager {
if(configObjTypes.nonEmpty && !configObjTypes.contains(childNode.getOrDefault("objectType", "").asInstanceOf[String]))
throw new ClientException("ERR_INVALID_CHILDREN", "Invalid Children objectType "+childNode.get("objectType")+" found for : "+childNode.get("identifier") + "| Please provide children having one of the objectType from "+ configObjTypes.asJava)
}

def isSecureContent (metadata: util.Map[String, AnyRef])(implicit ec: ExecutionContext): Boolean = {
var securityAttribute : util.Map[String, AnyRef] = metadata.getOrDefault("secureSettings", new util.HashMap[String, AnyRef]).asInstanceOf[util.Map[String, AnyRef]]
var isSecureContent = false
if (MapUtils.isNotEmpty(securityAttribute)) {
var orgList : util.ArrayList[String] = securityAttribute.getOrDefault("organisation", new util.ArrayList[String]).asInstanceOf[util.ArrayList[String]]
if (!CollectionUtils.isEmpty(orgList)) {
isSecureContent = true
}
}
isSecureContent
}

def validateContentSecurity(request: Request, metadata: util.Map[String, AnyRef])(implicit ec: ExecutionContext): Boolean = {
var securityAttribute : util.Map[String, AnyRef] = metadata.getOrDefault("secureSettings", new util.HashMap[String, AnyRef]).asInstanceOf[util.Map[String, AnyRef]]
var isUserAllowedToRead = true
if (MapUtils.isNotEmpty(securityAttribute)) {
var orgList : util.ArrayList[String] = securityAttribute.getOrDefault("organisation", new util.ArrayList[String]).asInstanceOf[util.ArrayList[String]]
if (!CollectionUtils.isEmpty(orgList)) {
//Content should be read by unique org users only.
var userChannelId : String = request.getRequest.getOrDefault("x-user-channel-id", "").asInstanceOf[String]
if (!orgList.contains(userChannelId)) {
isUserAllowedToRead = false
}
}
}
isUserAllowedToRead
}

def generateCSToken(children: util.List[String])(implicit ec: ExecutionContext): String = {
var csToken = "";
var claimsMap : util.Map[String, AnyRef] = new util.HashMap[String, AnyRef]
claimsMap.put("contentIdentifier", children)
csToken = JWTUtil.createHS256Token(claimsMap)
csToken
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ protected static Map<String, Object> getSystemPropertyQueryMap(Node node, String
if (StringUtils.isBlank(node.getIdentifier()))
node.setIdentifier(Identifier.getIdentifier(node.getGraphId(), Identifier.getUniqueIdFromTimestamp()));

if (node.getMetadata().containsKey("secureSettings")) {
node.setIdentifier(node.getIdentifier() + "_rc");
}
// Adding 'IL_UNIQUE_ID' Property
query.append(
SystemProperties.IL_UNIQUE_ID.name() + ": { SP_" + SystemProperties.IL_UNIQUE_ID.name() + " }, ");
Expand Down
93 changes: 93 additions & 0 deletions platform-core/auth-verifier/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>platform-core</artifactId>
<groupId>org.sunbird</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>auth-verifier</artifactId>

<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${fasterxml.jackson.version}</version>
</dependency>
<dependency>
<groupId>org.sunbird</groupId>
<artifactId>common-util</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.sunbird</groupId>
<artifactId>platform-telemetry</artifactId>
<version>1.0-SNAPSHOT</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>${powermock.api.mockito2.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.module.junit4.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>6.3</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.5</version>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>default-report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Loading