OBP-API/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala

1417 lines
59 KiB
Scala

/**
Open Bank Project - API
Copyright (C) 2011-2019, TESOBE GmbH.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Email: contact@tesobe.com
TESOBE GmbH.
Osloer Strasse 16/17
Berlin 13359, Germany
This product includes software developed at
TESOBE (http://www.tesobe.com/)
*/
package code.model.dataAccess
import code.UserRefreshes.UserRefreshes
import code.accountholders.AccountHolders
import code.api.util.APIUtil.{hasAnOAuthHeader, logger, validatePasswordOnCreation, _}
import code.api.util.ErrorMessages._
import code.api.util._
import code.api.v4_0_0.dynamic.DynamicEndpointHelper
import code.api.{APIFailure, DirectLogin, GatewayLogin, OAuthHandshake}
import code.bankconnectors.Connector
import code.context.UserAuthContextProvider
import code.entitlement.Entitlement
import code.loginattempts.LoginAttempt
import code.token.TokensOpenIDConnect
import code.users.Users
import code.util.Helper
import code.util.Helper.MdcLoggable
import code.views.Views
import com.openbankproject.commons.model.{User, _}
import net.liftweb.common._
import net.liftweb.http._
import net.liftweb.mapper._
import net.liftweb.util.Mailer.{BCC, From, Subject, To}
import net.liftweb.util._
import scala.collection.immutable.List
import scala.xml.{Elem, NodeSeq, Text}
import com.openbankproject.commons.ExecutionContext.Implicits.global
import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue
import org.apache.commons.lang3.StringUtils
import code.util.HydraUtil._
import com.github.dwickern.macros.NameOf.nameOf
import sh.ory.hydra.model.AcceptLoginRequest
import net.liftweb.http.S.fmapFunc
import scala.concurrent.Future
/**
* An O-R mapped "User" class that includes first name, last name, password
*
* 1 AuthUser : is used for authentication, only for webpage Login in stuff
* 1) It is MegaProtoUser, has lots of methods for validation username, password, email ....
* Such as lost password, reset password .....
* Lift have some helper methods to make these things easily.
*
*
*
* 2 ResourceUser: is only a normal LongKeyedMapper
* 1) All the accounts, transactions ,roles, views, accountHolders, customers... should be linked to ResourceUser.userId_ field.
* 2) The consumer keys, tokens are also belong ResourceUser
*
*
* 3 RelationShips:
* 1)When `Sign up` new user --> create AuthUser --> call AuthUser.save() --> create ResourceUser user.
* They share the same username and email.
* 2)AuthUser `user` field as the Foreign Key to link to Resource User.
* one AuthUser <---> one ResourceUser
*
*/
class AuthUser extends MegaProtoUser[AuthUser] with MdcLoggable {
def getSingleton = AuthUser // what's the "meta" server
object user extends MappedLongForeignKey(this, ResourceUser)
override lazy val firstName = new MyFirstName
protected class MyFirstName extends MappedString(this, 100) {
def isEmpty(msg: => String)(value: String): List[FieldError] =
value match {
case null => List(FieldError(this, Text(msg))) // issue 179
case e if e.trim.isEmpty => List(FieldError(this, Text(msg))) // issue 179
case _ => Nil
}
override def displayName = fieldOwner.firstNameDisplayName
override val fieldId = Some(Text("txtFirstName"))
override def validations = isEmpty(Helper.i18n("Please.enter.your.first.name")) _ :: super.validations
override def _toForm: Box[Elem] =
fmapFunc({s: List[String] => this.setFromAny(s)}){name =>
Full(appendFieldId(<input type={formInputType}
maxlength={maxLen.toString}
aria-labelledby={displayName}
aria-describedby={uniqueFieldId.getOrElse("")}
name={name}
value={get match {case null => "" case s => s.toString}}/>))
}
}
override lazy val lastName = new MyLastName
protected class MyLastName extends MappedString(this, 100) {
def isEmpty(msg: => String)(value: String): List[FieldError] =
value match {
case null => List(FieldError(this, Text(msg))) // issue 179
case e if e.trim.isEmpty => List(FieldError(this, Text(msg))) // issue 179
case _ => Nil
}
override def displayName = fieldOwner.lastNameDisplayName
override val fieldId = Some(Text("txtLastName"))
override def validations = isEmpty(Helper.i18n("Please.enter.your.last.name")) _ :: super.validations
override def _toForm: Box[Elem] =
fmapFunc({s: List[String] => this.setFromAny(s)}){name =>
Full(appendFieldId(<input type={formInputType}
maxlength={maxLen.toString}
aria-labelledby={displayName}
aria-describedby={uniqueFieldId.getOrElse("")}
name={name}
value={get match {case null => "" case s => s.toString}}/>))
}
}
/**
* Username is a valid email address or the regex below:
* Regex to validate a username
*
* ^(?=.{8,100}$)(?![_.])(?!.*[_.]{2})[a-zA-Z0-9._]+(?<![_.])$
* └─────┬────┘└───┬──┘└─────┬─────┘└─────┬─────┘ └───┬───┘
* │ │ │ │ no _ or . at the end
* │ │ │ │
* │ │ │ allowed characters
* │ │ │
* │ │ no __ or _. or ._ or .. inside
* │ │
* │ no _ or . at the beginning
*
* username is 8-100 characters long
*
*/
private val usernameRegex = """^(?=.{8,100}$)(?![_.])(?!.*[_.]{2})[a-zA-Z0-9._]+(?<![_.])$""".r
/**
* The username field for the User.
*/
lazy val username: userName = new userName()
class userName extends MappedString(this, 100) {
def isEmpty(msg: => String)(value: String): List[FieldError] =
value match {
case null => List(FieldError(this, Text(msg))) // issue 179
case e if e.trim.isEmpty => List(FieldError(this, Text(msg))) // issue 179
case _ => Nil
}
def usernameIsValid(msg: => String)(e: String) = e match {
case null => List(FieldError(this, Text(msg)))
case e if e.trim.isEmpty => List(FieldError(this, Text(msg)))
case e if emailRegex.findFirstMatchIn(e).isDefined => Nil // Email is valid username
case e if usernameRegex.findFirstMatchIn(e).isDefined => Nil
case _ => List(FieldError(this, Text(msg)))
}
override def displayName = S.?("Username")
override def dbIndexed_? = true
override def validations = isEmpty(Helper.i18n("Please.enter.your.username")) _ ::
usernameIsValid(Helper.i18n("invalid.username")) _ ::
valUnique(Helper.i18n("unique.username")) _ ::
valUniqueExternally(Helper.i18n("unique.username")) _ ::
super.validations
override val fieldId = Some(Text("txtUsername"))
override def _toForm: Box[Elem] =
fmapFunc({s: List[String] => this.setFromAny(s)}){name =>
Full(appendFieldId(<input type={formInputType}
maxlength={maxLen.toString}
aria-labelledby={displayName}
aria-describedby={uniqueFieldId.getOrElse("")}
name={name}
value={get match {case null => "" case s => s.toString}}/>))
}
/**
* Make sure that the field is unique in the CBS
*/
def valUniqueExternally(msg: => String)(uniqueUsername: String): List[FieldError] ={
if (APIUtil.getPropsAsBoolValue("connector.user.authentication", false)) {
Connector.connector.vend.checkExternalUserExists(uniqueUsername, None).map(_.sub) match {
case Full(returnedUsername) => // Get the username via connector
if(uniqueUsername == returnedUsername) { // Username is NOT unique
List(FieldError(this, Text(msg))) // provide the error message
} else {
Nil // All good. Allow username creation
}
case ParamFailure(message,_,_,APIFailure(errorMessage, errorCode)) if errorMessage.contains("NO DATA") => // Cannot get the username via connector
Nil // All good. Allow username creation
case _ => // Any other case we provide error message
List(FieldError(this, Text(msg)))
}
} else {
Nil // All good. Allow username creation
}
}
}
override lazy val password = new MyPasswordNew
lazy val signupPasswordRepeatText = getWebUiPropsValue("webui_signup_body_password_repeat_text", S.?("repeat"))
class MyPasswordNew extends MappedPassword(this) {
lazy val preFilledPassword = if (APIUtil.getPropsAsBoolValue("allow_pre_filled_password", true)) {get.toString} else ""
override def _toForm: Box[NodeSeq] = {
S.fmapFunc({s: List[String] => this.setFromAny(s)}){funcName =>
Full(
<span>
{appendFieldId(<input id="textPassword" aria-labelledby="Password" aria-describedby={uniqueFieldId.getOrElse("")} type={formInputType} name={funcName} value={preFilledPassword}/> ) }
<div id="signup-error" class="alert alert-danger hide">
<span data-lift={s"Msg?id=${uniqueFieldId.getOrElse("")}&errorClass=error"}/>
</div>
<div id ="repeat-password">{signupPasswordRepeatText}</div>
<input id="textPasswordRepeat" aria-labelledby="Password Repeat" aria-describedby={uniqueFieldId.getOrElse("")} type={formInputType} name={funcName} value={preFilledPassword}/>
<div id="signup-error" class="alert alert-danger hide">
<span data-lift={s"Msg?id=${uniqueFieldId.getOrElse("")}_repeat&errorClass=error"}/>
</div>
</span>)
}
}
override def displayName = fieldOwner.passwordDisplayName
private var passwordValue = ""
private var invalidPw = false
private var invalidMsg = ""
// TODO Remove double negative and abreviation.
// TODO “invalidPw” = false -> “strongPassword = true” etc.
override def setFromAny(f: Any): String = {
def checkPassword() = {
def isPasswordEmpty() = {
if (passwordValue.isEmpty())
true
else {
passwordValue match {
case "*" | null | MappedPassword.blankPw =>
true
case _ =>
false
}
}
}
isPasswordEmpty() match {
case true =>
invalidPw = true;
invalidMsg = Helper.i18n("please.enter.your.password")
S.error("authuser_password_repeat", Text(Helper.i18n("please.re-enter.your.password")))
case false =>
if (validatePasswordOnCreation(passwordValue))
invalidPw = false
else {
invalidPw = true
invalidMsg = S.?(ErrorMessages.InvalidStrongPasswordFormat.split(':')(1))
S.error("authuser_password_repeat", Text(invalidMsg))
}
}
}
f match {
case a: Array[String] if (a.length == 2 && a(0) == a(1)) => {
passwordValue = a(0).toString
checkPassword()
this.set(a(0))
}
case l: List[_] if (l.length == 2 && l.head.asInstanceOf[String] == l(1).asInstanceOf[String]) => {
passwordValue = l(0).asInstanceOf[String]
checkPassword()
this.set(l.head.asInstanceOf[String])
}
case _ => {
invalidPw = true;
invalidMsg = Helper.i18n("passwords.do.not.match")
S.error("authuser_password_repeat", Text(invalidMsg))
}
}
get
}
override def validate: List[FieldError] = {
if (!invalidPw && password.get != "*") super.validate
else if (invalidPw) List(FieldError(this, Text(invalidMsg))) ++ super.validate
else List(FieldError(this, Text(Helper.i18n("please.enter.your.password")))) ++ super.validate
}
}
/**
* The provider field for the User.
*/
lazy val provider: userProvider = new userProvider()
class userProvider extends MappedString(this, 100) {
override def displayName = S.?("provider")
override val fieldId = Some(Text("txtProvider"))
}
def getProvider() = {
if(provider.get == null) {
APIUtil.getPropsValue("hostname","")
} else if ( provider.get == "" || provider.get == APIUtil.getPropsValue("hostname","") ) {
APIUtil.getPropsValue("hostname","")
} else {
provider.get
}
}
def createUnsavedResourceUser() : ResourceUser = {
val user = Users.users.vend.createUnsavedResourceUser(getProvider(), Some(username.get), Some(username.get), Some(email.get), None).openOrThrowException(attemptedToOpenAnEmptyBox)
user
}
def getResourceUsersByEmail(userEmail: String) : List[ResourceUser] = {
Users.users.vend.getUserByEmail(userEmail) match {
case Full(userList) => userList
case _ => List()
}
}
def getResourceUserByUsername(username: String) : Box[User] = {
Users.users.vend.getUserByUserName(username)
}
override def save(): Boolean = {
if(! (user defined_?)){
logger.info("user reference is null. We will create a ResourceUser")
val resourceUser = createUnsavedResourceUser()
val savedUser = Users.users.vend.saveResourceUser(resourceUser)
user(savedUser) //is this saving resourceUser into a user field?
}
else {
logger.info("user reference is not null. Trying to update the ResourceUser")
Users.users.vend.getResourceUserByResourceUserId(user.get).map{ u =>{
logger.info("API User found ")
u.name_(username.get)
.email(email.get)
.providerId(username.get)
.save
}
}
}
super.save()
}
override def delete_!(): Boolean = {
user.obj.map(u => Users.users.vend.deleteResourceUser(u.id.get))
super.delete_!
}
// Regex to validate an email address as per W3C recommendations: https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
private val emailRegex = """^[a-zA-Z0-9\.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$""".r
def isEmailValid(e: String): Boolean = e match{
case null => false
case e if e.trim.isEmpty => false
case e if emailRegex.findFirstMatchIn(e).isDefined => true
case _ => false
}
// Override the validate method of MappedEmail class
// There's no way to override the default emailPattern from MappedEmail object
override lazy val email = new MyEmail(this, 48) {
override def validations = super.validations
override def dbIndexed_? = false
override def validate = i_is_! match {
case null => List(FieldError(this, Text(Helper.i18n("Please.enter.your.email"))))
case e if e.trim.isEmpty => List(FieldError(this, Text(Helper.i18n("Please.enter.your.email"))))
case e if (!isEmailValid(e)) => List(FieldError(this, Text(S.?("invalid.email.address"))))
case _ => Nil
}
override def _toForm: Box[Elem] =
fmapFunc({s: List[String] => this.setFromAny(s)}){name =>
Full(appendFieldId(<input type={formInputType}
maxlength={maxLen.toString}
aria-labelledby={displayName}
aria-describedby={uniqueFieldId.getOrElse("")}
name={name}
value={get match {case null => "" case s => s.toString}}/>))
}
}
}
/**
* The singleton that has methods for accessing the database
*/
object AuthUser extends AuthUser with MetaMegaProtoUser[AuthUser]{
import net.liftweb.util.Helpers._
/**Marking the locked state to show different error message */
val usernameLockedStateCode = Long.MaxValue
val connector = APIUtil.getPropsValue("connector").openOrThrowException("no connector set")
val starConnectorSupportedTypes = APIUtil.getPropsValue("starConnector_supported_types","")
override def emailFrom = APIUtil.getPropsValue("mail.users.userinfo.sender.address", "sender-not-set")
override def screenWrap = Full(<lift:surround with="default" at="content"><lift:bind /></lift:surround>)
// define the order fields will appear in forms and output
override def fieldOrder = List(id, firstName, lastName, email, username, password, provider)
override def signupFields = List(firstName, lastName, email, username, password)
// To force validation of email addresses set this to false (default as of 29 June 2021)
override def skipEmailValidation = APIUtil.getPropsAsBoolValue("authUser.skipEmailValidation", false)
override def loginXhtml = {
val loginXml = Templates(List("templates-hidden","_login")).map({
"form [action]" #> {S.uri} &
"#loginText * " #> {S.?("log.in")} &
"#usernameText * " #> {S.?("username")} &
"#passwordText * " #> {S.?("password")} &
"#login_challenge [value]" #> S.param("login_challenge").getOrElse("") &
"autocomplete=off [autocomplete] " #> APIUtil.getAutocompleteValue &
"#recoverPasswordLink * " #> {
"a [href]" #> {lostPasswordPath.mkString("/", "/", "")} &
"a *" #> {S.?("recover.password")}
} &
"#SignUpLink * " #> {
"a [href]" #> {AuthUser.signUpPath.foldLeft("")(_ + "/" + _)} &
"a *" #> {S.?("sign.up")}
}
})
<div>{loginXml getOrElse NodeSeq.Empty}</div>
}
/**
* Find current ResourceUser from the server.
* This method has no parameters, it depends on different login types:
* AuthUser: AuthUser.currentUser
* OAuthHandshake: OAuthHandshake.getUser
* DirectLogin: DirectLogin.getUser
* to get the current Resourceuser .
*
*/
def getCurrentUser: Box[User] = {
val authorization: Box[String] = S.request.map(_.header("Authorization")).flatten
val directLogin: Box[String] = S.request.map(_.header("DirectLogin")).flatten
for {
resourceUser <- if (AuthUser.currentUser.isDefined)
//AuthUser.currentUser.get.user.foreign // this will be issue when the resource user is in remote side
Users.users.vend.getUserByUserName(AuthUser.currentUser.openOrThrowException(ErrorMessages.attemptedToOpenAnEmptyBox).username.get)
else if (directLogin.isDefined) // Direct Login
DirectLogin.getUser
else if (hasDirectLoginHeader(authorization)) // Direct Login Deprecated
DirectLogin.getUser
else if (hasAnOAuthHeader(authorization)) {
OAuthHandshake.getUser
} else if (hasGatewayHeader(authorization)){
GatewayLogin.getUser
} else {
logger.debug(ErrorMessages.CurrentUserNotFoundException)
Failure(ErrorMessages.CurrentUserNotFoundException)
//This is a big problem, if there is no current user from here.
//throw new RuntimeException(ErrorMessages.CurrentUserNotFoundException)
}
} yield {
resourceUser
}
}
/**
* get current user.
* Note: 1. it will call getCurrentUser method,
*
*/
def getCurrentUserUsername: String = {
getCurrentUser match {
case Full(user) if user.provider.contains("google") && !user.emailAddress.isEmpty => user.emailAddress
case Full(user) if user.provider.contains("yahoo") && !user.emailAddress.isEmpty => user.emailAddress
case Full(user) if user.provider.contains("microsoft") && !user.emailAddress.isEmpty => user.emailAddress
case Full(user) => user.name
case _ => "" //TODO need more error handling for different user cases
}
}
def getIDTokenOfCurrentUser(): String = {
if(APIUtil.getPropsAsBoolValue("openid_connect.show_tokens", false)) {
AuthUser.currentUser match {
case Full(authUser) =>
TokensOpenIDConnect.tokens.vend.getOpenIDConnectTokenByAuthUser(authUser.id.get).map(_.idToken).getOrElse("")
case _ => ""
}
} else {
"This information is not allowed at this instance."
}
}
/**
* get current user.userId
* Note: 1.resourceuser has two ids: id(Long) and userid_(String),
*
* @return return userid_(String).
*/
def getCurrentResourceUserUserId: String = {
getCurrentUser match{
case Full(user) => user.userId
case _ => "" //TODO need more error handling for different user cases
}
}
/**
* The string that's generated when the user name is not found. By
* default: S.?("email.address.not.found")
* The function is overridden in order to prevent leak of information at password reset page if username / email exists or do not exist.
* I.e. we want to prevent case in which an anonymous user can get information from the message does some username/email exist or no in our system.
*/
override def userNameNotFoundString: String = "Thank you. If we found a matching user, password reset instructions have been sent."
/**
* Overridden to use the hostname set in the props file
*/
override def sendPasswordReset(name: String) {
findUserByUsernameLocally(name).toList ::: findUsersByEmailLocally(name) map {
// reason of case parameter name is "u" instead of "user": trait AuthUser have constant mumber name is "user"
// So if the follow case paramter name is "user" will cause compile warnings
case u if u.validated_? =>
u.resetUniqueId().save
//NOTE: here, if server_mode = portal, so we need modify the resetLink to portal_hostname, then developer can get proper response..
val resetPasswordLinkProps = APIUtil.getPropsValue("hostname", "ERROR")
val resetPasswordLink = APIUtil.getPropsValue("portal_hostname", resetPasswordLinkProps)+
passwordResetPath.mkString("/", "/", "/")+urlEncode(u.getUniqueId())
Mailer.sendMail(From(emailFrom),Subject(passwordResetEmailSubject + " - " + u.username),
To(u.getEmail) ::
generateResetEmailBodies(u, resetPasswordLink) :::
(bccEmail.toList.map(BCC(_))) :_*)
case u =>
sendValidationEmail(u)
}
// In order to prevent any leakage of information we use the same message for all cases
S.notice(userNameNotFoundString)
S.redirectTo(homePage)
}
override def lostPasswordXhtml = {
<div id="recover-password" tabindex="-1">
<h1>Recover Password</h1>
<div id="recover-password-explanation">Enter your email address or username and we'll email you a link to reset your password</div>
<form action={S.uri} method="post">
<div class="form-group">
<label>Username or email address</label> <span id="recover-password-email"><input id="email" type="text" /></span>
</div>
<div id="recover-password-submit">
<input type="submit" />
</div>
</form>
</div>
}
override def lostPassword = {
val bind =
"#email" #> SHtml.text("", sendPasswordReset _) &
"type=submit" #> lostPasswordSubmitButton(S.?("submit"))
bind(lostPasswordXhtml)
}
//override def def passwordResetMailBody(user: TheUserType, resetLink: String): Elem = { }
/**
* Overridden to use the hostname set in the props file
*/
override def sendValidationEmail(user: TheUserType) {
val resetLink = APIUtil.getPropsValue("hostname", "ERROR")+"/"+validateUserPath.mkString("/")+
"/"+urlEncode(user.getUniqueId())
val email: String = user.getEmail
val msgXml = signupMailBody(user, resetLink)
Mailer.sendMail(From(emailFrom),Subject(signupMailSubject),
To(user.getEmail) ::
generateValidationEmailBodies(user, resetLink) :::
(bccEmail.toList.map(BCC(_))) :_* )
}
def grantDefaultEntitlementsToAuthUser(user: TheUserType) = {
tryo{getResourceUserByUsername(user.username.get).head.userId} match {
case Full(userId)=>APIUtil.grantDefaultEntitlementsToNewUser(userId)
case _ => logger.error("Can not getResourceUserByUsername here, so it breaks the grantDefaultEntitlementsToNewUser process.")
}
}
override def validateUser(id: String): NodeSeq = findUserByUniqueId(id) match {
case Full(user) if !user.validated_? =>
user.setValidated(true).resetUniqueId().save
grantDefaultEntitlementsToAuthUser(user)
logUserIn(user, () => {
S.notice(S.?("account.validated"))
S.redirectTo(homePage)
})
case _ => S.error(S.?("invalid.validation.link")); S.redirectTo(homePage)
}
override def actionsAfterSignup(theUser: TheUserType, func: () => Nothing): Nothing = {
theUser.setValidated(skipEmailValidation).resetUniqueId()
theUser.save
if (!skipEmailValidation) {
sendValidationEmail(theUser)
S.notice(S.?("sign.up.message"))
func()
} else {
grantDefaultEntitlementsToAuthUser(theUser)
logUserIn(theUser, () => {
S.notice(S.?("welcome"))
func()
})
}
}
/**
* Set this to redirect to a certain page after a failed login
*/
object failedLoginRedirect extends SessionVar[Box[String]](Empty) {
override lazy val __nameSalt = Helpers.nextFuncName
}
def agreeTermsDiv = {
val agreeTermsHtml = getWebUiPropsValue("webui_agree_terms_html", "")
if(agreeTermsHtml.isEmpty){
val url = getWebUiPropsValue("webui_agree_terms_url", "")
if (url.isEmpty) {
s""
} else {
scala.xml.Unparsed(s"""<div id="signup-agree-terms" class="checkbox"><label><input type="checkbox" />I hereby agree to the <a href="$url" title="T &amp; C">Terms and Conditions</a></label></div>""")
}
} else{
scala.xml.Unparsed(s"""$agreeTermsHtml""")
}
}
def legalNoticeDiv = {
val agreeTermsHtml = getWebUiPropsValue("webui_legal_notice_html_text", "")
if(agreeTermsHtml.isEmpty){
s""
} else{
scala.xml.Unparsed(s"""$agreeTermsHtml""")
}
}
def agreePrivacyPolicy = {
val url = getWebUiPropsValue("webui_agree_privacy_policy_url", "")
val text = getWebUiPropsValue("webui_agree_privacy_policy_html_text", s"""<div id="signup-agree-privacy-policy"><label>By submitting this information you consent to processing your data by TESOBE GmbH according to our <a href="$url" title="Privacy Policy">Privacy Policy</a>. TESOBE shall use this information to send you emails and provide customer support.</label></div>""")
if (url.isEmpty) {
s""
} else {
scala.xml.Unparsed(s"""$text""")
}
}
def signupFormTitle = getWebUiPropsValue("webui_signup_form_title_text", S.?("sign.up"))
override def signupXhtml (user:AuthUser) = {
<div id="signup" tabindex="-1">
<form method="post" action={S.uriAndQueryString.getOrElse(S.uri)}>
<h1>{signupFormTitle}</h1>
{legalNoticeDiv}
<div id="signup-general-error" class="alert alert-danger hide"><span data-lift="Msg?id=error"/></div>
{localForm(user, false, signupFields)}
{agreeTermsDiv}
{agreePrivacyPolicy}
<div id="signup-submit">
<input type="submit" />
</div>
</form>
</div>
}
override def localForm(user: TheUserType, ignorePassword: Boolean, fields: List[FieldPointerType]): NodeSeq = {
for {
pointer <- fields
field <- computeFieldFromPointer(user, pointer).toList
if field.show_? && (!ignorePassword || !pointer.isPasswordField_?)
form <- field.toForm.toList
} yield {
if(field.uniqueFieldId.getOrElse("") == "authuser_password") {
<div class="form-group">
<label>{field.displayName}</label>
{form}
</div>
} else {
<div class="form-group">
<label>{field.displayName}</label>
{form}
<div id="signup-error" class="alert alert-danger hide"><span data-lift={s"Msg?id=${field.uniqueFieldId.getOrElse("")}&errorClass=error"}/></div>
</div>
}
}
}
def userLoginFailed = {
logger.info("failed: " + failedLoginRedirect.get)
// variable redir is from failedLoginRedirect, it is set-up in OAuthAuthorisation.scala as following code:
// val currentUrl = S.uriAndQueryString.getOrElse("/")
// AuthUser.failedLoginRedirect.set(Full(Helpers.appendParams(currentUrl, List((FailedLoginParam, "true")))))
val redir = failedLoginRedirect.get
//Check the internal redirect, in case for open redirect issue.
// variable redir is from loginRedirect, it is set-up in OAuthAuthorisation.scala as following code:
// val currentUrl = S.uriAndQueryString.getOrElse("/")
// AuthUser.loginRedirect.set(Full(Helpers.appendParams(currentUrl, List((LogUserOutParam, "false")))))
if (Helper.isValidInternalRedirectUrl(redir.toString)) {
S.redirectTo(redir.toString)
} else {
S.error(S.?(ErrorMessages.InvalidInternalRedirectUrl))
logger.info(ErrorMessages.InvalidInternalRedirectUrl + loginRedirect.get)
}
S.error("login", S.?("Invalid Username or Password"))
}
def getResourceUserId(username: String, password: String): Box[Long] = {
findUserByUsernameLocally(username) match {
// We have a user from the local provider.
case Full(user) if (user.getProvider() == APIUtil.getPropsValue("hostname","")) =>
if (
user.validated_? &&
// User is NOT locked AND the password is good
! LoginAttempt.userIsLocked(username) &&
user.testPassword(Full(password)))
{
// We logged in correctly, so reset badLoginAttempts counter (if it exists)
LoginAttempt.resetBadLoginAttempts(username)
Full(user.user.get) // Return the user.
}
// User is unlocked AND password is bad
else if (
user.validated_? &&
! LoginAttempt.userIsLocked(username) &&
! user.testPassword(Full(password))
) {
LoginAttempt.incrementBadLoginAttempts(username)
Empty
}
// User is locked
else if (LoginAttempt.userIsLocked(username))
{
LoginAttempt.incrementBadLoginAttempts(username)
logger.info(ErrorMessages.UsernameHasBeenLocked)
//TODO need to fix, use Failure instead, it is used to show the error message to the GUI
Full(usernameLockedStateCode)
}
else {
// Nothing worked, so just increment bad login attempts
LoginAttempt.incrementBadLoginAttempts(username)
Empty
}
// We have a user from an external provider.
case Full(user) if (user.getProvider() != APIUtil.getPropsValue("hostname","")) =>
APIUtil.getPropsAsBoolValue("connector.user.authentication", false) match {
case true if !LoginAttempt.userIsLocked(username) =>
val userId =
for {
authUser <- checkExternalUserViaConnector(username, password)
resourceUser <- tryo {
authUser.user
}
} yield {
LoginAttempt.resetBadLoginAttempts(username)
resourceUser.get
}
userId match {
case Full(l: Long) => Full(l)
case _ =>
LoginAttempt.incrementBadLoginAttempts(username)
Empty
}
case false =>
LoginAttempt.incrementBadLoginAttempts(username)
Empty
}
// Everything else.
case _ =>
LoginAttempt.incrementBadLoginAttempts(username)
Empty
}
}
/**
* This method is belong to AuthUser, it is used for authentication(Login stuff)
* 1 get the user over connector.
* 2 check whether it is existing in AuthUser table in obp side.
* 3 if not existing, will create new AuthUser.
* @return Return the authUser
*/
@deprecated("we have @checkExternalUserViaConnector method ","01-07-2020")
def getUserFromConnector(name: String, password: String):Box[AuthUser] = {
Connector.connector.vend.getUser(name, password) match {
case Full(InboundUser(extEmail, extPassword, extUsername)) => {
val extProvider = connector
val user = findUserByUsernameLocally(name) match {
// Check if the external user is already created locally
case Full(user) if user.validated_?
// && user.provider == extProvider
=> {
// Return existing user if found
logger.info("external user already exists locally, using that one")
user
}
// If not found, create a new user
case _ =>
// Create AuthUser using fetched data from connector
// assuming that user's email is always validated
logger.info("external user "+ extEmail +" does not exist locally, creating one")
val newUser = AuthUser.create
.firstName(extUsername)
.email(extEmail)
.username(extUsername)
// No need to store password, so store dummy string instead
.password(generateUUID())
.provider(extProvider)
.validated(true)
// Return created user
newUser.saveMe()
}
Full(user)
}
case _ => {
Empty
}
}
}
/**
* This method is belong to AuthUser, it is used for authentication(Login stuff)
* 1 get the user over connector.
* 2 check whether it is existing in AuthUser table in obp side.
* 3 if not existing, will create new AuthUser.
* @return Return the authUser
*/
def checkExternalUserViaConnector(username: String, password: String):Box[AuthUser] = {
Connector.connector.vend.checkExternalUserCredentials(username, password, None) match {
case Full(InboundExternalUser(aud, exp, iat, iss, sub, azp, email, emailVerified, name, userAuthContexts)) =>
val user = findUserByUsernameLocally(sub) match { // Check if the external user is already created locally
case Full(user) if user.validated_? => // Return existing user if found
logger.debug("external user already exists locally, using that one")
userAuthContexts match {
case Some(authContexts) => // Write user auth context to the database
UserAuthContextProvider.userAuthContextProvider.vend.createOrUpdateUserAuthContexts(user.userIdAsString, authContexts)
case None => // Do nothing
}
user
case _ => // If not found, create a new user
// Create AuthUser using fetched data from connector
// assuming that user's email is always validated
logger.debug("external user "+ sub + " does not exist locally, creating one")
AuthUser.create
.firstName(name.getOrElse(sub))
.email(email.getOrElse(""))
.username(sub)
// No need to store password, so store dummy string instead
.password(generateUUID())
// TODO add field stating external password check only.
.provider(iss)
.validated(emailVerified.exists(_.equalsIgnoreCase("true")))
.saveMe() //NOTE, we will create the resourceUser in the `saveMe()` method.
}
userAuthContexts match {
case Some(authContexts) => { // Write user auth context to the database
// get resourceUserId from AuthUser.
val resourceUserId = user.user.foreign.map(_.userId).getOrElse("")
// we try to catch this exception, the createOrUpdateUserAuthContexts can not break the login process.
tryo {UserAuthContextProvider.userAuthContextProvider.vend.createOrUpdateUserAuthContexts(resourceUserId, authContexts)}
.openOr(logger.error(s"${resourceUserId} checkExternalUserViaConnector.createOrUpdateUserAuthContexts throw exception! "))
}
case None => // Do nothing
}
Full(user)
case _ =>
Empty
}
}
def restoreSomeSessions(): Unit = {
activeBrand()
}
override protected def capturePreLoginState(): () => Unit = () => {restoreSomeSessions}
//overridden to allow a redirection if login fails
/**
* Success cases:
* case1: user validated && user not locked && user.provider from localhost && password correct --> Login in
* case2: user validated && user not locked && user.provider not localhost && password correct --> Login in
* case3: user from remote && checked over connector --> Login in
*
* Error cases:
* case1: user is locked --> UsernameHasBeenLocked
* case2: user.validated_? --> account.validation.error
* case3: right username but wrong password --> Invalid Login Credentials
* case4: wrong username --> Invalid Login Credentials
* case5: UnKnow error --> UnexpectedErrorDuringLogin
*/
override def login: NodeSeq = {
def redirectUri(): String = {
loginRedirect.get match {
case Full(url) =>
loginRedirect(Empty)
url
case _ =>
homePage
}
}
//Check the internal redirect, in case for open redirect issue.
// variable redirect is from loginRedirect, it is set-up in OAuthAuthorisation.scala as following code:
// val currentUrl = S.uriAndQueryString.getOrElse("/")
// AuthUser.loginRedirect.set(Full(Helpers.appendParams(currentUrl, List((LogUserOutParam, "false")))))
def checkInternalRedirectAndLogUseIn(preLoginState: () => Unit, redirect: String, user: AuthUser) = {
if (Helper.isValidInternalRedirectUrl(redirect)) {
logUserIn(user, () => {
S.notice(S.?("logged.in"))
preLoginState()
if(emailDomainToSpaceMappings.nonEmpty){
Future{
tryo{AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(user)}
.openOr(logger.error(s"${user} checkInternalRedirectAndLogUseIn.grantEntitlementsToUseDynamicEndpointsInSpaces throw exception! "))
}}
if(emailDomainToEntitlementMappings.nonEmpty){
Future{
tryo{AuthUser.grantEmailDomainEntitlementsToUser(user)}
.openOr(logger.error(s"${user} checkInternalRedirectAndLogUseIn.grantEmailDomainEntitlementsToUser throw exception! "))
}}
S.redirectTo(redirect)
})
} else {
S.error(S.?(ErrorMessages.InvalidInternalRedirectUrl))
logger.info(ErrorMessages.InvalidInternalRedirectUrl + loginRedirect.get)
}
}
def isObpProvider(user: AuthUser) = {
user.getProvider() == APIUtil.getPropsValue("hostname", "")
}
def obpUserIsValidatedAndNotLocked(usernameFromGui: String, user: AuthUser) = {
user.validated_? && !LoginAttempt.userIsLocked(usernameFromGui) &&
isObpProvider(user)
}
def externalUserIsValidatedAndNotLocked(usernameFromGui: String, user: AuthUser) = {
user.validated_? && !LoginAttempt.userIsLocked(usernameFromGui) &&
!isObpProvider(user)
}
def loginAction = {
if (S.post_?) {
val usernameFromGui = S.param("username").getOrElse("")
val passwordFromGui = S.param("password").getOrElse("")
val usernameEmptyField = S.param("username").map(_.isEmpty()).getOrElse(true)
val passwordEmptyField = S.param("password").map(_.isEmpty()).getOrElse(true)
val emptyField = usernameEmptyField || passwordEmptyField
emptyField match {
case true =>
if(usernameEmptyField)
S.error("login-form-username-error", Helper.i18n("please.enter.your.username"))
if(passwordEmptyField)
S.error("login-form-password-error", Helper.i18n("please.enter.your.password"))
case false =>
findUserByUsernameLocally(usernameFromGui) match {
case Full(user) if !user.validated_? =>
S.error(S.?("account.validation.error"))
// Check if user comes from localhost and
case Full(user) if obpUserIsValidatedAndNotLocked(usernameFromGui, user) =>
if(user.testPassword(Full(passwordFromGui))) { // if User is NOT locked and password is good
// Reset any bad attempt
LoginAttempt.resetBadLoginAttempts(usernameFromGui)
val preLoginState = capturePreLoginState()
logger.info("login redirect: " + loginRedirect.get)
val redirect = redirectUri()
checkInternalRedirectAndLogUseIn(preLoginState, redirect, user)
} else { // If user is NOT locked AND password is wrong => increment bad login attempt counter.
LoginAttempt.incrementBadLoginAttempts(usernameFromGui)
S.error(Helper.i18n("invalid.login.credentials"))
}
// If user is locked, send the error to GUI
case Full(user) if LoginAttempt.userIsLocked(usernameFromGui) =>
LoginAttempt.incrementBadLoginAttempts(usernameFromGui)
S.error(S.?(ErrorMessages.UsernameHasBeenLocked))
// Check if user came from kafka/obpjvm/stored_procedure and
// if User is NOT locked. Then check username and password
// from connector in case they changed on the south-side
case Full(user) if externalUserIsValidatedAndNotLocked(usernameFromGui, user) && testExternalPassword(usernameFromGui, passwordFromGui) =>
// Reset any bad attempts
LoginAttempt.resetBadLoginAttempts(usernameFromGui)
val preLoginState = capturePreLoginState()
logger.info("login redirect: " + loginRedirect.get)
val redirect = redirectUri()
//This method is used for connector = kafka* || obpjvm*
//It will update the views and createAccountHolder ....
registeredUserHelper(user.username.get)
checkInternalRedirectAndLogUseIn(preLoginState, redirect, user)
// If user cannot be found locally, try to authenticate user via connector
case Empty if (APIUtil.getPropsAsBoolValue("connector.user.authentication", false) ||
APIUtil.getPropsAsBoolValue("kafka.user.authentication", false) ||
APIUtil.getPropsAsBoolValue("obpjvm.user.authentication", false)) =>
val preLoginState = capturePreLoginState()
logger.info("login redirect: " + loginRedirect.get)
val redirect = redirectUri()
externalUserHelper(usernameFromGui, passwordFromGui) match {
case Full(user: AuthUser) =>
LoginAttempt.resetBadLoginAttempts(usernameFromGui)
checkInternalRedirectAndLogUseIn(preLoginState, redirect, user)
case _ =>
LoginAttempt.incrementBadLoginAttempts(username.get)
Empty
S.error(Helper.i18n("invalid.login.credentials"))
}
//If there is NO the username, throw the error message.
case Empty =>
S.error(Helper.i18n("invalid.login.credentials"))
case _ =>
LoginAttempt.incrementBadLoginAttempts(usernameFromGui)
S.error(S.?(ErrorMessages.UnexpectedErrorDuringLogin)) // Note we hit this if user has not clicked email validation link
}
}
}
}
// In this function we bind submit button to loginAction function.
// In case that unique token of submit button cannot be paired submit action will be omitted.
// Implemented in order to prevent a CSRF attack
def insertSubmitButton = {
scala.xml.XML.loadString(loginSubmitButton(loginButtonText, loginAction _).toString().replace("type=\"submit\"","class=\"submit\" type=\"submit\""))
}
val bind =
"submit" #> insertSubmitButton
bind(loginXhtml)
}
override def logout = {
logoutCurrentUser
S.request match {
case Full(a) => a.param("redirect") match {
case Full(customRedirect) => S.redirectTo(customRedirect)
case _ => S.redirectTo(homePage)
}
case _ => S.redirectTo(homePage)
}
}
/**
* The user authentications is not exciting in obp side, it need get the user via connector
*/
def testExternalPassword(usernameFromGui: String, passwordFromGui: String): Boolean = {
// TODO Remove kafka and obpjvm special cases
if (connector.startsWith("kafka") || connector == "obpjvm") {
getUserFromConnector(usernameFromGui, passwordFromGui) match {
case Full(user:AuthUser) => true
case _ => false
}
} else {
checkExternalUserViaConnector(usernameFromGui, passwordFromGui) match {
case Full(user:AuthUser) => true
case _ => false
}
}
}
/**
* This method will update the views and createAccountHolder ....
*/
def externalUserHelper(name: String, password: String): Box[AuthUser] = {
// TODO Remove kafka and obpjvm special cases
if (connector.startsWith("kafka") || connector == "obpjvm") {
for {
user <- getUserFromConnector(name, password)
u <- Users.users.vend.getUserByUserName(name)
} yield {
user
}
} else {
for {
user <- checkExternalUserViaConnector(name, password)
u <- Users.users.vend.getUserByUserName(name)
} yield {
user
}
}
}
/**
* This method will update the views and createAccountHolder ....
*/
def registeredUserHelper(username: String) = {
if (connector.startsWith("kafka") || connector == "obpjvm") {
for {
u <- Users.users.vend.getUserByUserName(username)
v <- Full (updateUserAccountViews(u, None))
} yield v
}
}
/**
* A Space is an alias for the OBP Bank. Each Bank / Space can contain many Dynamic Endpoints. If a User belongs to a Space,
* the User can use those endpoints but not modify them. If a User creates a Bank (aka Space) the user can create
* and modify Dynamic Endpoints and other objects in that Bank / Space.
*
* @return
*/
def mySpaces(user: AuthUser): List[BankId] = {
//1st: first check the user is validated
if (user.validated_?) {
//userEmail = robert.uk.29@example.com
// 2st get the email domain - `example.com`
val emailDomain = StringUtils.substringAfterLast(user.email.get, "@")
//3 return the bankIds
emailDomainToSpaceMappings.collectFirst {
case EmailDomainToSpaceMapping(`emailDomain`, ids) => ids.map(BankId(_));
} getOrElse Nil
} else {
Nil
}
}
def grantEntitlementsToUseDynamicEndpointsInSpaces(user: AuthUser) = {
if(emailDomainToSpaceMappings.nonEmpty) {
val createdByProcess = "grantEntitlementsToUseDynamicEndpointsInSpaces"
val userId = user.user.obj.map(_.userId).getOrElse("")
// user's already auto granted entitlements.
val entitlementsGrantedByThisProcess = Entitlement.entitlement.vend.getEntitlementsByUserId(userId)
.map(_.filter(role => role.createdByProcess == createdByProcess))
.getOrElse(Nil)
def alreadyHasEntitlement(role:ApiRole, bankId: String): Boolean =
entitlementsGrantedByThisProcess.exists(entitlement => entitlement.roleName == role.toString() && entitlement.bankId == bankId)
//call mySpaces --> get BankIds --> listOfRolesToUseAllDynamicEndpointsAOneBank (at each bank)--> Grant roles (for each role)
val allCurrentDynamicRoleToBankIdPairs: List[(ApiRole, String)] = for {
BankId(bankId) <- mySpaces(user: AuthUser)
role <- DynamicEndpointHelper.listOfRolesToUseAllDynamicEndpointsAOneBank(Some(bankId))
} yield {
if (!alreadyHasEntitlement(role, bankId)) {
Entitlement.entitlement.vend.addEntitlement(bankId, userId, role.toString, createdByProcess)
}
role -> bankId
}
// if user's auto granted entitlement invalid, delete it.
// invalid happens when some dynamic endpoints are removed, so the entitlements linked to the deleted dynamic endpoints are invalid.
for {
grantedEntitlement <- entitlementsGrantedByThisProcess
grantedEntitlementRoleName = grantedEntitlement.roleName
grantedEntitlementBankId = grantedEntitlement.bankId
} {
val isInValidEntitlement = !allCurrentDynamicRoleToBankIdPairs.exists { roleToBankIdPair =>
val(role, roleBankId) = roleToBankIdPair
role.toString() == grantedEntitlementRoleName && roleBankId == grantedEntitlementBankId
}
if(isInValidEntitlement) {
Entitlement.entitlement.vend.deleteEntitlement(Full(grantedEntitlement))
}
}
}
}
def grantEmailDomainEntitlementsToUser(user: AuthUser) = {
if(emailDomainToEntitlementMappings.nonEmpty){
val createdByProcess = "grantEmailDomainEntitlementsToUser"
val userId = user.user.obj.map(_.userId).getOrElse("")
// user's already auto granted entitlements.
val entitlementsGrantedByThisProcess = Entitlement.entitlement.vend.getEntitlementsByUserId(userId)
.map(_.filter(role => role.createdByProcess == createdByProcess))
.getOrElse(Nil)
def alreadyHasEntitlement(bankId: String, roleName:String): Boolean =
entitlementsGrantedByThisProcess.exists(entitlement => entitlement.roleName == roleName && entitlement.bankId == bankId)
val allEntitlementsFromCurrentProps: List[(String, String)] = for{
emailDomainToEntitlementMapping <- emailDomainToEntitlementMappings
domain = emailDomainToEntitlementMapping.domain
entitlement <- emailDomainToEntitlementMapping.entitlements if StringUtils.substringAfterLast(user.email.get, "@") == domain
roleName = entitlement.role_name
roleBankId = entitlement.bank_id
} yield {
if (!alreadyHasEntitlement(roleBankId, roleName)) {
Entitlement.entitlement.vend.addEntitlement(roleBankId, userId, roleName, createdByProcess)
}
roleName -> roleBankId
}
// if user's auto granted entitlement invalid, delete it.
// invalid happens when some dynamic endpoints are removed, so the entitlements linked to the deleted dynamic endpoints are invalid.
for {
grantedEntitlement <- entitlementsGrantedByThisProcess
grantedEntitlementRoleName = grantedEntitlement.roleName
grantedEntitlementBankId = grantedEntitlement.bankId
} {
val isInValidEntitlement = !allEntitlementsFromCurrentProps.exists { roleNameToBankIdPair =>
val(roleName, roleBankId) = roleNameToBankIdPair
roleName == grantedEntitlementRoleName && roleBankId == grantedEntitlementBankId
}
if(isInValidEntitlement) {
Entitlement.entitlement.vend.deleteEntitlement(Full(grantedEntitlement))
}
}
}
}
/**
* This is a helper method
* update the views, accountHolders for OBP side when sign up new remote user
*
*/
def updateUserAccountViews(user: User, callContext: Option[CallContext]): Unit = {
//get all accounts from Kafka
val accounts = Connector.connector.vend.getBankAccountsForUserLegacy(user.name,callContext).openOrThrowException(attemptedToOpenAnEmptyBox)
logger.debug(s"-->AuthUser.updateUserAccountViews.accounts : ${accounts} ")
updateUserAccountViews(user, accounts._1)
}
def updateUserAccountViewsFuture(user: User, callContext: Option[CallContext]) = {
for{
(accounts, _) <- Connector.connector.vend.getBankAccountsForUser(user.name,callContext) map {
connectorEmptyResponse(_, callContext)
}
}yield {
updateUserAccountViews(user, accounts)
UserRefreshes.UserRefreshes.vend.createOrUpdateRefreshUser(user.userId)
}
}
/**
* This is a helper method
* update the views, accountHolders for OBP side when sign up new remote user
* This method can only be used by the original user(account holder).
*/
def updateUserAccountViews(user: User, accounts: List[InboundAccount]): Unit = {
for {
account <- accounts
viewId <- account.viewsToGenerate if(user.isOriginalUser) // for now, we support four views here: Owner, Accountant, Auditor, _Public, first three are system views, the last is custom view.
bankAccountUID <- Full(BankIdAccountId(BankId(account.bankId), AccountId(account.accountId)))
view <- Views.views.vend.getOrCreateAccountView(bankAccountUID, viewId)//this method will return both system views and custom views back.
} yield {
if (view.isSystem)//if the view is a system view, we will call `grantAccessToSystemView`
Views.views.vend.grantAccessToSystemView(BankId(account.bankId), AccountId(account.accountId), view, user)
else //otherwise, we will call `grantAccessToCustomView`
Views.views.vend.grantAccessToCustomView(view.uid, user)
AccountHolders.accountHolders.vend.getOrCreateAccountHolder(user,bankAccountUID)
}
}
/**
* Find the authUser by author user name(authUser and resourceUser are the same).
* Only search for the local database.
*/
def findUserByUsernameLocally(name: String): Box[TheUserType] = {
find(By(this.username, name))
}
def passwordResetUrl(name: String, email: String, userId: String): String = {
find(By(this.username, name)) match {
case Full(authUser) if authUser.validated_? && authUser.email == email =>
Users.users.vend.getUserByUserId(userId) match {
case Full(u) if u.name == name && u.emailAddress == email =>
authUser.resetUniqueId().save
val resetLink = APIUtil.getPropsValue("hostname", "ERROR")+
passwordResetPath.mkString("/", "/", "/")+urlEncode(authUser.getUniqueId())
logger.warn(s"Password reset url is created for this user: $email")
// TODO Notify via email appropriate persons
resetLink
case _ => ""
}
case _ => ""
}
}
override def passwordResetXhtml = {
<div id="recover-password" tabindex="-1">
<h1>{if(S.queryString.isDefined) Helper.i18n("set.your.password") else S.?("reset.your.password")}</h1>
<form action={S.uri} method="post">
<div class="form-group">
<label for="password">{S.?("enter.your.new.password")}</label> <span><input id="password" class="form-control" type="password" /></span>
</div>
<div class="form-group">
<label for="repeatpassword">{S.?("repeat.your.new.password")}</label> <span><input id="repeatpassword" class="form-control" type="password" /></span>
</div>
<div class="form-group">
<input type="submit" class="btn btn-danger" />
</div>
</form>
</div>
}
/**
* Find the authUsers by author email(authUser and resourceUser are the same).
* Only search for the local database.
*/
protected def findUsersByEmailLocally(email: String): List[TheUserType] = {
val usernames: List[String] = this.getResourceUsersByEmail(email).map(_.user.name)
findAll(ByList(this.username, usernames))
}
lazy val signupSubmitButtonValue = getWebUiPropsValue("webui_signup_form_submit_button_value", S.?("sign.up"))
//overridden to allow redirect to loginRedirect after signup. This is mostly to allow
// loginFirst menu items to work if the user doesn't have an account. Without this,
// if a user tries to access a logged-in only page, and then signs up, they don't get redirected
// back to the proper page.
override def signup = {
val theUser: TheUserType = mutateUserOnSignup(createNewUserInstance())
val theName = signUpPath.mkString("")
//Check the internal redirect, in case for open redirect issue.
// variable redir is from loginRedirect, it is set-up in OAuthAuthorisation.scala as following code:
// val currentUrl = S.uriAndQueryString.getOrElse("/")
// AuthUser.loginRedirect.set(Full(Helpers.appendParams(currentUrl, List((LogUserOutParam, "false")))))
val loginRedirectSave = loginRedirect.is
def testSignup() {
validateSignup(theUser) match {
case Nil =>
//here we check loginRedirectSave (different from implementation in super class)
val redir = loginRedirectSave match {
case Full(url) =>
loginRedirect(Empty)
url
case _ =>
//if the register page url (user_mgt/sign_up?after-signup=link-to-customer) contains the parameter
//after-signup=link-to-customer,then it will redirect to the on boarding customer page.
S.params("after-signup") match {
case url if (url.nonEmpty && url.head.equals("link-to-customer")) =>
"/add-user-auth-context-update-request"
case _ =>
homePage
}
}
if (Helper.isValidInternalRedirectUrl(redir.toString)) {
actionsAfterSignup(theUser, () => {
S.redirectTo(redir)
})
} else {
S.error(S.?(ErrorMessages.InvalidInternalRedirectUrl))
logger.info(ErrorMessages.InvalidInternalRedirectUrl + loginRedirect.get)
}
case xs =>
xs.foreach{
e => S.error(e.field.uniqueFieldId.openOrThrowException("There is no uniqueFieldId."), e.msg)
}
signupFunc(Full(innerSignup _))
}
}
def innerSignup = {
val bind = "type=submit" #> signupSubmitButton(signupSubmitButtonValue, testSignup _)
bind(signupXhtml(theUser))
}
if(APIUtil.getPropsAsBoolValue("user_invitation.mandatory", false))
S.redirectTo("/user-invitation-info")
else
innerSignup
}
def scrambleAuthUser(userPrimaryKey: UserPrimaryKey): Box[Boolean] = tryo {
AuthUser.find(By(AuthUser.user, userPrimaryKey.value)) match {
case Full(user) =>
val scrambledUser = user.firstName(Helpers.randomString(16))
.email(Helpers.randomString(10) + "@example.com")
.username("DELETED-" + Helpers.randomString(16))
.firstName(Helpers.randomString(16))
.lastName(Helpers.randomString(16))
.password(Helpers.randomString(40))
.validated(false)
scrambledUser.save()
case Empty => true // There is a resource user but no the correlated Auth user
case _ => false // Error case
}
}
}