diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index cbe1955e9..398c648d1 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -166,6 +166,8 @@ class Boot extends Loggable{ Props.get("connector").getOrElse("") == "kafka" ) schemifyAll() + val superuser = Role.findOrCreateAndSave("superuser", "a category", code.model.dataAccess.MappedPermission.fromAPermission(APermission.all)) + // This sets up MongoDB config (for the mongodb connector) if(Props.get("connector").getOrElse("") == "mongodb") MongoConfig.init @@ -382,6 +384,8 @@ class Boot extends Loggable{ object ToSchemify { val models = List(OBPUser, + Role, + MappedPermission, Admin, Nonce, Token, diff --git a/src/main/scala/code/model/dataAccess/APermission.scala b/src/main/scala/code/model/dataAccess/APermission.scala new file mode 100644 index 000000000..5c52b9dc3 --- /dev/null +++ b/src/main/scala/code/model/dataAccess/APermission.scala @@ -0,0 +1,72 @@ +package code.model.dataAccess + + + + +/* APermission.scala */ + +/* + * A permission that has three parts; domain, actions, and entities + */ +case class APermission( + val domain: String, + val actions: Set[String] = Set(APermission.wildcardToken), + val entities: Set[String] = Set(APermission.wildcardToken)) +{ + def implies(p: APermission) = p match { + case APermission(d, a, e) => + if (d == APermission.wildcardToken) + true + else if (d == this.domain) { + if (a.contains(APermission.wildcardToken)) + true + else if (this.actions.headOption.map(it => a.contains(it)).getOrElse(false)) + if (e.contains(APermission.wildcardToken)) + true + else + this.entities.headOption.map(it => e.contains(it)).getOrElse(false) + else + false + } + else + false + case _ => false + } + + def implies(ps: Set[APermission]): Boolean = ps.exists(this.implies) + + override def toString = { + domain+APermission.partDivider+actions.mkString(APermission.subpartDivider)+APermission.partDivider+entities.mkString(APermission.subpartDivider) + } + +} + +object APermission { + // Permission + val permissionWilcardToken = "*" + val permissionPartDivider = ":" + val permissionSubpartDivider = "," + val permissionCaseSensitive = true + + lazy val wildcardToken = permissionWilcardToken + lazy val partDivider = permissionPartDivider + lazy val subpartDivider = permissionSubpartDivider + + def apply(domain: String, actions: String): APermission = + apply(domain, actions.split(APermission.subpartDivider).toSet) + + def apply(domain: String, actions: String, entities: String): APermission = + apply(domain, actions.split(APermission.subpartDivider).toSet, entities.split(APermission.subpartDivider).toSet) + + def fromString(s: String): APermission = s.split(partDivider).toList match { + case s :: Nil if (s == wildcardToken) => all + case s :: Nil if (s.length == 0) => none + case dom :: Nil => APermission(dom) + case dom :: acts :: Nil => APermission(dom, acts) + case dom :: acts :: ents :: Nil => APermission(dom, acts, ents) + case _ => none + } + + lazy val all = APermission(wildcardToken) + lazy val none = APermission("") +} \ No newline at end of file diff --git a/src/main/scala/code/model/dataAccess/MappedPermission.scala b/src/main/scala/code/model/dataAccess/MappedPermission.scala new file mode 100644 index 000000000..731c4f692 --- /dev/null +++ b/src/main/scala/code/model/dataAccess/MappedPermission.scala @@ -0,0 +1,45 @@ +package code.model.dataAccess + +import net.liftweb.mapper._ + + +/* + * Simple record for storing permissions. + */ +object MappedPermission extends MappedPermission with LongKeyedMetaMapper[MappedPermission] { + + def createUserPermission(uid: Long, aPerm: APermission) = { + create.userId(uid).permission(aPerm.toString) + } + + def removeAllUserPermissions(uid: Long) = { + MappedPermission.findAll(By(userId, uid)).map(_.delete_!) + } + + def toAPermission(perm: MappedPermission) = APermission.fromString(perm.permission.get) + def fromAPermission(aPerm: APermission): MappedPermission = MappedPermission.create.permission(aPerm.toString) + + def userPermissions(uid: Long): List[APermission] = MappedPermission.findAll(By(userId, uid)).map(toAPermission) + +} + +class MappedPermission extends LongKeyedMapper[MappedPermission] with IdPK { + def getSingleton = MappedPermission + + /** + * This field is empty for permissions attached directly to the user + */ + object roleId extends MappedStringForeignKey(this, Role, 32) { + def foreignMeta = Role + } + + /** + * This field is empty for permissions attached to a role + */ + object userId extends MappedLong(this) { + override def dbIndexed_? = true + } + + object permission extends MappedString(this, 1024) +} + diff --git a/src/main/scala/code/model/dataAccess/MappedRole.scala b/src/main/scala/code/model/dataAccess/MappedRole.scala new file mode 100644 index 000000000..9d7e1f5a4 --- /dev/null +++ b/src/main/scala/code/model/dataAccess/MappedRole.scala @@ -0,0 +1,53 @@ +package code.model.dataAccess + +import net.liftweb.common.{Box, Empty, Full} +import net.liftweb.mapper.{MappedString, Mapper} + + +abstract class MappedRole[T<:Mapper[T]](_fieldOwner: T) extends MappedString[T](_fieldOwner, 1024) { + + import scala.xml._ + import net.liftweb.http.SHtml._ + + def buildDisplayList: List[(String, String)] = { + Role.allRoles(Role.CAT_TEAM) map (r => (r.id.get, r.displayName)) + } + + def chosenElement = names match { + case name :: _ => Full(name) + case _ => Empty + } + + override def _toForm: Box[Elem] = + Full(selectObj[String](buildDisplayList, chosenElement, v => this.setRole(v))) + + override def asHtml = firstRole.map(_.asHtml).openOr(Text("")) + + def permissions: List[APermission] = names.flatMap(n => Role.find(n).map(_.permissions.allPerms).openOr(Nil)) + + def names: List[String] = { + val current = if (get == null) "" else get + current.split(",").toList + } + + def addRole(role: String*) = { + val current = if (get == null) "" else get + val add = (if (current.length() > 0) "," else "") + role.mkString(",") + set(current + add) + fieldOwner + } + + def setRole(role: String): T = { + removeAll + addRole(role) + fieldOwner + } + + def setRole(role: Role): T = setRole(role.id.get) + + def removeAll = { + set(""); this + } + + def firstRole: Box[Role] = names.headOption.flatMap(name => Role.find(name)) +} \ No newline at end of file diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 3724c1a02..3748b75d1 100755 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -36,7 +36,7 @@ import net.liftweb.util.Mailer.{BCC, To, Subject, From} import net.liftweb.util._ import net.liftweb.common._ import scala.xml.{Text, NodeSeq} -import net.liftweb.http.{SHtml, SessionVar, Templates, S} +import net.liftweb.http._ import net.liftweb.http.js.JsCmds.FocusOnLoad import java.util.UUID @@ -49,6 +49,45 @@ import code.sandbox.SandboxUserImport class OBPUser extends MegaProtoUser[OBPUser] with Logger { def getSingleton = OBPUser // what's the "meta" server + /* Roles START */ + + /* + * String representing the User ID + */ + override def userIdAsString: String = id.toString + + /* + * A list of this user's permissions + */ + object userRoles extends MappedRole(this) + lazy val authPermissions: Set[APermission] = (MappedPermission.userPermissions(id.get) ::: userRoles.permissions).toSet + + /* + * A list of this user's roles + */ + lazy val authRoles: Set[String] = userRoles.names.toSet + + // Request var that holds the User instance + //private object curUserId extends SessionVar[Box[String]](Empty) + //def currentUserId: Box[String] = curUserId.is + //private object curUser extends RequestVar[Box[OBPUser]](currentUserId.flatMap(findByStringId)) + //with CleanRequestVarOnSessionTransition { + // override lazy val __nameSalt = Helpers.nextFuncName + //} + //def currentUser: Box[OBPUser] = curUser.is + + import net.liftweb.mapper._ + + def findByStringId(strId: String): Box[OBPUser] = + try { + OBPUser.find(By(id, strId.toLong)) + } catch { + case e: Exception => Empty + } + + /* Roles END */ + + object user extends MappedLongForeignKey(this, APIUser) /** @@ -126,8 +165,8 @@ import net.liftweb.util.Helpers._ override def screenWrap = Full() // define the order fields will appear in forms and output - override def fieldOrder = List(id, firstName, lastName, email, password, provider) - override def signupFields = List(firstName, lastName, email, password) + //override def fieldOrder = List(id, firstName, lastName, email, password, provider) + //override def signupFields = List(firstName, lastName, email, password) // comment this line out to require email validations override def skipEmailValidation = true @@ -260,26 +299,40 @@ import net.liftweb.util.Helpers._ val producer: KafkaProducer = new KafkaProducer() // Send request to Kafka, marked with reqId // so we can fetch the corresponding response - val argList = Map( "email" -> username, - "password" -> password ) - producer.send(reqId, "getUser", argList, "1") + val isTemenos = username.endsWith("@TEMENOS") + val argList = if (isTemenos) { + Map("username" -> username.replaceAll("@TEMENOS", ""), + "password" -> password) + } else { + Map("email" -> username, + "password" -> password) + } + + producer.send(reqId, "getUser", argList, "1") // Request sent, now we wait for response with the same reqId val consumer = new KafkaConsumer() // Create entry only for the first item on returned list val r = consumer.getResponse(reqId).head + println("-------------------> " + r) // For testing without Kafka //val r = Map("email"->"test@email.me","password"->"secret","display_name"->"DN") - var recDisplayName = r.getOrElse("display_name", "Not Found") - var recEmail = r.getOrElse("email", "Not Found") + val recDisplayName = if (isTemenos) r.getOrElse("Name", "Not Found") + else r.getOrElse("display_name", "Not Found") + + val recEmail = if (isTemenos) r.getOrElse("Id", "Not Found") + "@TEMENOS" + else r.getOrElse("email", "Not Found") + + val recRoles = if (isTemenos) List(r.getOrElse("UserType", "Not Found")) + else r.getOrElse("roles", List()).asInstanceOf[List[String]] if (recEmail == username && recEmail != "Not Found") { if (recDisplayName == "") - Full(new SandboxUserImport( username, password, recEmail)) + Full(new SandboxUserImport( username, password, recRoles, recEmail)) else - Full(new SandboxUserImport( username, password, recDisplayName)) + Full(new SandboxUserImport( username, password, recRoles, recDisplayName)) } else { // If empty result from Kafka return empty data Empty @@ -308,10 +361,13 @@ import net.liftweb.util.Helpers._ case _ => 0 } } + def hasRole(role: String): Boolean = currentUser.map(u => hasRole(u, role)).openOr(false) + + def hasRole(user: OBPUser, role: String): Boolean = user.authRoles.exists(_ == role) def getExternalUser(username: String, password: String):Box[OBPUser] = { getUserViaKafka(username, password) match { - case Full(SandboxUserImport(extEmail, extPassword, extDisplayName)) => { + case Full(SandboxUserImport(extEmail, extPassword, extRoles, extDisplayName)) => { val preLoginState = capturePreLoginState() info("external user authenticated. login redir: " + loginRedirect.get) val redir = loginRedirect.get match { @@ -346,6 +402,16 @@ import net.liftweb.util.Helpers._ .password(dummyPassword) .provider(extProvider) .validated(true) + + for ( r <- extRoles ) { + info("adding role (" + r + ") to new user") + if (Role.find(r).openOr("") != "") { + newUser.userRoles.addRole(r).saveMe + info("role (" + r + ") added") + } + else + info("role (" + r + ") not defined") + } // Save the user in order to be able to log in newUser.save() // Return created user @@ -398,6 +464,8 @@ import net.liftweb.util.Helpers._ val user = getExternalUser(S.param("username").get, S.param("password").get) if (!user.isEmpty) { + println ("-----------------> admin: " + hasRole(user.get, "admin") ) + println ("-----------------> test: " + hasRole(user.get, "test") ) logUserIn(user.get, () => { S.notice(S.?("logged.in")) @@ -460,3 +528,6 @@ import net.liftweb.util.Helpers._ innerSignup } } + + + diff --git a/src/main/scala/code/model/dataAccess/Role.scala b/src/main/scala/code/model/dataAccess/Role.scala new file mode 100644 index 000000000..6c326ec3a --- /dev/null +++ b/src/main/scala/code/model/dataAccess/Role.scala @@ -0,0 +1,74 @@ +package code.model.dataAccess + +import net.liftweb.common.Loggable +import net.liftweb.http.S +import net.liftweb.mapper._ + +/* + * Simple record for storing roles. Role name is the PK. + */ +object Role extends Role with KeyedMetaMapper[String, Role] with Loggable { + + val R_SUPERUSER = "superuser" + val R_USER = "user" + val R_TEAM_OWNER = "owner" + val R_TEAM_MEMBER = "member" + val R_TEAM_WATCHER = "watcher" + + val CAT_SYSTEM = "system" + val CAT_TEAM = "team" + + override def dbTableName = "roles" + + def findOrCreate(roleId: String): Role = find(roleId).openOr(create.id(roleId)) + def findOrCreateAndSave(roleId: String, category: String, perms: MappedPermission*): Role = { + find(roleId).openOr { + logger.info("Create Role %s for category %s".format(roleId, category)) + val r = create.id(roleId).category(category) + r.permissions.appendAll(perms) + r.saveMe + } + } + + def allRoles(cat: String) = Role.findAll(By(category, cat)) + + lazy val TeamOwner = Role.find(R_TEAM_OWNER).openOrThrowException("No Owner Role found") + lazy val TeamMember = Role.find(R_TEAM_MEMBER).openOrThrowException("No Member Role found") + lazy val TeamWatcher = Role.find(R_TEAM_WATCHER).openOrThrowException("No Watcher Role found") + +} + +class Role extends KeyedMapper[String, Role] with OneToMany[String,Role] { + def getSingleton = Role + def primaryKeyField = id + object id extends MappedStringIndex(this, 32) { + override def writePermission_? = true + override def dbAutogenerated_? = false + override def dbNotNull_? = true + override def dbIndexed_? = true + override def displayName = "Name" + } + + object category extends MappedString(this, 50) + + object permissions extends MappedOneToMany(MappedPermission, MappedPermission.roleId) { + def allPerms: List[APermission] = all.map(MappedPermission.toAPermission) + } + + override def equals(other: Any): Boolean = other match { + case r: Role => r.id.get == this.id.get + case _ => false + } + + def displayName() = S ? ("userClientConnection.role."+id.get) + + override def asHtml = { + val cls = "label" + (id.get match { + case Role.R_TEAM_OWNER => " label-important" + case Role.R_TEAM_MEMBER => " label-info" + case _ => "" + }) + {displayName} + } + +} diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index 03030c65a..62f62cddf 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -597,6 +597,7 @@ case class SandboxLocationImport( case class SandboxUserImport( email : String, password : String, + roles: List[String], display_name : String) case class SandboxAccountImport( diff --git a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala index 4fd795b2a..f1c202270 100644 --- a/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala +++ b/src/test/scala/code/sandbox/SandboxDataLoadingTest.scala @@ -457,8 +457,8 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul val standardProducts = product1AtBank1 :: product2AtBank1 :: Nil - val user1 = SandboxUserImport(email = "user1@example.com", password = "qwerty", display_name = "User 1") - val user2 = SandboxUserImport(email = "user2@example.com", password = "qwerty", display_name = "User 2") + val user1 = SandboxUserImport(email = "user1@example.com", password = "qwerty", display_name = "User 1", roles = List()) + val user2 = SandboxUserImport(email = "user2@example.com", password = "qwerty", display_name = "User 2", roles = List()) val standardUsers = user1 :: user2 :: Nil