All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gitbucket.core.controller.SystemSettingsController.scala Maven / Gradle / Ivy

The newest version!
package gitbucket.core.controller

import java.io.FileInputStream
import gitbucket.core.admin.html
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService._
import gitbucket.core.service.{AccountService, RepositoryService}
import gitbucket.core.ssh.SshServer
import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.{AdminAuthenticator, Mailer}
import org.apache.commons.io.IOUtils
import org.apache.commons.mail.EmailException
import org.json4s.jackson.Serialization
import org.scalatra._
import org.scalatra.forms._
import org.scalatra.i18n.Messages

import scala.collection.mutable.ListBuffer
import scala.util.Using

class SystemSettingsController
    extends SystemSettingsControllerBase
    with AccountService
    with RepositoryService
    with AdminAuthenticator

case class Table(name: String, columns: Seq[Column])
case class Column(name: String, primaryKey: Boolean)

trait SystemSettingsControllerBase extends AccountManagementControllerBase {
  self: AccountService with RepositoryService with AdminAuthenticator =>

  private val form = mapping(
    "baseUrl" -> trim(label("Base URL", optional(text()))),
    "information" -> trim(label("Information", optional(text()))),
    "basicBehavior" -> mapping(
      "allowAccountRegistration" -> trim(label("Account registration", boolean())),
      "allowResetPassword" -> trim(label("Reset password", boolean())),
      "allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
      "isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())),
      "repositoryOperation" -> mapping(
        "create" -> trim(label("Allow all users to create repository", boolean())),
        "delete" -> trim(label("Allow all users to delete repository", boolean())),
        "rename" -> trim(label("Allow all users to rename repository", boolean())),
        "transfer" -> trim(label("Allow all users to transfer repository", boolean())),
        "fork" -> trim(label("Allow all users to fork repository", boolean()))
      )(RepositoryOperation.apply),
      "gravatar" -> trim(label("Gravatar", boolean())),
      "notification" -> trim(label("Notification", boolean())),
      "limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
    )(BasicBehavior.apply),
    "ssh" -> mapping(
      "enabled" -> trim(label("SSH access", boolean())),
      "bindAddress" -> mapping(
        "host" -> trim(label("Bind SSH host", optional(text()))),
        "port" -> trim(label("Bind SSH port", optional(number()))),
      )((hostOption, portOption) =>
        hostOption.map(h => SshAddress(h, portOption.getOrElse(DefaultSshPort), GenericSshUser))
      ),
      "publicAddress" -> mapping(
        "host" -> trim(label("Public SSH host", optional(text()))),
        "port" -> trim(label("Public SSH port", optional(number()))),
      )((hostOption, portOption) =>
        hostOption.map(h => SshAddress(h, portOption.getOrElse(PublicSshPort), GenericSshUser))
      ),
    )(Ssh.apply),
    "useSMTP" -> trim(label("SMTP", boolean())),
    "smtp" -> optionalIfNotChecked(
      "useSMTP",
      mapping(
        "host" -> trim(label("SMTP Host", text(required))),
        "port" -> trim(label("SMTP Port", optional(number()))),
        "user" -> trim(label("SMTP User", optional(text()))),
        "password" -> trim(label("SMTP Password", optional(text()))),
        "ssl" -> trim(label("Enable SSL", optional(boolean()))),
        "starttls" -> trim(label("Enable STARTTLS", optional(boolean()))),
        "fromAddress" -> trim(label("FROM Address", optional(text()))),
        "fromName" -> trim(label("FROM Name", optional(text())))
      )(Smtp.apply)
    ),
    "ldapAuthentication" -> trim(label("LDAP", boolean())),
    "ldap" -> optionalIfNotChecked(
      "ldapAuthentication",
      mapping(
        "host" -> trim(label("LDAP host", text(required))),
        "port" -> trim(label("LDAP port", optional(number()))),
        "bindDN" -> trim(label("Bind DN", optional(text()))),
        "bindPassword" -> trim(label("Bind Password", optional(text()))),
        "baseDN" -> trim(label("Base DN", text(required))),
        "userNameAttribute" -> trim(label("User name attribute", text(required))),
        "additionalFilterCondition" -> trim(label("Additional filter condition", optional(text()))),
        "fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
        "mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
        "tls" -> trim(label("Enable TLS", optional(boolean()))),
        "ssl" -> trim(label("Enable SSL", optional(boolean()))),
        "keystore" -> trim(label("Keystore", optional(text())))
      )(Ldap.apply)
    ),
    "oidcAuthentication" -> trim(label("OIDC", boolean())),
    "oidc" -> optionalIfNotChecked(
      "oidcAuthentication",
      mapping(
        "issuer" -> trim(label("Issuer", text(required))),
        "clientID" -> trim(label("Client ID", text(required))),
        "clientSecret" -> trim(label("Client secret", text(required))),
        "jwsAlgorithm" -> trim(label("Signature algorithm", optional(text())))
      )(OIDC.apply)
    ),
    "skinName" -> trim(label("AdminLTE skin name", text(required))),
    "userDefinedCss" -> trim(label("User-defined CSS", optional(text()))),
    "showMailAddress" -> trim(label("Show mail address", boolean())),
    "webhook" -> mapping(
      "blockPrivateAddress" -> trim(label("Block private address", boolean())),
      "whitelist" -> trim(label("Whitelist", multiLineText()))
    )(WebHook.apply),
    "upload" -> mapping(
      "maxFileSize" -> trim(label("Max file size", long(required))),
      "timeout" -> trim(label("Timeout", long(required))),
      "largeMaxFileSize" -> trim(label("Max file size for large file", long(required))),
      "largeTimeout" -> trim(label("Timeout for large file", long(required)))
    )(Upload.apply),
    "repositoryViewer" -> mapping(
      "maxFiles" -> trim(label("Max files", number(required))),
      "maxDiffFiles" -> trim(label("Max diff files", number(required))),
      "maxDiffLines" -> trim(label("Max diff lines", number(required)))
    )(RepositoryViewerSettings.apply),
    "defaultBranch" -> trim(label("Default branch", text(required)))
  )(SystemSettings.apply).verifying { settings =>
    Vector(
      if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
        Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
      } else None,
      if (settings.ssh.enabled && settings.ssh.bindAddress.isEmpty) {
        Some("ssh.bindAddress.host" -> "SSH bind host is required if SSH access is enabled.")
      } else None
    ).flatten
  }

  private val sendMailForm = mapping(
    "smtp" -> mapping(
      "host" -> trim(label("SMTP Host", text(required))),
      "port" -> trim(label("SMTP Port", optional(number()))),
      "user" -> trim(label("SMTP User", optional(text()))),
      "password" -> trim(label("SMTP Password", optional(text()))),
      "ssl" -> trim(label("Enable SSL", optional(boolean()))),
      "starttls" -> trim(label("Enable STARTTLS", optional(boolean()))),
      "fromAddress" -> trim(label("FROM Address", optional(text()))),
      "fromName" -> trim(label("FROM Name", optional(text())))
    )(Smtp.apply),
    "testAddress" -> trim(label("", text(required)))
  )(SendMailForm.apply)

  case class SendMailForm(smtp: Smtp, testAddress: String)

  case class DataExportForm(tableNames: List[String])

  case class NewUserForm(
    userName: String,
    password: String,
    fullName: String,
    mailAddress: String,
    extraMailAddresses: List[String],
    isAdmin: Boolean,
    description: Option[String],
    url: Option[String],
    fileId: Option[String]
  )

  case class EditUserForm(
    userName: String,
    password: Option[String],
    fullName: String,
    mailAddress: String,
    extraMailAddresses: List[String],
    isAdmin: Boolean,
    description: Option[String],
    url: Option[String],
    fileId: Option[String],
    clearImage: Boolean,
    isRemoved: Boolean
  )

  case class NewGroupForm(
    groupName: String,
    description: Option[String],
    url: Option[String],
    fileId: Option[String],
    members: String
  )

  case class EditGroupForm(
    groupName: String,
    description: Option[String],
    url: Option[String],
    fileId: Option[String],
    members: String,
    clearImage: Boolean,
    isRemoved: Boolean
  )

  val newUserForm = mapping(
    "userName" -> trim(label("Username", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
    "password" -> trim(label("Password", text(required, maxlength(40)))),
    "fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
    "mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress()))),
    "extraMailAddresses" -> list(
      trim(label("Additional Mail Address", text(maxlength(100), uniqueExtraMailAddress("userName"))))
    ),
    "isAdmin" -> trim(label("User Type", boolean())),
    "description" -> trim(label("bio", optional(text()))),
    "url" -> trim(label("URL", optional(text(maxlength(200))))),
    "fileId" -> trim(label("File ID", optional(text())))
  )(NewUserForm.apply)

  val editUserForm = mapping(
    "userName" -> trim(label("Username", text(required, maxlength(100), identifier))),
    "password" -> trim(label("Password", optional(text(maxlength(40))))),
    "fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
    "mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress("userName")))),
    "extraMailAddresses" -> list(
      trim(label("Additional Mail Address", text(maxlength(100), uniqueExtraMailAddress("userName"))))
    ),
    "isAdmin" -> trim(label("User Type", boolean())),
    "description" -> trim(label("bio", optional(text()))),
    "url" -> trim(label("URL", optional(text(maxlength(200))))),
    "fileId" -> trim(label("File ID", optional(text()))),
    "clearImage" -> trim(label("Clear image", boolean())),
    "removed" -> trim(label("Disable", boolean(disableByNotYourself("userName"))))
  )(EditUserForm.apply)

  val newGroupForm = mapping(
    "groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
    "description" -> trim(label("Group description", optional(text()))),
    "url" -> trim(label("URL", optional(text(maxlength(200))))),
    "fileId" -> trim(label("File ID", optional(text()))),
    "members" -> trim(label("Members", text(required, members)))
  )(NewGroupForm.apply)

  val editGroupForm = mapping(
    "groupName" -> trim(label("Group name", text(required, maxlength(100), identifier))),
    "description" -> trim(label("Group description", optional(text()))),
    "url" -> trim(label("URL", optional(text(maxlength(200))))),
    "fileId" -> trim(label("File ID", optional(text()))),
    "members" -> trim(label("Members", text(required, members))),
    "clearImage" -> trim(label("Clear image", boolean())),
    "removed" -> trim(label("Disable", boolean()))
  )(EditGroupForm.apply)

  get("/admin/dbviewer")(adminOnly {
    val conn = request2Session(request).conn
    val meta = conn.getMetaData
    val tables = ListBuffer[Table]()
    Using.resource(meta.getTables(null, "%", "%", Array("TABLE", "VIEW"))) { rs =>
      while (rs.next()) {
        val tableName = rs.getString("TABLE_NAME")

        val pkColumns = ListBuffer[String]()
        Using.resource(meta.getPrimaryKeys(null, null, tableName)) { rs =>
          while (rs.next()) {
            pkColumns += rs.getString("COLUMN_NAME").toUpperCase
          }
        }

        val columns = ListBuffer[Column]()
        Using.resource(meta.getColumns(null, "%", tableName, "%")) { rs =>
          while (rs.next()) {
            val columnName = rs.getString("COLUMN_NAME").toUpperCase
            columns += Column(columnName, pkColumns.contains(columnName))
          }
        }

        tables += Table(tableName.toUpperCase, columns.toSeq)
      }
    }
    html.dbviewer(tables.toSeq)
  })

  post("/admin/dbviewer/_query")(adminOnly {
    contentType = formats("json")
    params.get("query").collectFirst {
      case query if query.trim.nonEmpty =>
        val trimmedQuery = query.trim
        if (trimmedQuery.nonEmpty) {
          try {
            val conn = request2Session(request).conn
            Using.resource(conn.prepareStatement(query)) { stmt =>
              if (trimmedQuery.toUpperCase.startsWith("SELECT")) {
                Using.resource(stmt.executeQuery()) { rs =>
                  val meta = rs.getMetaData
                  val columns = for (i <- 1 to meta.getColumnCount) yield {
                    meta.getColumnName(i)
                  }
                  val result = ListBuffer[Map[String, String]]()
                  while (rs.next()) {
                    val row = columns.map { columnName =>
                      columnName -> Option(rs.getObject(columnName)).map(_.toString).getOrElse("")
                    }.toMap
                    result += row
                  }
                  Ok(Serialization.write(Map("type" -> "query", "columns" -> columns, "rows" -> result)))
                }
              } else {
                val rows = stmt.executeUpdate()
                Ok(Serialization.write(Map("type" -> "update", "rows" -> rows)))
              }
            }
          } catch {
            case e: Exception =>
              Ok(Serialization.write(Map("type" -> "error", "message" -> e.toString)))
          }
        }
    } getOrElse Ok(Serialization.write(Map("type" -> "error", "message" -> "query is empty")))
  })

  get("/admin/system")(adminOnly {
    html.settings(flash.get("info"))
  })

  post("/admin/system", form)(adminOnly { form =>
    saveSystemSettings(form)

    if (
      form.ssh.bindAddress != context.settings.sshBindAddress || form.ssh.publicAddress != context.settings.sshPublicAddress
    ) {
      SshServer.stop()
      for {
        bindAddress <- form.ssh.bindAddress
        publicAddress <- form.ssh.publicAddress.orElse(form.ssh.bindAddress)
        baseUrl <- form.baseUrl
      } SshServer.start(bindAddress, publicAddress, baseUrl)
    }

    flash.update("info", "System settings has been updated.")
    redirect("/admin/system")
  })

  post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
    try {
      new Mailer(
        context.settings.copy(
          smtp = Some(form.smtp),
          basicBehavior = context.settings.basicBehavior.copy(notification = true)
        )
      ).send(
        to = form.testAddress,
        subject = "Test message from GitBucket",
        textMsg = "This is a test message from GitBucket.",
        htmlMsg = None,
        loginAccount = context.loginAccount
      )

      "Test mail has been sent to: " + form.testAddress

    } catch {
      case e: EmailException => s"[Error] ${e.getCause}"
      case e: Exception      => s"[Error] ${e.toString}"
    }
  })

  get("/admin/plugins")(adminOnly {
    html.plugins(PluginRegistry().getPlugins(), flash.get("info"))
  })

  post("/admin/plugins/_reload")(adminOnly {
    PluginRegistry.reload(request.getServletContext(), loadSystemSettings(), request2Session(request).conn)
    flash.update("info", "All plugins were reloaded.")
    redirect("/admin/plugins")
  })

  post("/admin/plugins/:pluginId/_uninstall")(adminOnly {
    val pluginId = params("pluginId")

    if (PluginRegistry().getPlugins().exists(_.pluginId == pluginId)) {
      PluginRegistry
        .uninstall(pluginId, request.getServletContext, loadSystemSettings(), request2Session(request).conn)
      flash.update("info", s"${pluginId} was uninstalled.")
    }

    redirect("/admin/plugins")
  })

  get("/admin/users")(adminOnly {
    val includeRemoved = params.get("includeRemoved").exists(_.toBoolean)
    val includeGroups = params.get("includeGroups").exists(_.toBoolean)
    val users = getAllUsers(includeRemoved, includeGroups)
    val members = users.collect {
      case account if (account.isGroupAccount) =>
        account.userName -> getGroupMembers(account.userName).map(_.userName)
    }.toMap

    html.userlist(users, members, includeRemoved, includeGroups)
  })

  get("/admin/users/_newuser")(adminOnly {
    html.user(None, Nil)
  })

  post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
    createAccount(
      form.userName,
      pbkdf2_sha256(form.password),
      form.fullName,
      form.mailAddress,
      form.isAdmin,
      form.description,
      form.url
    )
    updateImage(form.userName, form.fileId, false)
    updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses.filter(_ != ""))
    redirect("/admin/users")
  })

  get("/admin/users/:userName/_edituser")(adminOnly {
    val userName = params("userName")
    val extraMails = getAccountExtraMailAddresses(userName)
    html.user(getAccountByUserName(userName, true), extraMails, flash.get("error"))
  })

  post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
    val userName = params("userName")
    getAccountByUserName(userName, true).map { account =>
      if (account.isAdmin && (form.isRemoved || !form.isAdmin) && isLastAdministrator(account)) {
        flash.update("error", "Account can't be turned off because this is last one administrator.")
        redirect(s"/admin/users/${userName}/_edituser")
      } else {
        if (form.isRemoved) {
          // Remove repositories
          //        getRepositoryNamesOfUser(userName).foreach { repositoryName =>
          //          deleteRepository(userName, repositoryName)
          //          FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
          //          FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
          //          FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
          //        }
          // Remove from GROUP_MEMBER and COLLABORATOR
          removeUserRelatedData(userName)
        }

        updateAccount(
          account.copy(
            password = form.password.map(pbkdf2_sha256).getOrElse(account.password),
            fullName = form.fullName,
            mailAddress = form.mailAddress,
            isAdmin = form.isAdmin,
            description = form.description,
            url = form.url,
            isRemoved = form.isRemoved
          )
        )

        updateImage(userName, form.fileId, form.clearImage)
        updateAccountExtraMailAddresses(userName, form.extraMailAddresses.filter(_ != ""))

        // call hooks
        if (form.isRemoved) PluginRegistry().getAccountHooks.foreach(_.deleted(userName))

        redirect("/admin/users")
      }
    } getOrElse NotFound()
  })

  get("/admin/users/_newgroup")(adminOnly {
    html.usergroup(None, Nil)
  })

  post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
    createGroup(form.groupName, form.description, form.url)
    updateGroupMembers(
      form.groupName,
      form.members
        .split(",")
        .map {
          _.split(":") match {
            case Array(userName, isManager) => (userName, isManager.toBoolean)
          }
        }
        .toList
    )
    updateImage(form.groupName, form.fileId, false)
    redirect("/admin/users")
  })

  get("/admin/users/:groupName/_editgroup")(adminOnly {
    val groupName = params("groupName")
    html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName))
  })

  post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
    val groupName = params("groupName")
    val members = form.members
      .split(",")
      .map {
        _.split(":") match {
          case Array(userName, isManager) => (userName, isManager.toBoolean)
        }
      }
      .toList

    getAccountByUserName(groupName, true).map { account =>
      updateGroup(groupName, form.description, form.url, form.isRemoved)

      if (form.isRemoved) {
        // Remove from GROUP_MEMBER
        updateGroupMembers(form.groupName, Nil)
//          // Remove repositories
//          getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
//            deleteRepository(groupName, repositoryName)
//            FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
//            FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
//            FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
//          }
      } else {
        // Update GROUP_MEMBER
        updateGroupMembers(form.groupName, members)
//          // Update COLLABORATOR for group repositories
//          getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
//            removeCollaborators(form.groupName, repositoryName)
//            members.foreach { case (userName, isManager) =>
//              addCollaborator(form.groupName, repositoryName, userName)
//            }
//          }
      }

      updateImage(form.groupName, form.fileId, form.clearImage)
      redirect("/admin/users")

    } getOrElse NotFound()
  })

  get("/admin/data")(adminOnly {
    import gitbucket.core.util.JDBCUtil._
    val session = request2Session(request)
    html.data(session.conn.allTableNames())
  })

  post("/admin/export")(adminOnly {
    import gitbucket.core.util.JDBCUtil._
    val file = request2Session(request).conn.exportAsSQL(request.getParameterValues("tableNames").toSeq)

    contentType = "application/octet-stream"
    response.setHeader("Content-Disposition", "attachment; filename=" + file.getName)
    response.setContentLength(file.length.toInt)

    Using.resource(new FileInputStream(file)) { in =>
      IOUtils.copy(in, response.outputStream)
    }

    ()
  })

  private def multiLineText(constraints: Constraint*): SingleValueType[Seq[String]] =
    new SingleValueType[Seq[String]](constraints*) {
      def convert(value: String, messages: Messages): Seq[String] = {
        if (value == null) {
          Nil
        } else {
          value.split("\n").toIndexedSeq.map(_.trim)
        }
      }
    }

  private def members: Constraint =
    new Constraint() {
      override def validate(name: String, value: String, messages: Messages): Option[String] = {
        if (
          value.split(",").exists {
            _.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
          }
        ) None
        else Some("Must select one manager at least.")
      }
    }

  protected def disableByNotYourself(paramName: String): Constraint =
    new Constraint() {
      override def validate(name: String, value: String, messages: Messages): Option[String] = {
        for {
          userName <- params.get(paramName)
          loginAccount <- context.loginAccount
          if userName == loginAccount.userName && params.get("removed") == Some("true")
        } yield "You can't disable your account yourself"
      }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy