mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-01 19:15:59 +01:00
Merge branch 'ldap-auth'
This commit is contained in:
@@ -36,6 +36,7 @@ object MyBuild extends Build {
|
||||
"org.pegdown" % "pegdown" % "1.3.0",
|
||||
"org.apache.commons" % "commons-compress" % "1.5",
|
||||
"com.typesafe.slick" %% "slick" % "1.0.1",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.3.171",
|
||||
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.StringUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
|
||||
@@ -24,19 +23,11 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
|
||||
}
|
||||
|
||||
post("/signin", form){ form =>
|
||||
getAccountByUserName(form.userName).collect {
|
||||
case account if(!account.isGroupAccount && account.password == sha1(form.password)) => {
|
||||
session.setAttribute("LOGIN_ACCOUNT", account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
session.get("REDIRECT").map { redirectUrl =>
|
||||
session.removeAttribute("REDIRECT")
|
||||
redirect(redirectUrl.asInstanceOf[String])
|
||||
}.getOrElse {
|
||||
redirect("/")
|
||||
}
|
||||
}
|
||||
} getOrElse redirect("/signin")
|
||||
val settings = loadSystemSettings()
|
||||
authenticate(loadSystemSettings(), form.userName, form.password) match {
|
||||
case Some(account) => signin(account)
|
||||
case None => redirect("/signin")
|
||||
}
|
||||
}
|
||||
|
||||
get("/signout"){
|
||||
@@ -44,4 +35,19 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
|
||||
redirect("/")
|
||||
}
|
||||
|
||||
/**
|
||||
* Set account information into HttpSession and redirect.
|
||||
*/
|
||||
private def signin(account: model.Account) = {
|
||||
session.setAttribute("LOGIN_ACCOUNT", account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
session.get("REDIRECT").map { redirectUrl =>
|
||||
session.removeAttribute("REDIRECT")
|
||||
redirect(redirectUrl.asInstanceOf[String])
|
||||
}.getOrElse {
|
||||
redirect("/")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,12 +17,22 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"smtp" -> optionalIfNotChecked("notification", 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())))
|
||||
)(Smtp.apply))
|
||||
"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())))
|
||||
)(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", text(required))),
|
||||
"bindPassword" -> trim(label("Bind Password", text(required))),
|
||||
"baseDN" -> trim(label("Base DN", text(required))),
|
||||
"userNameAttribute" -> trim(label("User name attribute", text(required))),
|
||||
"mailAttribute" -> trim(label("Mail address attribute", text(required)))
|
||||
)(Ldap.apply))
|
||||
)(SystemSettings.apply)
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,48 @@ package service
|
||||
import model._
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import Database.threadLocalSession
|
||||
import service.SystemSettingsService.SystemSettings
|
||||
import util.StringUtil._
|
||||
import model.GroupMember
|
||||
import scala.Some
|
||||
import model.Account
|
||||
import util.LDAPUtil
|
||||
|
||||
trait AccountService {
|
||||
|
||||
def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
|
||||
if(settings.ldapAuthentication){
|
||||
ldapAuthentication(settings, userName, password)
|
||||
} else {
|
||||
defaultAuthentication(userName, password)
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate by internal database.
|
||||
*/
|
||||
private def defaultAuthentication(userName: String, password: String) = {
|
||||
getAccountByUserName(userName).collect {
|
||||
case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account)
|
||||
} getOrElse None
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate by LDAP.
|
||||
*/
|
||||
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
|
||||
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
|
||||
case Right(mailAddress) => {
|
||||
// Create or update account by LDAP information
|
||||
getAccountByUserName(userName) match {
|
||||
case Some(x) => updateAccount(x.copy(mailAddress = mailAddress))
|
||||
case None => createAccount(userName, "", mailAddress, false, None)
|
||||
}
|
||||
getAccountByUserName(userName)
|
||||
}
|
||||
case Left(errorMessage) => defaultAuthentication(userName, password)
|
||||
}
|
||||
}
|
||||
|
||||
def getAccountByUserName(userName: String): Option[Account] =
|
||||
Query(Accounts) filter(_.userName is userName.bind) firstOption
|
||||
|
||||
|
||||
@@ -19,6 +19,18 @@ trait SystemSettingsService {
|
||||
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
|
||||
}
|
||||
}
|
||||
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
|
||||
if(settings.ldapAuthentication){
|
||||
settings.ldap.map { ldap =>
|
||||
props.setProperty(LdapHost, ldap.host)
|
||||
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
|
||||
props.setProperty(LdapBindDN, ldap.bindDN)
|
||||
props.setProperty(LdapBindPassword, ldap.bindPassword)
|
||||
props.setProperty(LdapBaseDN, ldap.baseDN)
|
||||
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
||||
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
|
||||
}
|
||||
}
|
||||
props.store(new java.io.FileOutputStream(GitBucketConf), null)
|
||||
}
|
||||
|
||||
@@ -41,6 +53,19 @@ trait SystemSettingsService {
|
||||
getOptionValue[Boolean](props, SmtpSsl, None)))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
getValue(props, LdapAuthentication, false),
|
||||
if(getValue(props, LdapAuthentication, false)){
|
||||
Some(Ldap(
|
||||
getValue(props, LdapHost, ""),
|
||||
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
|
||||
getValue(props, LdapBindDN, ""),
|
||||
getValue(props, LdapBindPassword, ""),
|
||||
getValue(props, LdapBaseDN, ""),
|
||||
getValue(props, LdapUserNameAttribute, ""),
|
||||
getValue(props, LdapMailAddressAttribute, "")))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -54,8 +79,19 @@ object SystemSettingsService {
|
||||
allowAccountRegistration: Boolean,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
smtp: Option[Smtp]
|
||||
)
|
||||
smtp: Option[Smtp],
|
||||
ldapAuthentication: Boolean,
|
||||
ldap: Option[Ldap])
|
||||
|
||||
case class Ldap(
|
||||
host: String,
|
||||
port: Option[Int],
|
||||
bindDN: String,
|
||||
bindPassword: String,
|
||||
baseDN: String,
|
||||
userNameAttribute: String,
|
||||
mailAttribute: String)
|
||||
|
||||
case class Smtp(
|
||||
host: String,
|
||||
port: Option[Int],
|
||||
@@ -63,6 +99,8 @@ object SystemSettingsService {
|
||||
password: Option[String],
|
||||
ssl: Option[Boolean])
|
||||
|
||||
val DefaultLdapPort = 389
|
||||
|
||||
private val AllowAccountRegistration = "allow_account_registration"
|
||||
private val Gravatar = "gravatar"
|
||||
private val Notification = "notification"
|
||||
@@ -71,6 +109,14 @@ object SystemSettingsService {
|
||||
private val SmtpUser = "smtp.user"
|
||||
private val SmtpPassword = "smtp.password"
|
||||
private val SmtpSsl = "smtp.ssl"
|
||||
private val LdapAuthentication = "ldap_authentication"
|
||||
private val LdapHost = "ldap.host"
|
||||
private val LdapPort = "ldap.port"
|
||||
private val LdapBindDN = "ldap.bindDN"
|
||||
private val LdapBindPassword = "ldap.bind_password"
|
||||
private val LdapBaseDN = "ldap.baseDN"
|
||||
private val LdapUserNameAttribute = "ldap.username_attribute"
|
||||
private val LdapMailAddressAttribute = "ldap.mail_attribute"
|
||||
|
||||
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
|
||||
val value = props.getProperty(key)
|
||||
|
||||
@@ -2,14 +2,13 @@ package servlet
|
||||
|
||||
import javax.servlet._
|
||||
import javax.servlet.http._
|
||||
import util.StringUtil._
|
||||
import service.{AccountService, RepositoryService}
|
||||
import service.{SystemSettingsService, AccountService, RepositoryService}
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
|
||||
*/
|
||||
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService {
|
||||
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter])
|
||||
|
||||
@@ -58,12 +57,12 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
||||
}
|
||||
}
|
||||
|
||||
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = {
|
||||
getAccountByUserName(username).map { account =>
|
||||
account.password == sha1(password) && hasWritePermission(repository.owner, repository.name, Some(account))
|
||||
} getOrElse false
|
||||
}
|
||||
|
||||
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean =
|
||||
authenticate(loadSystemSettings(), username, password) match {
|
||||
case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account))
|
||||
case None => false
|
||||
}
|
||||
|
||||
private def requireAuth(response: HttpServletResponse): Unit = {
|
||||
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
|
||||
|
||||
97
src/main/scala/util/LDAPUtil.scala
Normal file
97
src/main/scala/util/LDAPUtil.scala
Normal file
@@ -0,0 +1,97 @@
|
||||
package util
|
||||
|
||||
import service.SystemSettingsService.Ldap
|
||||
import service.SystemSettingsService
|
||||
import com.novell.ldap.{LDAPReferralException, LDAPEntry, LDAPConnection}
|
||||
|
||||
/**
|
||||
* Utility for LDAP authentication.
|
||||
*/
|
||||
object LDAPUtil {
|
||||
|
||||
private val LDAP_VERSION: Int = 3
|
||||
|
||||
/**
|
||||
* Try authentication by LDAP using given configuration.
|
||||
* Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage).
|
||||
*/
|
||||
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = {
|
||||
bind(
|
||||
ldapSettings.host,
|
||||
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||
ldapSettings.bindDN,
|
||||
ldapSettings.bindPassword
|
||||
) match {
|
||||
case Some(conn) => {
|
||||
withConnection(conn) { conn =>
|
||||
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
|
||||
case Some(userDN) => userAuthentication(ldapSettings, userDN, password)
|
||||
case None => Left("User does not exist")
|
||||
}
|
||||
}
|
||||
}
|
||||
case None => Left("System LDAP authentication failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = {
|
||||
bind(
|
||||
ldapSettings.host,
|
||||
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||
userDN,
|
||||
password
|
||||
) match {
|
||||
case Some(conn) => {
|
||||
withConnection(conn) { conn =>
|
||||
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
|
||||
case Some(mailAddress) => Right(mailAddress)
|
||||
case None => Left("Can't find mail address.")
|
||||
}
|
||||
}
|
||||
}
|
||||
case None => Left("User LDAP Authentication Failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private def bind(host: String, port: Int, dn: String, password: String): Option[LDAPConnection] = {
|
||||
val conn: LDAPConnection = new LDAPConnection
|
||||
try {
|
||||
conn.connect(host, port)
|
||||
conn.bind(LDAP_VERSION, dn, password.getBytes)
|
||||
Some(conn)
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
if (conn.isConnected) conn.disconnect()
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = {
|
||||
try {
|
||||
f(conn)
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
|
||||
val results = conn.search(baseDN, LDAPConnection.SCOPE_SUB, userNameAttribute + "=" + userName, null, false)
|
||||
(for(i <- 0 to results.getCount) yield try {
|
||||
Some(results.next)
|
||||
} catch {
|
||||
case ex: LDAPReferralException => None // NOTE(tanacasino): Referral follow is off. so ignores it.(for AD)
|
||||
}).flatten.collectFirst {
|
||||
case x if(x != null) => x.getDN
|
||||
}
|
||||
}
|
||||
|
||||
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = {
|
||||
val results = conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)
|
||||
if (results.hasMore) {
|
||||
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,15 +18,17 @@
|
||||
<span id="error-userName" class="error"></span>
|
||||
</fieldset>
|
||||
}
|
||||
<fieldset>
|
||||
<label for="password"><strong>Password</strong>
|
||||
@if(account.nonEmpty){
|
||||
(Input to change password)
|
||||
}
|
||||
</label>
|
||||
<input type="password" name="password" id="password" value=""/>
|
||||
<span id="error-password" class="error"></span>
|
||||
</fieldset>
|
||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||
<fieldset>
|
||||
<label for="password"><strong>Password</strong>
|
||||
@if(account.nonEmpty){
|
||||
(Input to change password)
|
||||
}
|
||||
</label>
|
||||
<input type="password" name="password" id="password" value=""/>
|
||||
<span id="error-password" class="error"></span>
|
||||
</fieldset>
|
||||
}
|
||||
<fieldset>
|
||||
<label for="mailAddress"><strong>Mail Address</strong></label>
|
||||
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<div class="box">
|
||||
<div class="box-header">System Settings</div>
|
||||
<div class="box-content">
|
||||
<!--====================================================================-->
|
||||
<!-- Account registration -->
|
||||
<!--====================================================================-->
|
||||
<label><strong>Account registration</strong></label>
|
||||
<fieldset>
|
||||
<label>
|
||||
@@ -19,6 +22,9 @@
|
||||
<strong>Deny</strong> - Only administrators can create account.
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Services -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><strong>Services</strong></label>
|
||||
<fieldset>
|
||||
@@ -27,6 +33,71 @@
|
||||
Gravatar
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Authentication -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><strong>Authentication</strong></label>
|
||||
<fieldset>
|
||||
<label>
|
||||
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
|
||||
LDAP
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="form-horizontal ldap">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapHost">LDAP Host</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapHost" name="ldap.host" value="@settings.ldap.map(_.host)"/>
|
||||
<span id="error-ldap_host" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapPort">LDAP Port</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapPort" name="ldap.port" class="input-mini" value="@settings.ldap.map(_.port)"/>
|
||||
<span id="error-ldap_port" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapBindDN">Bind DN</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapBindDN" name="ldap.bindDN" value="@settings.ldap.map(_.bindDN)"/>
|
||||
<span id="error-ldap_bindDN" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapBindPassword">Bind Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" value="@settings.ldap.map(_.bindPassword)"/>
|
||||
<span id="error-ldap_bindPassword" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapBaseDN">Base DN</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapBaseDN" name="ldap.baseDN" value="@settings.ldap.map(_.baseDN)"/>
|
||||
<span id="error-ldap_baseDN" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapUserNameAttribute">User name attribute</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" value="@settings.ldap.map(_.userNameAttribute)"/>
|
||||
<span id="error-ldap_userNameAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" value="@settings.ldap.map(_.mailAttribute)"/>
|
||||
<span id="error-ldap_mailAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Notification email -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><strong>Notification email</strong></label>
|
||||
<fieldset>
|
||||
@@ -35,7 +106,7 @@
|
||||
Send notifications
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-horizontal notification">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="smtpHost">SMTP Host</label>
|
||||
<div class="controls">
|
||||
@@ -81,7 +152,11 @@
|
||||
<script>
|
||||
$(function(){
|
||||
$('#notification').change(function(){
|
||||
$('.form-horizontal input').prop('disabled', !$(this).prop('checked'));
|
||||
$('.notification input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
|
||||
$('#ldapAuthentication').change(function(){
|
||||
$('.ldap input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
});
|
||||
</script>
|
||||
@@ -10,16 +10,18 @@
|
||||
<span id="error-userName" class="error"></span>
|
||||
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="password">
|
||||
<strong>Password</strong>
|
||||
@if(account.isDefined){
|
||||
(Input to change password)
|
||||
}
|
||||
</label>
|
||||
<span id="error-password" class="error"></span>
|
||||
<input type="password" name="password" id="password" value="" autocomplete="off"/>
|
||||
</fieldset>
|
||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||
<fieldset>
|
||||
<label for="password">
|
||||
<strong>Password</strong>
|
||||
@if(account.isDefined){
|
||||
(Input to change password)
|
||||
}
|
||||
</label>
|
||||
<span id="error-password" class="error"></span>
|
||||
<input type="password" name="password" id="password" value="" autocomplete="off"/>
|
||||
</fieldset>
|
||||
}
|
||||
<fieldset>
|
||||
<label for="mailAddress"><strong>Mail Address</strong></label>
|
||||
<span id="error-mailAddress" class="error"></span>
|
||||
|
||||
Reference in New Issue
Block a user