Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GroupUserMapper.scala: Backport fix from TheHive for group mapper functionality #438

Merged
merged 3 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# StrangeBee Security Policies

At [StrangeBee](https://www.strangebee.com) we take the security our software and services seriously, including following applications and projects:
- TheHive (TheHive 5, and [previous open source version](https://github.com/TheHive-Project/TheHive))
- [Cortex](https://github.com/TheHive-Project/Cortex)
- [Cortex-Analyzers](https://github.com/TheHive-Project/Cortex-Analyzers)

## Reporting a vulnerability
If you believe you have found a security vulnerability in our applications and services (TheHive, Cortex, Cortex-Analyzers ...), report it to us.

**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**

Instead, please send security vulnerabilities by emailing the StrangeBee Security team:

```
security[@]strangebee.com
```

In this email, please include as much information as possible that can help us better understand and resolve the issue:
- Application and version
- Special configuration and usage required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Exploit code is any
- Impact of the issue

This will be very useful and help us triage your report more quickly.

More information regarding our Security policies and Advisories can be found here: [https://github.com/StrangeBeeCorp/security]().
83 changes: 72 additions & 11 deletions app/org/thp/cortex/services/mappers/GroupUserMapper.scala
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
package org.thp.cortex.services.mappers

import scala.concurrent.{ExecutionContext, Future}
import scala.util.parsing.combinator._

import play.api.Configuration
import play.api.{Configuration, Logger}
import play.api.libs.json._
import play.api.libs.ws.WSClient

import javax.inject.Inject

import org.elastic4play.AuthenticationError
import org.elastic4play.{AuthenticationError, AuthorizationError}
import org.elastic4play.controllers.Fields

class GroupUserMapper(
loginAttrName: String,
nameAttrName: String,
groupAttrName: String,
groupsAttrName: String,
organizationAttrName: Option[String],
defaultRoles: Seq[String],
defaultOrganization: Option[String],
groupsUrl: String,
groupsUrl: Option[String],
mappings: Map[String, Seq[String]],
ws: WSClient,
implicit val ec: ExecutionContext
Expand All @@ -32,21 +33,81 @@ class GroupUserMapper(
configuration.getOptional[String]("auth.sso.attributes.organization"),
configuration.getOptional[Seq[String]]("auth.sso.defaultRoles").getOrElse(Seq()),
configuration.getOptional[String]("auth.sso.defaultOrganization"),
configuration.getOptional[String]("auth.sso.groups.url").getOrElse(""),
configuration.getOptional[String]("auth.sso.groups.url"),
configuration.getOptional[Map[String, Seq[String]]]("auth.sso.groups.mappings").getOrElse(Map()),
ws,
ec
)

override val name: String = "group"

private[GroupUserMapper] lazy val logger = Logger(getClass)

private class RoleListParser extends RegexParsers {
val str = "[a-zA-Z0-9_]+".r
val strSpc = "[a-zA-Z0-9_ ]+".r
val realStr = ("\""~>strSpc<~"\"" | "'"~>strSpc<~"'" | str)

def expr: Parser[Seq[String]] = {
"[" ~ opt(realStr ~ rep("," ~ realStr)) ~ "]" ^^ {
case _ ~ Some(firstRole ~ list) ~ _ ⇒ list.foldLeft(Seq(firstRole)) {
case (queue, _ ~ role) ⇒ role +: queue
}
case _ ~ _ ⇒ Seq.empty[String]
} | opt(realStr) ^^ {
case Some(role) ⇒ Seq(role)
case None ⇒ Seq.empty[String]
}
}
}

override def getUserFields(jsValue: JsValue, authHeader: Option[(String, String)]): Future[Fields] = {

val apiCall = authHeader.fold(ws.url(groupsUrl))(headers => ws.url(groupsUrl).addHttpHeaders(headers))
apiCall.get.flatMap { r =>
val jsonGroups = (r.json \ groupAttrName).as[Seq[String]]
val mappedRoles = jsonGroups.flatMap(mappings.get).maxBy(_.length)
val roles = if (mappedRoles.nonEmpty) mappedRoles else defaultRoles
groupsUrl match {
case Some(groupsEndpointUrl) ⇒ {
logger.debug(s"Retreiving groups from ${groupsEndpointUrl}")
val apiCall = authHeader.fold(ws.url(groupsEndpointUrl))(headers ⇒ ws.url(groupsEndpointUrl).addHttpHeaders(headers))
apiCall.get.flatMap { r ⇒ extractGroupsThenBuildUserFields(jsValue, r.json) }
}
case None ⇒ {
logger.debug(s"Extracting groups from user info")
extractGroupsThenBuildUserFields(jsValue, jsValue)
}
}
}

private def extractGroupsThenBuildUserFields(jsValue: JsValue, groupsContainer: JsValue): Future[Fields] = {
(groupsContainer \ groupsAttrName) match {
// Groups received as valid JSON array
case JsDefined(JsArray(groupsList)) ⇒ mapGroupsAndBuildUserFields(jsValue, groupsList.map(_.as[String]).toList)

// Groups list received as string (invalid JSON, for example: "ROLE" or "['Role 1', ROLE2, 'Role_3']")
case JsDefined(JsString(groupsStr)) ⇒ {
val parser = new RoleListParser
parser.parseAll(parser.expr, groupsStr) match {
case parser.Success(result, _) ⇒ mapGroupsAndBuildUserFields(jsValue, result)
case err: parser.NoSuccess ⇒ Future.failed(AuthenticationError(s"User info fails: can't parse groups list (${err.msg})"))
}
}

// Invalid group list
case JsDefined(error) ⇒
Future.failed(AuthenticationError(s"User info fails: invalid groups list received in user info ('${error}' of type ${error.getClass})"))

// Groups field is undefined
case _: JsUndefined ⇒
Future.failed(AuthenticationError(s"User info fails: groups attribute ${groupsAttrName} doesn't exist in user info"))
}
}

private def mapGroupsAndBuildUserFields(jsValue: JsValue, jsonGroups: Seq[String]): Future[Fields] = {
val mappedRoles = jsonGroups.flatMap(mappings.get).flatten.toSet
val roles = if (mappedRoles.nonEmpty) mappedRoles else defaultRoles

if (roles.isEmpty) {
Future.failed(AuthorizationError(s"No matched roles for user"))
} else {
logger.debug(s"Computed roles: ${roles.mkString(", ")}")

val fields = for {
login <- (jsValue \ loginAttrName).validate[String]
Expand All @@ -58,7 +119,7 @@ class GroupUserMapper(
} yield Fields(Json.obj("login" -> login.toLowerCase, "name" -> name, "roles" -> roles, "organization" -> organization))
fields match {
case JsSuccess(f, _) => Future.successful(f)
case JsError(errors) => Future.failed(AuthenticationError(s"User info fails: ${errors.map(_._1).mkString}"))
case JsError(errors) Future.failed(AuthenticationError(s"User info fails: ${errors.map(_._2).map(_.map(_.messages.mkString(", ")).mkString("; ")).mkString}"))
}
}
}
Expand Down