diff --git a/user-management/pom-template.xml b/user-management/pom-template.xml
index fa3dd82c5..27c58bda4 100644
--- a/user-management/pom-template.xml
+++ b/user-management/pom-template.xml
@@ -48,6 +48,12 @@
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
diff --git a/user-management/src/main/scala/bootstrap/rudder/plugin/UserManagementConf.scala b/user-management/src/main/scala/bootstrap/rudder/plugin/UserManagementConf.scala
index fa361ebbd..63050ef70 100644
--- a/user-management/src/main/scala/bootstrap/rudder/plugin/UserManagementConf.scala
+++ b/user-management/src/main/scala/bootstrap/rudder/plugin/UserManagementConf.scala
@@ -38,6 +38,7 @@
package bootstrap.rudder.plugin
import bootstrap.liftweb.RudderConfig
+import bootstrap.liftweb.UserFileProcessing
import com.normation.plugins.PluginStatus
import com.normation.plugins.RudderPluginModule
import com.normation.plugins.usermanagement.CheckRudderPluginEnableImpl
@@ -66,9 +67,9 @@ object UserManagementConf extends RudderPluginModule {
lazy val api = new UserManagementApiImpl(
RudderConfig.userRepository,
- RudderConfig.restExtractorService,
RudderConfig.rudderUserListProvider,
- new UserManagementService(RudderConfig.userRepository)
+ RudderConfig.authenticationProviders,
+ new UserManagementService(RudderConfig.userRepository, UserFileProcessing.getUserResourceFile())
)
RudderConfig.userAuthorisationLevel.overrideLevel(new UserManagementAuthorizationLevel(pluginStatusService))
diff --git a/user-management/src/main/scala/com/normation/plugins/usermanagement/DataTypes.scala b/user-management/src/main/scala/com/normation/plugins/usermanagement/DataTypes.scala
index da0052c9b..c1ce6b52b 100644
--- a/user-management/src/main/scala/com/normation/plugins/usermanagement/DataTypes.scala
+++ b/user-management/src/main/scala/com/normation/plugins/usermanagement/DataTypes.scala
@@ -37,16 +37,17 @@
package com.normation.plugins.usermanagement
+import bootstrap.liftweb.AuthBackendProvidersManager
import bootstrap.liftweb.PasswordEncoder
-import bootstrap.liftweb.RudderConfig
import bootstrap.liftweb.ValidatedUserList
+import com.normation.rudder.Role
import com.normation.rudder.Role.Custom
import com.normation.rudder.RudderRoles
import com.normation.zio._
+import io.scalaland.chimney.Transformer
import net.liftweb.common.Logger
-import net.liftweb.json.{Serialization => S}
-import net.liftweb.json.JsonAST.JValue
import org.slf4j.LoggerFactory
+import zio.json._
/**
* Applicative log of interest for Rudder ops.
@@ -57,22 +58,38 @@ object UserManagementLogger extends Logger {
object Serialisation {
+ implicit val jsonUserFormDataDecoder: JsonDecoder[JsonUserFormData] = DeriveJsonDecoder.gen[JsonUserFormData]
+ implicit val jsonRoleAuthorizationsDecoder: JsonDecoder[JsonRoleAuthorizations] = DeriveJsonDecoder.gen[JsonRoleAuthorizations]
+
+ implicit val jsonUserEncoder: JsonEncoder[JsonUser] = DeriveJsonEncoder.gen[JsonUser]
+ implicit val jsonAuthConfigEncoder: JsonEncoder[JsonAuthConfig] = DeriveJsonEncoder.gen[JsonAuthConfig]
+ implicit val jsonRoleEncoder: JsonEncoder[JsonRole] = DeriveJsonEncoder.gen[JsonRole]
+ implicit val jsonInternalUserDataEncoder: JsonEncoder[JsonInternalUserData] = DeriveJsonEncoder.gen[JsonInternalUserData]
+ implicit val jsonAddedUserEncoder: JsonEncoder[JsonAddedUser] = DeriveJsonEncoder.gen[JsonAddedUser]
+ implicit val jsonUpdatedUserEncoder: JsonEncoder[JsonUpdatedUser] = DeriveJsonEncoder.gen[JsonUpdatedUser]
+ implicit val jsonUsernameEncoder: JsonEncoder[JsonUsername] = DeriveJsonEncoder.gen[JsonUsername]
+ implicit val jsonDeletedUserEncoder: JsonEncoder[JsonDeletedUser] = DeriveJsonEncoder.gen[JsonDeletedUser]
+ implicit val jsonReloadStatusEncoder: JsonEncoder[JsonReloadStatus] = DeriveJsonEncoder.gen[JsonReloadStatus]
+ implicit val jsonReloadResultEncoder: JsonEncoder[JsonReloadResult] = DeriveJsonEncoder.gen[JsonReloadResult]
+ implicit val jsonRoleCoverageEncoder: JsonEncoder[JsonRoleCoverage] = DeriveJsonEncoder.gen[JsonRoleCoverage]
+ implicit val jsonCoverageEncoder: JsonEncoder[JsonCoverage] = DeriveJsonEncoder.gen[JsonCoverage]
+
implicit class AuthConfigSer(auth: ValidatedUserList) {
- def toJson: JValue = {
+ def serialize(implicit authProviderManager: AuthBackendProvidersManager): JsonAuthConfig = {
val encoder: String = PassEncoderToString(auth)
- val authBackendsProvider = RudderConfig.authenticationProviders.getConfiguredProviders().map(_.name).toSet
+ val authBackendsProvider = authProviderManager.getConfiguredProviders().map(_.name).toSet
// for now, we can only guess if the role list can be extended/overridden (and only guess for the worse).
// The correct solution is to get that from rudder, but it will be done along with other enhancement about
// user / roles management.
// Also, until then, we need to update that test is other backend get that possibility
- val roleListOverride = if(authBackendsProvider.contains("oidc") || authBackendsProvider.contains("oauth2")) {
+ val roleListOverride = if (authBackendsProvider.contains("oidc") || authBackendsProvider.contains("oauth2")) {
"override" // should be a type provided by rudder core
} else {
"none"
}
- val jUser = auth.users.map {
+ val jUser = auth.users.map {
case (_, u) =>
val (rs, custom) = {
UserManagementService
@@ -89,10 +106,8 @@ object Serialisation {
rs.map(_.name)
)
}.toList.sortBy(_.login)
- val json = JsonAuthConfig(encoder, roleListOverride, authBackendsProvider, jUser)
- import net.liftweb.json._
- implicit val formats = S.formats(NoTypeHints)
- Extraction.decompose(json)
+ val json = JsonAuthConfig(encoder, roleListOverride, authBackendsProvider, jUser)
+ json
}
}
@@ -120,3 +135,89 @@ final case class JsonUser(
authz: Set[String],
permissions: Set[String]
)
+
+final case class JsonRole(
+ @jsonField("id") name: String,
+ rights: List[String]
+)
+
+final case class JsonReloadResult(reload: JsonReloadStatus)
+
+object JsonReloadResult {
+ val Done = JsonReloadResult(JsonReloadStatus("Done"))
+}
+final case class JsonReloadStatus(status: String)
+
+final case class JsonInternalUserData(
+ username: String,
+ password: String,
+ permissions: List[String]
+)
+
+object JsonInternalUserData {
+ implicit val transformer: Transformer[User, JsonInternalUserData] = Transformer.derive[User, JsonInternalUserData]
+}
+
+final case class JsonAddedUser(
+ addedUser: JsonInternalUserData
+) extends AnyVal
+object JsonAddedUser {
+ implicit val transformer: Transformer[User, JsonAddedUser] = Transformer.derive[User, JsonAddedUser]
+}
+
+final case class JsonUpdatedUser(
+ updatedUser: JsonInternalUserData
+) extends AnyVal
+object JsonUpdatedUser {
+ implicit val transformer: Transformer[User, JsonUpdatedUser] = Transformer.derive[User, JsonUpdatedUser]
+}
+
+final case class JsonUsername(
+ username: String
+)
+
+final case class JsonDeletedUser(
+ deletedUser: JsonUsername
+) extends AnyVal
+object JsonDeletedUser {
+ implicit val usernameTransformer: Transformer[String, JsonUsername] = JsonUsername(_)
+ implicit val transformer: Transformer[String, JsonDeletedUser] = Transformer.derive[String, JsonDeletedUser]
+}
+
+final case class JsonUserFormData(
+ username: String,
+ password: String,
+ permissions: List[String],
+ isPreHashed: Boolean
+)
+
+object JsonUserFormData {
+ implicit val transformer: Transformer[JsonUserFormData, User] = Transformer.derive[JsonUserFormData, User]
+}
+
+final case class JsonCoverage(
+ coverage: JsonRoleCoverage
+) extends AnyVal
+object JsonCoverage {
+ implicit val transformer: Transformer[(Set[Role], Set[Custom]), JsonCoverage] =
+ Transformer.derive[(Set[Role], Set[Custom]), JsonCoverage]
+}
+
+final case class JsonRoleCoverage(
+ permissions: Set[String],
+ custom: List[String]
+)
+
+object JsonRoleCoverage {
+ implicit private[JsonRoleCoverage] val roleTransformer: Transformer[Role, String] = _.name
+ implicit private[JsonRoleCoverage] val customRolesTransformer: Transformer[Set[Custom], List[String]] =
+ _.flatMap(_.rights.authorizationTypes.map(_.id)).toList.sorted
+
+ implicit val transformer: Transformer[(Set[Role], Set[Custom]), JsonRoleCoverage] =
+ Transformer.derive[(Set[Role], Set[Custom]), JsonRoleCoverage]
+}
+
+final case class JsonRoleAuthorizations(
+ permissions: List[String],
+ authz: List[String]
+)
diff --git a/user-management/src/main/scala/com/normation/plugins/usermanagement/Serialization.scala b/user-management/src/main/scala/com/normation/plugins/usermanagement/Serialization.scala
deleted file mode 100644
index 09f2506cf..000000000
--- a/user-management/src/main/scala/com/normation/plugins/usermanagement/Serialization.scala
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.normation.plugins.usermanagement
-
-import com.normation.rudder.Role
-import com.normation.rudder.Role.Custom
-import net.liftweb.json.JsonAST.JValue
-import net.liftweb.json.JsonDSL._
-
-object Serialization {
- def serializeRoleInfo(infos: Map[String, List[String]]): JValue = {
- infos.map {
- case (k, v) =>
- (("id" -> k)
- ~ ("rights" -> v))
- }
- }
-
- def serializeUser(u: User): JValue = {
- (("username" -> u.username)
- ~ ("password" -> u.password)
- ~ ("permissions" -> u.permissions))
- }
-
- def serializeRole(rs: Set[Role]): JValue = {
- val (permissions, customs) = rs.partition {
- case Custom(_) => false
- case _ => true
- }
- (("permissions" -> permissions.map(_.name))
- ~ ("custom" -> customs.flatMap(_.rights.authorizationTypes.map(_.id)).toSeq.sorted))
- }
-}
diff --git a/user-management/src/main/scala/com/normation/plugins/usermanagement/UserManagementService.scala b/user-management/src/main/scala/com/normation/plugins/usermanagement/UserManagementService.scala
index c9513f686..c666d28cb 100644
--- a/user-management/src/main/scala/com/normation/plugins/usermanagement/UserManagementService.scala
+++ b/user-management/src/main/scala/com/normation/plugins/usermanagement/UserManagementService.scala
@@ -111,14 +111,11 @@ object UserManagementIO {
}
}
- def getUserFilePath: IOResult[File] = {
- val resources: IOResult[UserFile] = UserFileProcessing.getUserResourceFile()
- resources.map { r =>
- if (r.name.startsWith("classpath:"))
- File(new CPResource(UserFileProcessing.DEFAULT_AUTH_FILE_NAME).getPath)
- else
- File(r.name)
- }
+ def getUserFilePath(resourceFile: UserFile): File = {
+ if (resourceFile.name.startsWith("classpath:"))
+ File(new CPResource(UserFileProcessing.DEFAULT_AUTH_FILE_NAME).getPath)
+ else
+ File(resourceFile.name)
}
}
@@ -196,7 +193,7 @@ object UserManagementService {
}
-class UserManagementService(userRepository: UserRepository) {
+class UserManagementService(userRepository: UserRepository, getUserResourceFile: IOResult[UserFile]) {
import UserManagementService._
/*
@@ -205,7 +202,7 @@ class UserManagementService(userRepository: UserRepository) {
*/
def add(newUser: User, isPreHashed: Boolean): IOResult[User] = {
for {
- file <- getUserFilePath
+ file <- getUserResourceFile.map(getUserFilePath(_))
parsedFile <- IOResult.attempt(ConstructingParser.fromFile(file.toJava, preserveWS = true))
userXML <- IOResult.attempt(parsedFile.document().children)
user <- (userXML \\ "authentication").head match {
@@ -234,7 +231,7 @@ class UserManagementService(userRepository: UserRepository) {
*/
def remove(toDelete: String, actor: EventActor): IOResult[Unit] = {
for {
- file <- getUserFilePath
+ file <- getUserResourceFile.map(getUserFilePath(_))
parsedFile <- IOResult.attempt(ConstructingParser.fromFile(file.toJava, preserveWS = true))
userXML <- IOResult.attempt(parsedFile.document().children)
toUpdate = (userXML \\ "authentication").head
@@ -254,7 +251,7 @@ class UserManagementService(userRepository: UserRepository) {
*/
def update(currentUser: String, newUser: User, isPreHashed: Boolean): IOResult[Unit] = {
for {
- file <- getUserFilePath
+ file <- getUserResourceFile.map(getUserFilePath(_))
parsedFile <- IOResult.attempt(ConstructingParser.fromFile(file.toJava, preserveWS = true))
userXML <- IOResult.attempt(parsedFile.document().children)
toUpdate = (userXML \\ "authentication").head
@@ -285,7 +282,7 @@ class UserManagementService(userRepository: UserRepository) {
def getAll: IOResult[UserFileInfo] = {
for {
- file <- getUserFilePath
+ file <- getUserResourceFile.map(getUserFilePath(_))
parsedFile <- IOResult.attempt(ConstructingParser.fromFile(file.toJava, preserveWS = true))
userXML <- IOResult.attempt(parsedFile.document().children)
res <- (userXML \\ "authentication").head match {
diff --git a/user-management/src/main/scala/com/normation/plugins/usermanagement/api/UserManagementApi.scala b/user-management/src/main/scala/com/normation/plugins/usermanagement/api/UserManagementApi.scala
index 5747e6f1e..9dc0ab052 100644
--- a/user-management/src/main/scala/com/normation/plugins/usermanagement/api/UserManagementApi.scala
+++ b/user-management/src/main/scala/com/normation/plugins/usermanagement/api/UserManagementApi.scala
@@ -37,24 +37,34 @@
package com.normation.plugins.usermanagement.api
+import bootstrap.liftweb.AuthBackendProvidersManager
import bootstrap.liftweb.FileUserDetailListProvider
-import com.normation.box._
import com.normation.errors._
-import com.normation.plugins.usermanagement.Serialization
+import com.normation.plugins.usermanagement.JsonAddedUser
+import com.normation.plugins.usermanagement.JsonCoverage
+import com.normation.plugins.usermanagement.JsonDeletedUser
+import com.normation.plugins.usermanagement.JsonReloadResult
+import com.normation.plugins.usermanagement.JsonRole
+import com.normation.plugins.usermanagement.JsonRoleAuthorizations
+import com.normation.plugins.usermanagement.JsonUpdatedUser
+import com.normation.plugins.usermanagement.JsonUserFormData
+import com.normation.plugins.usermanagement.Serialisation._
import com.normation.plugins.usermanagement.User
import com.normation.plugins.usermanagement.UserManagementService
import com.normation.rudder.AuthorizationType
import com.normation.rudder.Role
+import com.normation.rudder.Role.Custom
import com.normation.rudder.RudderRoles
import com.normation.rudder.api.ApiAuthorization
import com.normation.rudder.api.ApiVersion
import com.normation.rudder.api.HttpAction.DELETE
import com.normation.rudder.api.HttpAction.GET
import com.normation.rudder.api.HttpAction.POST
+import com.normation.rudder.apidata.ZioJsonExtractor
import com.normation.rudder.facts.nodes.NodeSecurityContext
-import com.normation.rudder.repository.json.DataExtractor.CompleteJson
import com.normation.rudder.rest._
import com.normation.rudder.rest.EndpointSchema.syntax._
+import com.normation.rudder.rest.implicits._
import com.normation.rudder.rest.lift.DefaultParams
import com.normation.rudder.rest.lift.LiftApiModule
import com.normation.rudder.rest.lift.LiftApiModule0
@@ -62,19 +72,13 @@ import com.normation.rudder.rest.lift.LiftApiModuleProvider
import com.normation.rudder.users.RudderAccount
import com.normation.rudder.users.RudderUserDetail
import com.normation.rudder.users.UserRepository
-import com.normation.zio._
import com.softwaremill.quicklens._
-import net.liftweb.common.Box
-import net.liftweb.common.Failure
-import net.liftweb.common.Full
+import io.scalaland.chimney.syntax._
import net.liftweb.http.LiftResponse
import net.liftweb.http.Req
-import net.liftweb.json.Formats
-import net.liftweb.json.JsonDSL._
-import net.liftweb.json.JString
-import net.liftweb.json.JValue
-import net.liftweb.json.NoTypeHints
import sourcecode.Line
+import zio.ZIO
+import zio.syntax._
/*
* This file contains the internal API used to discuss with the JS application.
@@ -151,30 +155,14 @@ object UserManagementApi extends ApiModuleProvider[UserManagementApi] {
class UserManagementApiImpl(
userRepo: UserRepository,
- restExtractorService: RestExtractorService,
userService: FileUserDetailListProvider,
+ authProvider: AuthBackendProvidersManager,
userManagementService: UserManagementService
) extends LiftApiModuleProvider[UserManagementApi] {
api =>
- implicit val formats: Formats = net.liftweb.json.Serialization.formats(NoTypeHints)
-
override def schemas = UserManagementApi
- def extractUser(json: JValue): Box[User] = {
- for {
- username <- CompleteJson.extractJsonString(json, "username")
- password <- CompleteJson.extractJsonString(json, "password")
- permissions <- CompleteJson.extractJsonListString(json, "permissions")
- } yield {
- User(username, password, permissions.toSet)
- }
- }
-
- def extractIsHashed(json: JValue): Box[Boolean] = {
- CompleteJson.extractJsonBoolean(json, "isPreHashed")
- }
-
override def getLiftEndpoints(): List[LiftApiModule] = {
UserManagementApi.endpoints.map {
case UserManagementApi.GetUserInfo => GetUserInfo
@@ -187,18 +175,11 @@ class UserManagementApiImpl(
}.toList
}
- def response(function: Box[JValue], req: Req, errorMessage: String, id: Option[String], dataName: String)(implicit
- action: String
- ): LiftResponse = {
- RestUtils.response(restExtractorService, dataName, id)(function, req, errorMessage)
- }
-
/*
* Return a Json Object that list users with their authorizations
*/
object GetUserInfo extends LiftApiModule0 {
- val schema = UserManagementApi.GetUserInfo
- val restExtractor = api.restExtractorService
+ val schema = UserManagementApi.GetUserInfo
def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = {
import com.normation.plugins.usermanagement.Serialisation._
@@ -217,52 +198,46 @@ class UserManagementApiImpl(
case Some(x) => (x.getUsername, x)
}
})
- file.modify(_.users).setTo(updatedUsers.toMap)
-
- }).either.runNow match {
- case Left(err) =>
- RestUtils.toJsonError(None, JString(s"Error when retrieving user list: ${err.fullMsg}"))(schema.name, params.prettify)
- case Right(authFile) =>
- RestUtils.toJsonResponse(None, authFile.toJson)(schema.name, params.prettify)
- }
+ val authFile = file.modify(_.users).setTo(updatedUsers.toMap)
+ authFile.serialize(authProvider)
+ }).chainError("Error when retrieving user list").toLiftResponseOne(params, schema, None)
}
}
object GetRoles extends LiftApiModule0 {
- val schema = UserManagementApi.GetRoles
- val restExtractor = api.restExtractorService
+ val schema = UserManagementApi.GetRoles
def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = {
- val allRoleAndAuthz: Map[String, List[String]] = RudderRoles.getAllRoles.runNow.values
- .map(role => role.name -> role.rights.authorizationTypes.map(_.id).toList.sorted)
- .map {
- case (k, v) => {
- val authz_all = v
- .map(_.split("_").head)
- .map(authz => if (v.count(_.split("_").head == authz) == 3) s"${authz}_all" else authz)
- .filter(_.contains("_"))
- .distinct
- val authz_type = v.filter(x => !authz_all.map(_.split("_").head).contains(x.split("_").head))
- k -> (authz_type ++ authz_all)
- }
- }
- .toMap
- RestUtils.toJsonResponse(None, Serialization.serializeRoleInfo(allRoleAndAuthz))(schema.name, params.prettify)
+ (for {
+ allRoles <- RudderRoles.getAllRoles
+ roles = allRoles.values.toList
+
+ json = roles.map(role => {
+ val displayAuthz = role.rights.authorizationTypes.map(_.id).toList.sorted
+ val authz_all = displayAuthz
+ .map(_.split("_").head)
+ .map(authz => if (displayAuthz.count(_.split("_").head == authz) == 3) s"${authz}_all" else authz)
+ .filter(_.contains("_"))
+ .distinct
+ val authz_type = displayAuthz.filter(x => !authz_all.map(_.split("_").head).contains(x.split("_").head))
+ JsonRole(role.name, authz_type ++ authz_all)
+ })
+
+ } yield {
+ json
+ }).toLiftResponseOne(params, schema, None)
}
}
object ReloadUsersConf extends LiftApiModule0 {
- val schema = UserManagementApi.ReloadUsersConf
- val restExtractor = api.restExtractorService
+ val schema = UserManagementApi.ReloadUsersConf
def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = {
- implicit val action = "reloadUserConf"
-
- val value: Box[JValue] = for {
- response <- reload().toBox
+ (for {
+ _ <- reload()
} yield {
- "status" -> "Done"
- }
- response(value, req, "Could not reload user's configuration", None, "reload")
+ JsonReloadResult.Done
+ }).chainError("Could not reload user's configuration")
+ .toLiftResponseOne(params, schema, None)
}
}
@@ -271,32 +246,26 @@ class UserManagementApiImpl(
}
object AddUser extends LiftApiModule0 {
- val schema = UserManagementApi.AddUser
- val restExtractor = api.restExtractorService
+ val schema = UserManagementApi.AddUser
def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = {
- implicit val action = "addUser"
-
- val value: Box[JValue] = for {
- json <- req.json ?~! "No JSON data sent"
- user <- extractUser(json)
- isPreHashed <- extractIsHashed(json)
- checkExistence <- if (userService.authConfig.users.keySet contains user.username)
- Failure(s"User '${user.username}' already exists")
- else Full("ok")
- added <- userManagementService.add(user, isPreHashed).toBox
- _ <- reload().toBox
+
+ (for {
+ user <- ZioJsonExtractor.parseJson[JsonUserFormData](req).toIO
+ _ <- ZIO.when(userService.authConfig.users.keySet contains user.username) {
+ Inconsistency(s"User '${user.username}' already exists").fail
+ }
+ added <- userManagementService.add(user.transformInto[User], user.isPreHashed)
+ _ <- reload()
} yield {
- Serialization.serializeUser(added)
- }
- response(value, req, "Could not add user", None, "addedUser")
+ added.transformInto[JsonAddedUser]
+ }).chainError("Could not add user").toLiftResponseOne(params, schema, None)
}
}
object DeleteUser extends LiftApiModule {
- val schema = UserManagementApi.DeleteUser
- val restExtractor = api.restExtractorService
+ val schema = UserManagementApi.DeleteUser
def process(
version: ApiVersion,
@@ -306,21 +275,18 @@ class UserManagementApiImpl(
params: DefaultParams,
authzToken: AuthzToken
): LiftResponse = {
- implicit val action = "deleteUser"
- val value: Box[JValue] = for {
- _ <- userManagementService.remove(id, authzToken.qc.actor).toBox
- _ <- reload().toBox
+ (for {
+ _ <- userManagementService.remove(id, authzToken.qc.actor)
+ _ <- reload()
} yield {
- "username" -> id
- }
- response(value, req, s"Could not delete user ${id}", None, "deletedUser")
+ id.transformInto[JsonDeletedUser]
+ }).chainError(s"Could not delete user ${id}").toLiftResponseOne(params, schema, None)
}
}
object UpdateUserInfos extends LiftApiModule {
- val schema = UserManagementApi.UpdateUserInfos
- val restExtractor = api.restExtractorService
+ val schema = UserManagementApi.UpdateUserInfos
def process(
version: ApiVersion,
@@ -330,32 +296,26 @@ class UserManagementApiImpl(
params: DefaultParams,
authzToken: AuthzToken
): LiftResponse = {
- implicit val action = "updateInfosUser"
-
- val value: Box[JValue] = for {
- json <- req.json ?~! "No JSON data sent"
- user <- extractUser(json)
- isPreHashed <- extractIsHashed(json)
+ (for {
+ user <- ZioJsonExtractor.parseJson[JsonUserFormData](req).toIO
checkExistence <- if (!(userService.authConfig.users.keySet contains id)) {
// we may have users that where added by OIDC, and still want to add them in file
- userRepo.get(id).toBox.flatMap {
- case Some(u) => userManagementService.add(User(u.id, "", Set()), isPreHashed).toBox
- case None => Failure(s"'$id' does not exists")
+ userRepo.get(id).flatMap {
+ case Some(u) => userManagementService.add(User(u.id, "", Set()), user.isPreHashed)
+ case None => Inconsistency(s"'$id' does not exists").fail
}
} else {
- userManagementService.update(id, user, isPreHashed).toBox
+ userManagementService.update(id, user.transformInto[User], user.isPreHashed)
}
- _ <- reload().toBox
+ _ <- reload()
} yield {
- Serialization.serializeUser(user)
- }
- response(value, req, s"Could not update user '$id''", None, "updatedUser")
+ user.transformInto[User].transformInto[JsonUpdatedUser]
+ }).chainError(s"Could not update user '$id'").toLiftResponseOne(params, schema, None)
}
}
object RoleCoverage extends LiftApiModule {
- val schema = UserManagementApi.RoleCoverage
- val restExtractor = api.restExtractorService
+ val schema = UserManagementApi.RoleCoverage
def process(
version: ApiVersion,
@@ -365,20 +325,22 @@ class UserManagementApiImpl(
params: DefaultParams,
authzToken: AuthzToken
): LiftResponse = {
- implicit val action = "rolesCoverageOnRights"
-
- val value: Box[JValue] = for {
- permissions <- restExtractorService.extractList("permissions")(req)(json => Full(json))
- authzs <- restExtractorService.extractList("authz")(req)(json => Full(json))
- parsed <- RudderRoles.parseRoles(permissions).toBox
- coverage <- UserManagementService.computeRoleCoverage(
- parsed.toSet,
- authzs.flatMap(a => AuthorizationType.parseRight(a).getOrElse(Set())).toSet ++ Role.ua
- )
+ (for {
+ data <- ZioJsonExtractor.parseJson[JsonRoleAuthorizations](req).toIO
+ parsed <- RudderRoles.parseRoles(data.permissions)
+ coverage <- UserManagementService
+ .computeRoleCoverage(
+ parsed.toSet,
+ data.authz.flatMap(a => AuthorizationType.parseRight(a).getOrElse(Set())).toSet ++ Role.ua
+ )
+ .notOptional("Could not compute role's coverage")
+ roleAndCustoms = coverage.partitionMap {
+ case c: Custom => Right(c)
+ case r => Left(r)
+ }
} yield {
- Serialization.serializeRole(coverage)
- }
- response(value, req, s"Could not get role's coverage user from request", None, "coverage")
+ roleAndCustoms.transformInto[JsonCoverage]
+ }).chainError(s"Could not get role's coverage user from request").toLiftResponseOne(params, schema, None)
}
}
}
diff --git a/user-management/src/main/scala/com/normation/plugins/usermanagement/snippet/UserPlugin.scala b/user-management/src/main/scala/com/normation/plugins/usermanagement/snippet/UserPlugin.scala
index aabdfad6d..6d0f3d95f 100644
--- a/user-management/src/main/scala/com/normation/plugins/usermanagement/snippet/UserPlugin.scala
+++ b/user-management/src/main/scala/com/normation/plugins/usermanagement/snippet/UserPlugin.scala
@@ -43,10 +43,12 @@ import net.liftweb.http.DispatchSnippet
import net.liftweb.http.js.JE._
import net.liftweb.http.js.JsCmds._
import scala.xml.NodeSeq
+import zio.json._
class UserPlugin extends DispatchSnippet {
- private[this] val userService = RudderConfig.rudderUserListProvider
+ private[this] val userService = RudderConfig.rudderUserListProvider
+ private[this] val authProviderManager = RudderConfig.authenticationProviders
def dispatch = { case "getAuthzConfig" => getAuthzConfig }
@@ -54,6 +56,6 @@ class UserPlugin extends DispatchSnippet {
def getAuthzConfig: NodeSeq => NodeSeq = { xml: NodeSeq =>
import com.normation.plugins.usermanagement.Serialisation._
- Script(JsRaw(s"""var authzConfig = ${userService.authConfig.toJson};"""))
+ Script(JsRaw(s"""var authzConfig = ${userService.authConfig.serialize(authProviderManager).toJson};"""))
}
}
diff --git a/user-management/src/test/resources/test-users.xml b/user-management/src/test/resources/test-users.xml
new file mode 100644
index 000000000..1ba585b0f
--- /dev/null
+++ b/user-management/src/test/resources/test-users.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/user-management/src/test/resources/usermanagement_api/api_usermanagement.yml b/user-management/src/test/resources/usermanagement_api/api_usermanagement.yml
new file mode 100644
index 000000000..ca9a23858
--- /dev/null
+++ b/user-management/src/test/resources/usermanagement_api/api_usermanagement.yml
@@ -0,0 +1,365 @@
+description: Get information about registered users in Rudder
+method: GET
+url: /api/latest/usermanagement/users
+response:
+ code: 200
+ content: >-
+ {
+ "action" : "getUserInfo",
+ "result" : "success",
+ "data" : {
+ "digest" : "BCRYPT",
+ "roleListOverride" : "none",
+ "authenticationBackends" : [],
+ "users" : [
+ {
+ "login" : "user1",
+ "authz" : [],
+ "permissions" : [
+ "inventory",
+ "configuration",
+ "rule_only",
+ "user"
+ ]
+ },
+ {
+ "login" : "user2",
+ "authz" : [],
+ "permissions" : [
+ "inventory",
+ "read_only",
+ "rule_only"
+ ]
+ },
+ {
+ "login" : "user3",
+ "authz" : [],
+ "permissions" : []
+ }
+ ]
+ }
+ }
+---
+description: Get roles and their authorizations
+method: GET
+url: /api/latest/usermanagement/roles
+response:
+ code: 200
+ content: >-
+ {
+ "action" : "getRoles",
+ "result" : "success",
+ "data" : [
+ {
+ "id" : "administration_only",
+ "rights" : [
+ "administration_all",
+ "userAccount_all"
+ ]
+ },
+ {
+ "id" : "administrator",
+ "rights" : [
+ "any_rights"
+ ]
+ },
+ {
+ "id" : "compliance",
+ "rights" : [
+ "configuration_read",
+ "directive_read",
+ "group_read",
+ "node_read",
+ "parameter_read",
+ "rule_read",
+ "technique_read",
+ "compliance_all",
+ "userAccount_all"
+ ]
+ },
+ {
+ "id" : "configuration",
+ "rights" : [
+ "configuration_all",
+ "directive_all",
+ "parameter_all",
+ "rule_all",
+ "technique_all",
+ "userAccount_all"
+ ]
+ },
+ {
+ "id" : "deployer",
+ "rights" : [
+ "configuration_read",
+ "directive_read",
+ "group_read",
+ "node_read",
+ "parameter_read",
+ "rule_read",
+ "technique_read",
+ "compliance_all",
+ "deployer_all",
+ "userAccount_all"
+ ]
+ },
+ {
+ "id" : "inventory",
+ "rights" : [
+ "node_read",
+ "userAccount_all"
+ ]
+ },
+ {
+ "id" : "no_rights",
+ "rights" : [
+ "no_rights"
+ ]
+ },
+ {
+ "id" : "read_only",
+ "rights" : [
+ "administration_read",
+ "compliance_read",
+ "configuration_read",
+ "deployer_read",
+ "deployment_read",
+ "directive_read",
+ "group_read",
+ "node_read",
+ "parameter_read",
+ "rule_read",
+ "technique_read",
+ "validator_read",
+ "userAccount_all"
+ ]
+ },
+ {
+ "id" : "rule_only",
+ "rights" : [
+ "configuration_read",
+ "rule_read",
+ "userAccount_all"
+ ]
+ },
+ {
+ "id" : "user",
+ "rights" : [
+ "configuration_all",
+ "directive_all",
+ "group_all",
+ "node_all",
+ "parameter_all",
+ "rule_all",
+ "technique_all",
+ "userAccount_all"
+ ]
+ },
+ {
+ "id" : "validator",
+ "rights" : [
+ "configuration_read",
+ "directive_read",
+ "group_read",
+ "node_read",
+ "parameter_read",
+ "rule_read",
+ "technique_read",
+ "compliance_all",
+ "userAccount_all",
+ "validator_all"
+ ]
+ },
+ {
+ "id" : "workflow",
+ "rights" : [
+ "configuration_read",
+ "directive_read",
+ "group_read",
+ "node_read",
+ "parameter_read",
+ "rule_read",
+ "technique_read",
+ "compliance_all",
+ "deployer_all",
+ "userAccount_all",
+ "validator_all"
+ ]
+ }
+ ]
+ }
+---
+description: Reload (read again rudder-users.xml and process result) information about registered users in Rudder
+method: POST
+url: /api/latest/usermanagement/users/reload
+response:
+ code: 200
+ content: >-
+ {
+ "action" : "reloadUsersConf",
+ "result" : "success",
+ "data" : {
+ "reload" : {
+ "status" : "Done"
+ }
+ }
+ }
+---
+description: Add a user with his information and privileges
+method: POST
+url: /api/latest/usermanagement
+headers:
+ - "Content-Type: application/json"
+body: >-
+ {
+ "username" : "test_user",
+ "password" : "password",
+ "permissions" : ["any_rights"],
+ "isPreHashed" : true
+ }
+response:
+ code: 200
+ content: >-
+ {
+ "action" : "addUser",
+ "result" : "success",
+ "data" : {
+ "addedUser" : {
+ "username" : "test_user",
+ "password" : "password",
+ "permissions" : ["any_rights"]
+ }
+ }
+ }
+---
+description: Add a user which already exists
+method: POST
+url: /api/latest/usermanagement
+headers:
+ - "Content-Type: application/json"
+body: >-
+ {
+ "username" : "user1",
+ "password" : "password",
+ "permissions" : ["any_rights"],
+ "isPreHashed" : true
+ }
+response:
+ code: 500
+ content: >-
+ {
+ "action" : "addUser",
+ "result" : "error",
+ "errorDetails" : "Could not add user; cause was: Inconsistency: User 'user1' already exists"
+ }
+---
+description: Delete a user from the system
+method: DELETE
+url: /api/latest/usermanagement/test_user
+response:
+ code: 200
+ content: >-
+ {
+ "action" : "deleteUser",
+ "result" : "success",
+ "data" : {
+ "deletedUser" : {
+ "username" : "test_user"
+ }
+ }
+ }
+---
+description: Delete a user which does not exist
+method: DELETE
+url: /api/latest/usermanagement/hello%20world
+response:
+ code: 200
+ content: >-
+ {
+ "action" : "deleteUser",
+ "result" : "success",
+ "data" : {
+ "deletedUser" : {
+ "username" : "hello+world"
+ }
+ }
+ }
+---
+description: Update user's infos
+method: POST
+url: /api/latest/usermanagement/update/user1
+headers:
+ - "Content-Type: application/json"
+body: >-
+ {
+ "username" : "user1",
+ "password" : "password1234",
+ "permissions" : ["any_rights"],
+ "isPreHashed" : true
+ }
+response:
+ code: 200
+ content: >-
+ {
+ "action" : "updateUserInfos",
+ "result" : "success",
+ "data" : {
+ "updatedUser" : {
+ "username" : "user1",
+ "password" : "password1234",
+ "permissions" : [
+ "any_rights"
+ ]
+ }
+ }
+ }
+---
+description: Update user's infos with a non-existing username
+method: POST
+url: /api/latest/usermanagement/update/hello%20world
+headers:
+ - "Content-Type: application/json"
+body: >-
+ {
+ "username" : "hello world",
+ "password" : "test",
+ "permissions" : [],
+ "isPreHashed" : true
+ }
+response:
+ code: 500
+ content: >-
+ {
+ "action" : "updateUserInfos",
+ "result" : "error",
+ "errorDetails" : "Could not update user 'hello+world'; cause was: Inconsistency: 'hello+world' does not exists"
+ }
+---
+description: Get the coverage of roles over rights
+method: POST
+url: /api/latest/usermanagement/coverage/user1
+headers:
+ - "Content-Type: application/json"
+body: >-
+ {
+ "permissions" : ["administration_only"],
+ "authz" : ["administration"]
+ }
+response:
+ code: 200
+ content: >-
+ {
+ "action" : "roleCoverage",
+ "result" : "success",
+ "data" : {
+ "coverage" : {
+ "permissions" : [],
+ "custom" : [
+ "userAccount_edit",
+ "userAccount_read",
+ "userAccount_write"
+ ]
+ }
+ }
+ }
+
diff --git a/user-management/src/test/scala/com/normation/plugins/usermanagement/MockServices.scala b/user-management/src/test/scala/com/normation/plugins/usermanagement/MockServices.scala
new file mode 100644
index 000000000..b3be450be
--- /dev/null
+++ b/user-management/src/test/scala/com/normation/plugins/usermanagement/MockServices.scala
@@ -0,0 +1,102 @@
+package com.normation.plugins.usermanagement
+
+import better.files.File
+import bootstrap.liftweb.FileUserDetailListProvider
+import bootstrap.liftweb.UserFile
+import com.normation.errors.IOResult
+import com.normation.rudder.rest.AuthorizationApiMapping
+import com.normation.rudder.rest.RoleApiMapping
+import com.normation.rudder.users.EventTrace
+import com.normation.rudder.users.SessionId
+import com.normation.rudder.users.UserAuthorisationLevel
+import com.normation.rudder.users.UserInfo
+import com.normation.rudder.users.UserRepository
+import com.normation.rudder.users.UserSession
+import java.nio.charset.StandardCharsets
+import org.apache.commons.io.IOUtils
+import org.joda.time.DateTime
+import zio.json.ast.Json
+import zio.syntax._
+
+class MockServices(userInfos: List[UserInfo], usersFile: File) {
+
+ object userRepo extends UserRepository {
+
+ override def logStartSession(
+ userId: String,
+ permissions: List[String],
+ tenants: String,
+ sessionId: SessionId,
+ authenticatorName: String,
+ date: DateTime
+ ): IOResult[Unit] = ???
+
+ override def logCloseSession(userId: String, date: DateTime, cause: String): IOResult[Unit] = ???
+
+ override def closeAllOpenSession(endDate: DateTime, endCause: String): IOResult[Unit] = ???
+
+ override def getLastPreviousLogin(userId: String): IOResult[Option[UserSession]] = ???
+
+ override def deleteOldSessions(olderThan: DateTime): IOResult[Unit] = ???
+
+ override def setExistingUsers(origin: String, users: List[String], trace: EventTrace): IOResult[Unit] = ???
+
+ override def disable(
+ userId: List[String],
+ notLoggedSince: Option[DateTime],
+ excludeFromOrigin: List[String],
+ trace: EventTrace
+ ): IOResult[List[String]] = ???
+
+ override def delete(
+ userId: List[String],
+ notLoggedSince: Option[DateTime],
+ excludeFromOrigin: List[String],
+ trace: EventTrace
+ ): IOResult[List[String]] = {
+ userId.succeed
+ }
+
+ override def purge(
+ userId: List[String],
+ deletedSince: Option[DateTime],
+ excludeFromOrigin: List[String],
+ trace: EventTrace
+ ): IOResult[List[String]] = ???
+
+ override def setActive(userId: List[String], trace: EventTrace): IOResult[Unit] = ???
+
+ override def updateInfo(
+ id: String,
+ name: Option[Option[String]],
+ email: Option[Option[String]],
+ otherInfo: Option[Json.Obj]
+ ): IOResult[Unit] = ???
+
+ override def getAll(): IOResult[List[UserInfo]] = userInfos.succeed
+
+ override def get(userId: String): IOResult[Option[UserInfo]] = {
+ userInfos.find(_.id == userId).succeed
+ }
+
+ }
+
+ val usersInputStream = () => IOUtils.toInputStream(usersFile.contentAsString, StandardCharsets.UTF_8)
+
+ val userService = {
+ val usersFile = UserFile("test-users.xml", usersInputStream)
+
+ val authLevel = new UserAuthorisationLevel {
+ override def userAuthEnabled: Boolean = true
+ override def name: String = "Test user auth level"
+ }
+ val roleApiMapping = new RoleApiMapping(AuthorizationApiMapping.Core)
+
+ val res = new FileUserDetailListProvider(roleApiMapping, authLevel, usersFile)
+ res.reload()
+ res
+ }
+
+ val userManagementService =
+ new UserManagementService(userRepo, UserFile(usersFile.pathAsString, usersInputStream).succeed)
+}
diff --git a/user-management/src/test/scala/com/normation/plugins/usermanagement/api/UserManagementApiTest.scala b/user-management/src/test/scala/com/normation/plugins/usermanagement/api/UserManagementApiTest.scala
new file mode 100644
index 000000000..72319b3a1
--- /dev/null
+++ b/user-management/src/test/scala/com/normation/plugins/usermanagement/api/UserManagementApiTest.scala
@@ -0,0 +1,98 @@
+package com.normation.plugins.usermanagement.api
+
+import better.files.File
+import better.files.Resource
+import bootstrap.liftweb.AuthBackendProvidersManager
+import com.normation.plugins.usermanagement.MockServices
+import com.normation.rudder.api.ApiVersion
+import com.normation.rudder.rest.TraitTestApiFromYamlFiles
+import com.normation.rudder.users.UserInfo
+import com.normation.rudder.users.UserStatus
+import java.nio.file.Files
+import net.liftweb.common.Loggable
+import org.joda.time.DateTime
+import org.junit.runner.RunWith
+import org.specs2.mutable.Specification
+import org.specs2.runner.JUnitRunner
+import org.specs2.specification.AfterAll
+import zio.json.ast.Json
+
+@RunWith(classOf[JUnitRunner])
+class UserManagementApiTest extends Specification with TraitTestApiFromYamlFiles with Loggable with AfterAll {
+ sequential
+
+ val tmpDir: File = File(Files.createTempDirectory("rudder-test-"))
+ override def yamlSourceDirectory = "usermanagement_api"
+ override def yamlDestTmpDirectory = tmpDir / "templates"
+
+ val testUserFile = Resource
+ .url("test-users.xml")
+ .map(File(_))
+ .map(_.copyTo(tmpDir / "test-users.xml"))
+ .getOrElse(throw new Exception("Cannot find test-users.xml in test resources"))
+
+ val mockServices = new MockServices(
+ List(
+ UserInfo( // user3 not in the file will get empty permissions and authz
+ "user3",
+ DateTime.parse("2024-02-01T01:01:01Z"),
+ UserStatus.Active,
+ "manager",
+ None,
+ None,
+ None,
+ List.empty,
+ Json.Obj()
+ ),
+ UserInfo(
+ "user2",
+ DateTime.parse("2024-02-01T01:01:01Z"),
+ UserStatus.Active,
+ "manager",
+ None,
+ None,
+ None,
+ List.empty,
+ Json.Obj()
+ ),
+ UserInfo(
+ "user1",
+ DateTime.parse("2024-02-01T01:01:01Z"),
+ UserStatus.Active,
+ "manager",
+ None,
+ None,
+ None,
+ List.empty,
+ Json.Obj()
+ )
+ ),
+ testUserFile
+ )
+ val modules = List(
+ new UserManagementApiImpl(
+ mockServices.userRepo,
+ mockServices.userService,
+ new AuthBackendProvidersManager(),
+ mockServices.userManagementService
+ )
+ )
+
+ val apiVersions = ApiVersion(13, true) :: ApiVersion(14, false) :: Nil
+ val (rudderApi, liftRules) = TraitTestApiFromYamlFiles.buildLiftRules(modules, apiVersions, None)
+
+ override def transformations: Map[String, String => String] = Map()
+
+ // we are testing error cases, so we don't want to output error log for them
+ org.slf4j.LoggerFactory
+ .getLogger("com.normation.rudder.rest.RestUtils")
+ .asInstanceOf[ch.qos.logback.classic.Logger]
+ .setLevel(ch.qos.logback.classic.Level.OFF)
+
+ override def afterAll(): Unit = {
+ tmpDir.delete()
+ }
+
+ doTest(semanticJson = true)
+
+}