<commit_before>/**
 * Copyright (C) 2015 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.fr

import enumeratum._
import org.orbeon.oxf.common.OXFException
import org.orbeon.oxf.externalcontext.{Credentials, CredentialsSupport, ServletPortletRequest, SimpleRole}
import org.orbeon.oxf.http.Headers
import org.orbeon.oxf.properties.Properties
import org.orbeon.oxf.util.MarkupUtils._
import org.orbeon.oxf.webapp.{SessionFacade, UserRolesFacade}
import org.slf4j.LoggerFactory

import scala.util.control.NonFatal

object FormRunnerAuth {

  val AllHeaderNamesLower = Set(
    Headers.OrbeonUsernameLower,
    Headers.OrbeonGroupLower,
    Headers.OrbeonRolesLower,
    Headers.OrbeonCredentialsLower
  )

  sealed abstract class AuthMethod(override val entryName: String) extends EnumEntry

  object AuthMethod extends Enum[AuthMethod] {

     val values = findValues

     case object Container extends AuthMethod("container")
     case object Header    extends AuthMethod("header")
  }

  import Private._

  // Get the username, group and roles from the request, based on the Form Runner configuration.
  // The first time this is called, the result is stored into the required session. The subsequent times,
  // the value stored in the session is retrieved. This ensures that authentication information remains an
  // invariant for a given session.
  //
  // See https://github.com/orbeon/orbeon-forms/issues/2464
  // See also https://github.com/orbeon/orbeon-forms/issues/2632
  def getCredentialsUseSession(
    userRoles  : UserRolesFacade,
    session    : SessionFacade,
    getHeader  : String => List[String]
  ): Option[Credentials] =
    ServletPortletRequest.findCredentialsInSession(session) orElse {

      val newCredentialsOpt = findCredentialsFromContainerOrHeaders(userRoles, getHeader)

      // Only store the information into the session if we get user credentials. This handles the case of the initial
      // login. See: https://github.com/orbeon/orbeon-forms/issues/2732
      newCredentialsOpt foreach
        (ServletPortletRequest.storeCredentialsInSession(session, _))

      newCredentialsOpt
    }

  def getCredentialsAsHeadersUseSession(
    userRoles  : UserRolesFacade,
    session    : SessionFacade,
    getHeader  : String => List[String]
  ): List[(String, Array[String])] = {

    getCredentialsUseSession(userRoles, session, getHeader) match {
      case Some(credentials) =>
        val result = CredentialsSupport.toHeaders(credentials)
        Logger.debug(s"setting auth headers to: ${headersAsJSONString(result)}")
        result
      case None =>
        // Don't set any headers in case there is no username
        Logger.warn(s"not setting credentials headers because credentials are not found")
        Nil
    }
  }

  private object Private {

    val Logger = LoggerFactory.getLogger("org.orbeon.auth")

    val PropertyPrefix                      = "oxf.fr.authentication."

    val MethodPropertyName                  = PropertyPrefix + "method"
    val ContainerRolesPropertyName          = PropertyPrefix + "container.roles"
    val ContainerRolesSplitPropertyName     = PropertyPrefix + "container.roles.split"
    val HeaderUsernamePropertyName          = PropertyPrefix + "header.username"
    val HeaderRolesPropertyName             = PropertyPrefix + "header.roles"
    val HeaderRolesSplitPropertyName        = PropertyPrefix + "header.roles.split"
    val HeaderGroupPropertyName             = PropertyPrefix + "header.group"
    val HeaderRolesPropertyNamePropertyName = PropertyPrefix + "header.roles.property-name"
    val HeaderCredentialsPropertyName       = PropertyPrefix + "header.credentials"

    val NameValueMatch = "([^=]+)=([^=]+)".r

    def headersAsJSONString(headers: List[(String, Array[String])]): String = {

      val headerAsJSONStrings =
        headers map {
          case (name, values) =>
            val valuesAsString = values.map(_.escapeJavaScript).mkString("""["""", """", """", """"]""")
            s""""$name": $valuesAsString"""
        }

      headerAsJSONStrings.mkString("{", ", ", "}")
    }

    def findCredentialsFromContainerOrHeaders(
      userRoles : UserRolesFacade,
      getHeader : String => List[String]
    ): Option[Credentials] = {

      val propertySet = Properties.instance.getPropertySet

      val requestedAuthMethod = propertySet.getString(MethodPropertyName, "container")

      AuthMethod.withNameOption(requestedAuthMethod) match {

        case Some(authMethod @ AuthMethod.Container) =>

          Logger.debug(s"using `$authMethod` method")

          val usernameOpt    = Option(userRoles.getRemoteUser)
          val rolesStringOpt = propertySet.getNonBlankString(ContainerRolesPropertyName)

          Logger.debug(s"usernameOpt: `$usernameOpt`, roles property: `$rolesStringOpt`")

          usernameOpt map { username =>

              // Wrap exceptions as Liferay throws if the role is not available instead of returning false
              def isUserInRole(role: String) =
                try userRoles.isUserInRole(role)
                catch { case NonFatal(_) => false}

              val rolesSplitRegex =
                propertySet.getString(ContainerRolesSplitPropertyName, """,|\s+""")

              val rolesList =
                for {
                  rolesString <- rolesStringOpt.toList
                  roleName    <- rolesString split rolesSplitRegex
                  if isUserInRole(roleName)
                } yield
                  SimpleRole(roleName)

              Credentials(
                username      = username,
                roles         = rolesList,
                group         = rolesList.headOption map (_.roleName),
                organizations = Nil
              )
          }

        case Some(authMethod @ AuthMethod.Header) =>

          Logger.debug(s"using `$authMethod` method")

          val headerPropertyName =
            propertySet.getNonBlankString(HeaderRolesPropertyNamePropertyName)

          def headerList(name: String) =
            propertySet.getNonBlankString(name).toList flatMap (p => getHeader(p.toLowerCase))

          val rolesSplit = propertySet.getString(HeaderRolesSplitPropertyName, """(\s*[,\|]\s*)+""")
          def splitRoles(value: String) = value split rolesSplit

          Logger.debug(s"using properties: $HeaderRolesPropertyNamePropertyName=`$headerPropertyName`, $HeaderRolesSplitPropertyName=`$rolesSplit`")

          // If configured, a header can have the form `name=value` where `name` is specified in a property
          def splitWithinRole(value: String) = headerPropertyName match {
            case Some(propertyName) =>
              value match {
                case NameValueMatch(`propertyName`, value) => List(value)
                case _                                     => Nil
              }
            case _ => List(value)
          }

          import org.orbeon.oxf.util.CoreUtils._

          // Credentials coming from the JSON-encoded HTTP header
          def fromCredentialsHeader =
            headerList(HeaderCredentialsPropertyName).headOption flatMap
            (CredentialsSupport.parseCredentials(_, decodeForHeader = true)) kestrel
            (_ => Logger.debug(s"found from credential headers"))

          // Credentials coming from individual headers (requires at least the username)
          def fromIndividualHeaders =
            headerList(HeaderUsernamePropertyName).headOption map { username =>

              // Roles: all headers with the given name are used, each header value is split, and result combined
              // See also: https://github.com/orbeon/orbeon-forms/issues/1690
              val roles =
                headerList(HeaderRolesPropertyName) flatMap splitRoles flatMap splitWithinRole map SimpleRole.apply

              Credentials(
                username      = username,
                roles         = roles,
                group         = headerList(HeaderGroupPropertyName).headOption,
                organizations = Nil
              )
            } kestrel
            (_ => Logger.debug(s"found from individual headers"))

          fromCredentialsHeader orElse fromIndividualHeaders

        case None =>
          throw new OXFException(s"'$MethodPropertyName' property: unsupported authentication method `$requestedAuthMethod`")
      }
    }
  }
}
<commit_msg>Minor refactoring: move unused definitions to `Private` object<commit_after>/**
 * Copyright (C) 2015 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.fr

import enumeratum._
import org.orbeon.oxf.common.OXFException
import org.orbeon.oxf.externalcontext.{Credentials, CredentialsSupport, ServletPortletRequest, SimpleRole}
import org.orbeon.oxf.http.Headers
import org.orbeon.oxf.properties.Properties
import org.orbeon.oxf.util.MarkupUtils._
import org.orbeon.oxf.webapp.{SessionFacade, UserRolesFacade}
import org.slf4j.LoggerFactory

import scala.util.control.NonFatal

object FormRunnerAuth {

  val AllHeaderNamesLower = Set(
    Headers.OrbeonUsernameLower,
    Headers.OrbeonGroupLower,
    Headers.OrbeonRolesLower,
    Headers.OrbeonCredentialsLower
  )

  import Private._

  def getCredentialsAsHeadersUseSession(
    userRoles  : UserRolesFacade,
    session    : SessionFacade,
    getHeader  : String => List[String]
  ): List[(String, Array[String])] = {

    getCredentialsUseSession(userRoles, session, getHeader) match {
      case Some(credentials) =>
        val result = CredentialsSupport.toHeaders(credentials)
        Logger.debug(s"setting auth headers to: ${headersAsJSONString(result)}")
        result
      case None =>
        // Don't set any headers in case there is no username
        Logger.warn(s"not setting credentials headers because credentials are not found")
        Nil
    }
  }

  private object Private {

    val Logger = LoggerFactory.getLogger("org.orbeon.auth")

    val PropertyPrefix                      = "oxf.fr.authentication."

    val MethodPropertyName                  = PropertyPrefix + "method"
    val ContainerRolesPropertyName          = PropertyPrefix + "container.roles"
    val ContainerRolesSplitPropertyName     = PropertyPrefix + "container.roles.split"
    val HeaderUsernamePropertyName          = PropertyPrefix + "header.username"
    val HeaderRolesPropertyName             = PropertyPrefix + "header.roles"
    val HeaderRolesSplitPropertyName        = PropertyPrefix + "header.roles.split"
    val HeaderGroupPropertyName             = PropertyPrefix + "header.group"
    val HeaderRolesPropertyNamePropertyName = PropertyPrefix + "header.roles.property-name"
    val HeaderCredentialsPropertyName       = PropertyPrefix + "header.credentials"

    val NameValueMatch = "([^=]+)=([^=]+)".r

    sealed abstract class AuthMethod(override val entryName: String) extends EnumEntry
    object AuthMethod extends Enum[AuthMethod] {
       val values = findValues
       case object Container extends AuthMethod("container")
       case object Header    extends AuthMethod("header")
    }

    // Get the username, group and roles from the request, based on the Form Runner configuration.
    // The first time this is called, the result is stored into the required session. The subsequent times,
    // the value stored in the session is retrieved. This ensures that authentication information remains an
    // invariant for a given session.
    //
    // See https://github.com/orbeon/orbeon-forms/issues/2464
    // See also https://github.com/orbeon/orbeon-forms/issues/2632
    def getCredentialsUseSession(
      userRoles  : UserRolesFacade,
      session    : SessionFacade,
      getHeader  : String => List[String]
    ): Option[Credentials] =
      ServletPortletRequest.findCredentialsInSession(session) orElse {

        val newCredentialsOpt = findCredentialsFromContainerOrHeaders(userRoles, getHeader)

        // Only store the information into the session if we get user credentials. This handles the case of the initial
        // login. See: https://github.com/orbeon/orbeon-forms/issues/2732
        newCredentialsOpt foreach
          (ServletPortletRequest.storeCredentialsInSession(session, _))

        newCredentialsOpt
      }

    def headersAsJSONString(headers: List[(String, Array[String])]): String = {

      val headerAsJSONStrings =
        headers map {
          case (name, values) =>
            val valuesAsString = values.map(_.escapeJavaScript).mkString("""["""", """", """", """"]""")
            s""""$name": $valuesAsString"""
        }

      headerAsJSONStrings.mkString("{", ", ", "}")
    }

    def findCredentialsFromContainerOrHeaders(
      userRoles : UserRolesFacade,
      getHeader : String => List[String]
    ): Option[Credentials] = {

      val propertySet = Properties.instance.getPropertySet

      val requestedAuthMethod = propertySet.getString(MethodPropertyName, "container")

      AuthMethod.withNameOption(requestedAuthMethod) match {

        case Some(authMethod @ AuthMethod.Container) =>

          Logger.debug(s"using `$authMethod` method")

          val usernameOpt    = Option(userRoles.getRemoteUser)
          val rolesStringOpt = propertySet.getNonBlankString(ContainerRolesPropertyName)

          Logger.debug(s"usernameOpt: `$usernameOpt`, roles property: `$rolesStringOpt`")

          usernameOpt map { username =>

              // Wrap exceptions as Liferay throws if the role is not available instead of returning false
              def isUserInRole(role: String) =
                try userRoles.isUserInRole(role)
                catch { case NonFatal(_) => false}

              val rolesSplitRegex =
                propertySet.getString(ContainerRolesSplitPropertyName, """,|\s+""")

              val rolesList =
                for {
                  rolesString <- rolesStringOpt.toList
                  roleName    <- rolesString split rolesSplitRegex
                  if isUserInRole(roleName)
                } yield
                  SimpleRole(roleName)

              Credentials(
                username      = username,
                roles         = rolesList,
                group         = rolesList.headOption map (_.roleName),
                organizations = Nil
              )
          }

        case Some(authMethod @ AuthMethod.Header) =>

          Logger.debug(s"using `$authMethod` method")

          val headerPropertyName =
            propertySet.getNonBlankString(HeaderRolesPropertyNamePropertyName)

          def headerList(name: String) =
            propertySet.getNonBlankString(name).toList flatMap (p => getHeader(p.toLowerCase))

          val rolesSplit = propertySet.getString(HeaderRolesSplitPropertyName, """(\s*[,\|]\s*)+""")
          def splitRoles(value: String) = value split rolesSplit

          Logger.debug(s"using properties: $HeaderRolesPropertyNamePropertyName=`$headerPropertyName`, $HeaderRolesSplitPropertyName=`$rolesSplit`")

          // If configured, a header can have the form `name=value` where `name` is specified in a property
          def splitWithinRole(value: String) = headerPropertyName match {
            case Some(propertyName) =>
              value match {
                case NameValueMatch(`propertyName`, value) => List(value)
                case _                                     => Nil
              }
            case _ => List(value)
          }

          import org.orbeon.oxf.util.CoreUtils._

          // Credentials coming from the JSON-encoded HTTP header
          def fromCredentialsHeader =
            headerList(HeaderCredentialsPropertyName).headOption flatMap
            (CredentialsSupport.parseCredentials(_, decodeForHeader = true)) kestrel
            (_ => Logger.debug(s"found from credential headers"))

          // Credentials coming from individual headers (requires at least the username)
          def fromIndividualHeaders =
            headerList(HeaderUsernamePropertyName).headOption map { username =>

              // Roles: all headers with the given name are used, each header value is split, and result combined
              // See also: https://github.com/orbeon/orbeon-forms/issues/1690
              val roles =
                headerList(HeaderRolesPropertyName) flatMap splitRoles flatMap splitWithinRole map SimpleRole.apply

              Credentials(
                username      = username,
                roles         = roles,
                group         = headerList(HeaderGroupPropertyName).headOption,
                organizations = Nil
              )
            } kestrel
            (_ => Logger.debug(s"found from individual headers"))

          fromCredentialsHeader orElse fromIndividualHeaders

        case None =>
          throw new OXFException(s"'$MethodPropertyName' property: unsupported authentication method `$requestedAuthMethod`")
      }
    }
  }
}

{'fraction_non_alphanumeric': 0.08358225467418963, 'fraction_numerical': 0.002544529262086514, 'mean_word_length': 2.610744957060116, 'pattern_counts': {'":': 2, '<': 7, '<?xml version=': 0, '>': 41, 'https://': 8, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4556276', 'n_tokens_mistral': 4737, 'n_tokens_neox': 4557, 'n_words': 1237}