org.nlpcraft.server.user.NCUserManager.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nlpcraft Show documentation
Show all versions of nlpcraft Show documentation
An API to convert natural language into actions.
/*
* "Commons Clause" License, https://commonsclause.com/
*
* The Software is provided to you by the Licensor under the License,
* as defined below, subject to the following condition.
*
* Without limiting other conditions in the License, the grant of rights
* under the License will not include, and the License does not grant to
* you, the right to Sell the Software.
*
* For purposes of the foregoing, "Sell" means practicing any or all of
* the rights granted to you under the License to provide to third parties,
* for a fee or other consideration (including without limitation fees for
* hosting or consulting/support services related to the Software), a
* product or service whose value derives, entirely or substantially, from
* the functionality of the Software. Any license notice or attribution
* required by the License must also include this Commons Clause License
* Condition notice.
*
* Software: NLPCraft
* License: Apache 2.0, https://www.apache.org/licenses/LICENSE-2.0
* Licensor: Copyright (C) 2018 DataLingvo, Inc. https://www.datalingvo.com
*
* _ ____ ______ ______
* / | / / /___ / ____/________ _/ __/ /_
* / |/ / / __ \/ / / ___/ __ `/ /_/ __/
* / /| / / /_/ / /___/ / / /_/ / __/ /_
* /_/ |_/_/ .___/\____/_/ \__,_/_/ \__/
* /_/
*/
package org.nlpcraft.server.user
import java.util.{Timer, TimerTask}
import org.apache.commons.validator.routines.EmailValidator
import org.apache.ignite.{IgniteAtomicSequence, IgniteCache, IgniteSemaphore}
import org.nlpcraft.common.blowfish.NCBlowfishHasher
import org.nlpcraft.common.{NCLifecycle, _}
import org.nlpcraft.server.NCConfigurable
import org.nlpcraft.server.endpoints.{NCEndpointCacheKey, NCEndpointManager}
import org.nlpcraft.server.ignite.NCIgniteHelpers._
import org.nlpcraft.server.ignite.NCIgniteInstance
import org.nlpcraft.server.mdo.{NCUserMdo, NCUserPropertyMdo}
import org.nlpcraft.server.notification.NCNotificationManager
import org.nlpcraft.server.sql.{NCSql, NCSqlManager}
import org.nlpcraft.server.tx.NCTxManager
import scala.collection.JavaConverters._
import scala.util.control.Exception._
/**
* User CRUD manager.
*/
object NCUserManager extends NCLifecycle("User manager") with NCIgniteInstance {
// Static email validator.
private final val EMAIL_VALIDATOR = EmailValidator.getInstance()
// Caches.
@volatile private var tokenSigninCache: IgniteCache[String, SigninSession] = _
@volatile private var idSigninCache: IgniteCache[Long, Set[String]] = _
@volatile private var usersSeq: IgniteAtomicSequence = _
@volatile private var pwdSeq: IgniteAtomicSequence = _
@volatile private var userLock: IgniteSemaphore = _
// Access token timeout scanner.
@volatile private var scanner: Timer = _
// Session holder.
private case class SigninSession(
acsToken: String,
userId: Long,
signinMs: Long,
lastAccessMs: Long,
endpoints: Set[String]
)
private object Config extends NCConfigurable {
final val prefix = "server.user"
val pwdPoolBlowup: Int = getInt(s"$prefix.pwdPoolBlowup")
val timeoutScannerFreqMins: Int = getInt(s"$prefix.timeoutScannerFreqMins")
val accessTokenExpireTimeoutMins: Int = getInt(s"$prefix.accessTokenExpireTimeoutMins")
lazy val scannerMs: Int = timeoutScannerFreqMins * 60 * 1000
lazy val expireMs: Int = accessTokenExpireTimeoutMins * 60 * 1000
override def check(): Unit = {
if (pwdPoolBlowup <= 1)
abortError(s"Configuration parameter '$prefix.pwdPoolBlowup' must be > 1")
if (timeoutScannerFreqMins <= 0)
abortError(s"Configuration parameter '$prefix.timeoutScannerFreqMins' must be > 0")
if (accessTokenExpireTimeoutMins <= 0)
abortError(s"Configuration parameter '$prefix.accessTokenExpireTimeoutMins' must be > 0")
}
}
Config.check()
/**
* Starts this manager.
*/
override def start(): NCLifecycle = {
ensureStopped()
catching(wrapIE) {
usersSeq = NCSql.mkSeq(ignite, "usersSeq", "nc_user", "id")
pwdSeq = NCSql.mkSeq(ignite, "pwdSeq", "passwd_pool", "id")
tokenSigninCache = ignite.cache[String, SigninSession]("user-token-signin-cache")
idSigninCache = ignite.cache[Long, Set[String]]("user-id-signin-cache")
require(tokenSigninCache != null)
require(idSigninCache != null)
}
scanner = new Timer("timeout-scanner")
scanner.scheduleAtFixedRate(
new TimerTask() {
def run() {
try {
val now = U.nowUtcMs()
// Check access tokens for expiration.
catching(wrapIE) {
NCTxManager.startTx {
for (ses ← tokenSigninCache.asScala.map(_.getValue)
if now - ses.lastAccessMs >= Config.expireMs
) {
tokenSigninCache -= ses.acsToken
clearSigninCache(ses)
if (ses.endpoints.nonEmpty)
NCEndpointManager.cancelNotifications(
ses.userId,
(k: NCEndpointCacheKey) ⇒ ses.endpoints.contains(k.getEndpoint)
)
// Notification.
NCNotificationManager.addEvent("NC_ACCESS_TOKEN_TIMEDOUT",
"acsTok" → ses.acsToken,
"userId" → ses.userId,
"signinMs" → ses.signinMs,
"lastAccessMs" → ses.lastAccessMs
)
logger.trace(s"Access token timed out: ${ses.acsToken}")
}
}
}
}
catch {
case e: Throwable ⇒ logger.error("Error during timeout scanner process.", e)
}
}
},
Config.scannerMs,
Config.scannerMs
)
userLock = ignite.semaphore("userSemaphore", 1, true, true)
logger.info(s"Access tokens will be scanned for timeout every ${Config.timeoutScannerFreqMins} min.")
logger.info(s"Access tokens inactive for ${Config.accessTokenExpireTimeoutMins} min will be invalidated.")
NCSql.sql {
if (!NCSql.exists("nc_user"))
try {
val email = "[email protected]"
val pwd = "admin"
addUser0(
email,
pwd,
"Hermann",
"Minkowski",
avatarUrl = None,
isAdmin = true,
None
)
logger.info(s"Default admin user ($email/$pwd) created.")
}
catch {
case e: NCE ⇒ logger.error(s"Failed to add default admin user: ${e.getLocalizedMessage}", e)
}
}
super.start()
}
/**
* Gets the list of all current users.
*/
@throws[NCE]
def getAllUsers: Map[NCUserMdo, Option[Seq[NCUserPropertyMdo]]] = {
ensureStarted()
NCSql.sql {
NCSqlManager.getAllUsers
}
}
/**
* Gets flag which indicates there are another admin users in the system or not.
*
* @param usrId User ID.
*/
@throws[NCE]
def isOtherAdminsExist(usrId: Long): Boolean = {
ensureStarted()
NCSql.sql {
NCSqlManager.isOtherAdminsExist(usrId)
}
}
/**
* Stops this manager.
*/
override def stop(): Unit = {
if (scanner != null)
scanner.cancel()
scanner = null
tokenSigninCache = null
idSigninCache = null
super.stop()
}
/**
*
* @param acsTok Access token to sign out.
*/
@throws[NCE]
def signout(acsTok: String): Unit = {
ensureStarted()
catching(wrapIE) {
NCTxManager.startTx {
tokenSigninCache -== acsTok match {
case Some(ses) ⇒
clearSigninCache(ses)
if (ses.endpoints.nonEmpty)
NCEndpointManager.cancelNotifications(
ses.userId,
(k: NCEndpointCacheKey) ⇒ ses.endpoints.contains(k.getEndpoint)
)
// Notification.
NCNotificationManager.addEvent("NC_USER_SIGNED_OUT",
"acsTok" → ses.acsToken,
"userId" → ses.userId,
"signinMs" → ses.signinMs,
"lastAccessMs" → ses.lastAccessMs
)
logger.info(s"User signed out: ${ses.userId}")
case None ⇒ // No-op.
}
}
}
}
/**
* Gets user ID associated with active access token, if any.
*
* @param acsTkn Access token.
* @return
*/
@throws[NCE]
def getUserForAccessToken(acsTkn: String): Option[NCUserMdo] = {
ensureStarted()
getUserIdForAccessToken(acsTkn).flatMap(getUser)
}
/**
* Gets user ID associated with active access token, if any.
*
* @param acsTkn Access token.
* @return
*/
@throws[NCE]
def getUserIdForAccessToken(acsTkn: String): Option[Long] = {
ensureStarted()
catching(wrapIE) {
tokenSigninCache(acsTkn) match {
case Some(ses) ⇒
val now = U.nowUtcMs()
// Update login session.
tokenSigninCache += acsTkn → SigninSession(acsTkn, ses.userId, ses.signinMs, now, ses.endpoints)
Some(ses.userId) // Bingo!
case None ⇒ None
}
}
}
/**
* Gets user for given user ID.
*
* @param usrId User ID.
*/
@throws[NCE]
def getUser(usrId: Long): Option[NCUserMdo] = {
ensureStarted()
NCSql.sql {
NCSqlManager.getUser(usrId)
}
}
/**
* Gets user properties for given user ID.
*
* @param usrId User ID.
*/
@throws[NCE]
def getUserProperties(usrId: Long): Seq[NCUserPropertyMdo] = {
ensureStarted()
NCSql.sql {
NCSqlManager.getUserProperties(usrId)
}
}
/**
* Gets user for given user ID.
*
* @param usrId User ID.
*/
@throws[NCE]
def getUserEndpoints(usrId: Long): Set[String] = {
ensureStarted()
catching(wrapIE) {
idSigninCache(usrId) match {
case Some(toks) ⇒ toks.flatMap(tok ⇒ tokenSigninCache(tok)).flatMap(_.endpoints)
case None ⇒ Set.empty
}
}
}
/**
*
* @param email User email (as username).
* @param passwd User password.
* @return
*/
@throws[NCE]
def signin(email: String, passwd: String): Option[String] = {
ensureStarted()
catching(wrapIE) {
NCTxManager.startTx {
NCSql.sql {
NCSqlManager.getUserByEmail(email)
} match {
case Some(usr) ⇒
NCSql.sql {
if (!NCSqlManager.isKnownPasswordHash(NCBlowfishHasher.hash(passwd, usr.passwordSalt)))
None
else {
val acsTkn = U.genGuid()
val now = U.nowUtcMs()
tokenSigninCache += acsTkn → SigninSession(acsTkn, usr.id, now, now, Set.empty)
idSigninCache(usr.id) match {
case Some(toks) ⇒ idSigninCache += usr.id → (toks ++ Set(acsTkn))
case None ⇒ idSigninCache += usr.id → Set(acsTkn)
}
// Notification.
NCNotificationManager.addEvent("NC_USER_SIGNED_IN",
"userId" → usr.id,
"firstName" → usr.firstName,
"lastName" → usr.lastName,
"email" → usr.email
)
logger.info(s"User signed in [" +
s"email=${usr.email}, " +
s"name=${usr.firstName} ${usr.lastName}" +
s"]")
Some(acsTkn)
}
}
case None ⇒ None
}
}
}
}
/**
*
* @param usrId
* @param firstName
* @param lastName
* @param avatarUrl
* @param props
* @return
*/
@throws[NCE]
def updateUser(
usrId: Long,
firstName: String,
lastName: String,
avatarUrl: Option[String],
props: Option[Map[String, String]]
): Unit = {
ensureStarted()
NCSql.sql {
val n =
NCSqlManager.updateUser(
usrId,
firstName,
lastName,
avatarUrl,
props
)
if (n == 0)
throw new NCE(s"Unknown user ID: $usrId")
}
// Notification.
NCNotificationManager.addEvent("NC_USER_UPDATE",
"userId" → usrId,
"firstName" → firstName,
"lastName" → lastName
)
}
/**
*
* @param usrId
* @param isAdmin
* @return
*/
@throws[NCE]
def updateUserPermissions(usrId: Long, isAdmin: Boolean): Unit = {
ensureStarted()
NCSql.sql {
val n = NCSqlManager.updateUser(usrId, isAdmin)
if (n == 0)
throw new NCE(s"Unknown user ID: $usrId")
}
// Notification.
NCNotificationManager.addEvent("NC_USER_UPDATE",
"userId" → usrId,
"isAdmin" → isAdmin
)
}
/**
*
* @param usrId
* @return
*/
@throws[NCE]
def deleteUser(usrId: Long): Unit = {
ensureStarted()
val usr =
NCSql.sql {
val usr = NCSqlManager.getUser(usrId).getOrElse(throw new NCE(s"Unknown user ID: $usrId"))
NCSqlManager.deleteUser(usr.id)
usr
}
// Notification.
NCNotificationManager.addEvent("NC_USER_DELETE",
"userId" → usrId,
"firstName" → usr.firstName,
"lastName" → usr.lastName,
"email" → usr.email
)
}
/**
*
* @param usrId ID of the user to reset password for.
* @param newPasswd New password to set.
*/
@throws[NCE]
def resetPassword(usrId: Long, newPasswd: String): Unit = {
ensureStarted()
NCSql.sql {
val usr = NCSqlManager.getUser(usrId).getOrElse(throw new NCE(s"Unknown user ID: $usrId"))
val salt = NCBlowfishHasher.hash(usr.email)
// Add actual hash for the password.
// NOTE: we don't "stir up" password pool for password resets.
NCSqlManager.addPasswordHash(pwdSeq.incrementAndGet(), NCBlowfishHasher.hash(newPasswd, salt))
}
catching(wrapIE) {
NCTxManager.startTx {
idSigninCache(usrId) match {
case Some(toks) ⇒
tokenSigninCache --= toks
idSigninCache -= usrId
case None ⇒ // No-op.
}
}
}
// Notification.
NCNotificationManager.addEvent("NC_USER_PASSWD_RESET",
"userId" → usrId
)
}
/**
*
* @param email
* @param pwd
* @param firstName
* @param lastName
* @param avatarUrl
* @param isAdmin
* @return
*/
@throws[NCE]
def addUser(
email: String,
pwd: String,
firstName: String,
lastName: String,
avatarUrl: Option[String],
isAdmin: Boolean,
props: Option[Map[String, String]]
): Long = {
ensureStarted()
val id =
NCSql.sql {
addUser0(email, pwd, firstName, lastName, avatarUrl, isAdmin, props)
}
NCNotificationManager.addEvent(
"NC_USER_ADD",
"userId" → id,
"firstName" → firstName,
"lastName" → lastName,
"email" → email
)
logger.info(s"User $email created.")
id
}
/**
*
* @param email
* @param pwd
* @param firstName
* @param lastName
* @param avatarUrl
* @param isAdmin
*/
@throws[NCE]
def addUser0(
email: String,
pwd: String,
firstName: String,
lastName: String,
avatarUrl: Option[String],
isAdmin: Boolean,
props: Option[Map[String, String]]
): Long =
// Some database implementations (including Ignite database) may not support unique constraints.
// Because we have to support user email unique values, adding user operation is synchronized.
try {
userLock.acquire()
val normEmail = U.normalizeEmail(email)
if (!EMAIL_VALIDATOR.isValid(normEmail))
throw new NCE(s"New user email is invalid: $normEmail")
if (NCSqlManager.getUserByEmail(normEmail).isDefined)
throw new NCE(s"User with this email already exists: $normEmail")
val newUsrId = usersSeq.incrementAndGet()
val salt = NCBlowfishHasher.hash(normEmail)
NCSqlManager.addUser(
newUsrId,
normEmail,
firstName,
lastName,
avatarUrl,
salt,
isAdmin,
props
)
// Add actual hash for the password.
NCSqlManager.addPasswordHash(pwdSeq.incrementAndGet(), NCBlowfishHasher.hash(pwd, salt))
// "Stir up" password pool with each user.
(0 to Math.round((Math.random() * Config.pwdPoolBlowup) + Config.pwdPoolBlowup).toInt).foreach(_ ⇒
NCSqlManager.addPasswordHash(pwdSeq.incrementAndGet(), NCBlowfishHasher.hash(U.genGuid()))
)
newUsrId
}
finally
userLock.release()
/**
*
* @param ses
*/
private def clearSigninCache(ses: SigninSession): Unit =
idSigninCache(ses.userId) match {
case Some(toks) ⇒
val fixedToks = toks -- Seq(ses.acsToken)
if (fixedToks.isEmpty)
idSigninCache -= ses.userId
else
idSigninCache += ses.userId → fixedToks
case None ⇒ // No-op.
}
/**
* Updates endpoint for user session.
*
* @param tok User login token.
* @param change Change method.
*/
private def updateEndpoints(tok: String, change: Set[String] ⇒ Set[String]): Option[Long] =
catching(wrapIE) {
tokenSigninCache(tok) match {
case Some(ses) ⇒ Some(ses)
case None ⇒
logger.error(s"Token cache not found for: $tok")
None
}
} match {
case Some(ses) ⇒
tokenSigninCache +=
ses.acsToken →
SigninSession(ses.acsToken, ses.userId, ses.signinMs, ses.lastAccessMs, change(ses.endpoints)
)
Some(ses.userId)
case None ⇒ None
}
/**
* Registers session level user endpoint.
*
* @param tok User login token.
* @param ep Endpoint URL.
*/
def registerEndpoint(tok: String, ep: String): Unit = {
ensureStarted()
updateEndpoints(tok, (eps: Set[String]) ⇒ eps ++ Set(ep)) match {
case Some(usrId) ⇒
// Notification.
NCNotificationManager.addEvent("NC_USER_ADD_ENDPOINT",
"userId" → usrId,
"endpoint" → ep
)
case None ⇒ None
}
}
/**
* De-registers session level user endpoint if some was registered
*
* @param tok User login token.
* @param ep Endpoint URL.
*/
def removeEndpoint(tok: String, ep: String): Unit = {
ensureStarted()
updateEndpoints(tok, (eps: Set[String]) ⇒ eps -- Set(ep)) match {
case Some(usrId) ⇒
// Notification.
NCNotificationManager.addEvent("NC_USER_REMOVE_ENDPOINT",
"userId" → usrId,
"endpoint" → ep
)
case None ⇒ // No-op.
}
}
/**
* De-registers session level user endpoints if some was registered.
*
* @param tok User login token.
*/
def removeEndpoints(tok: String): Unit = {
ensureStarted()
updateEndpoints(tok, (_: Set[String]) ⇒ Set.empty) match {
case Some(usrId) ⇒
// Notification.
NCNotificationManager.addEvent("NC_USER_REMOVE_ENDPOINTS",
"userId" → usrId
)
case None ⇒ // No-op.
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy