Newer
Older
GitBucket / src / main / scala / gitbucket / core / service / AccountFederationService.scala
@Hidetake Iwata Hidetake Iwata on 25 Jan 2018 3 KB Refactor
package gitbucket.core.service

import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.{AccountFederations, Accounts}
import gitbucket.core.model.{Account, AccountFederation}
import gitbucket.core.util.SyntaxSugars.~
import org.slf4j.LoggerFactory

trait AccountFederationService {
  self: AccountService =>

  private val logger = LoggerFactory.getLogger(classOf[AccountFederationService])

  /**
    * Get or create a user account federated with OIDC or SAML IdP.
    *
    * @param issuer            Issuer
    * @param subject           Subject
    * @param mailAddress       Mail address
    * @param preferredUserName Username (if this is none, username will be generated from the mail address)
    * @param fullName          Fullname (defaults to username)
    * @return Account
    */
  def getOrCreateFederatedUser(issuer: String,
                               subject: String,
                               mailAddress: String,
                               preferredUserName: Option[String],
                               fullName: Option[String])(implicit s: Session): Option[Account] =
    getAccountByFederation(issuer, subject) match {
      case Some(account) if !account.isRemoved =>
        Some(account)
      case Some(account) =>
        logger.info(s"Federated user found but disabled: userName=${account.userName}, isRemoved=${account.isRemoved}")
        None
      case None =>
        findAvailableUserName(preferredUserName, mailAddress) flatMap { userName =>
          createAccount(userName, "", fullName.getOrElse(userName), mailAddress, isAdmin = false, None, None)
          createAccountFederation(issuer, subject, userName)
          getAccountByUserName(userName)
        }
    }

  private def extractSafeStringForUserName(s: String) = """^[a-zA-Z0-9][a-zA-Z0-9\-_.]*""".r.findPrefixOf(s)

  /**
    * Find an available username from the preferred username or mail address.
    *
    * @param mailAddress       Mail address
    * @param preferredUserName Username
    * @return Available username
    */
  private def findAvailableUserName(preferredUserName: Option[String], mailAddress: String)(implicit s: Session): Option[String] = {
    preferredUserName.flatMap(n => extractSafeStringForUserName(n)).orElse(extractSafeStringForUserName(mailAddress)) match {
      case Some(safeUserName) =>
        getAccountByUserName(safeUserName, includeRemoved = true) match {
          case None => Some(safeUserName)
          case Some(_) =>
            logger.info(s"User ($safeUserName) already exists. preferredUserName=$preferredUserName, mailAddress=$mailAddress")
            None
        }
      case None =>
        logger.info(s"Could not extract username from preferredUserName=$preferredUserName, mailAddress=$mailAddress")
        None
    }
  }

  def getAccountByFederation(issuer: String, subject: String)(implicit s: Session): Option[Account] =
    AccountFederations.filter(_.byPrimaryKey(issuer, subject))
      .join(Accounts).on { case af ~ ac => af.userName === ac.userName }
      .map { case _ ~ ac => ac }
      .firstOption

  def createAccountFederation(issuer: String, subject: String, userName: String)(implicit s: Session): Unit =
    AccountFederations insert AccountFederation(issuer, subject, userName)
}