controllers.adminapi.UsersController.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otoroshi_2.12 Show documentation
Show all versions of otoroshi_2.12 Show documentation
Lightweight api management on top of a modern http reverse proxy
The newest version!
package otoroshi.controllers.adminapi
import otoroshi.actions.{ApiAction, ApiActionContext}
import akka.http.scaladsl.util.FastFuture
import otoroshi.env.Env
import otoroshi.events._
import otoroshi.models.{BackOfficeUser, PrivateAppsUser}
import org.joda.time.DateTime
import org.mindrot.jbcrypt.BCrypt
import otoroshi.models.RightsChecker.{SuperAdminOnly, TenantAdminOnly}
import otoroshi.models.{UserRights, _}
import otoroshi.utils.controllers.{AdminApiHelper, JsonApiError, SendAuditAndAlert}
import otoroshi.utils.syntax.implicits._
import play.api.libs.json.{JsValue, Json}
import play.api.mvc._
import otoroshi.security.IdGenerator
import scala.concurrent.Future
class UsersController(ApiAction: ApiAction, cc: ControllerComponents)(implicit env: Env)
extends AbstractController(cc)
with AdminApiHelper {
implicit lazy val ec = env.otoroshiExecutionContext
private val fakeBackOfficeUser = BackOfficeUser(
randomId = IdGenerator.token,
name = "fake user",
email = "[email protected]",
profile = Json.obj(),
simpleLogin = false,
authConfigId = "none",
tags = Seq.empty,
metadata = Map.empty,
rights = UserRights.superAdmin,
location = EntityLocation(),
adminEntityValidators = Map.empty
)
def sessions() =
ApiAction.async { ctx =>
ctx.checkRights(TenantAdminOnly) {
val options = SendAuditAndAlert("ACCESS_ADMIN_SESSIONS", s"User accessed admin session", None, Json.obj(), ctx)
fetchWithPaginationAndFilteringAsResult(ctx, "filter.".some, (e: BackOfficeUser) => e.toJson, options) {
env.datastores.backOfficeUserDataStore.findAll().map(_.filter(ctx.canUserRead)).fright[JsonApiError]
}
}
}
def discardSession(id: String) =
ApiAction.async { ctx =>
ctx.checkRights(TenantAdminOnly) {
env.datastores.globalConfigDataStore.singleton().filter(!_.apiReadOnly).flatMap { _ =>
env.datastores.backOfficeUserDataStore.findById(id).flatMap {
case None => NotFound(Json.obj("error" -> "Session not found !")).future
case Some(session) if !ctx.canUserWrite(session) => ctx.fforbidden
case Some(_) => {
env.datastores.backOfficeUserDataStore.discardSession(id) map { _ =>
val event = AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
None,
"DISCARD_SESSION",
s"Admin discarded an Admin session",
ctx.from,
ctx.ua,
Json.obj("sessionId" -> id)
)
Audit.send(event)
Alerts
.send(
SessionDiscardedAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
fakeBackOfficeUser,
event,
ctx.from,
ctx.ua
)
)
Ok(Json.obj("done" -> true))
}
}
}
} recover { case _ =>
Ok(Json.obj("done" -> false))
}
}
}
def discardAllSessions() =
ApiAction.async { ctx =>
ctx.checkRights(SuperAdminOnly) {
env.datastores.globalConfigDataStore.singleton().filter(!_.apiReadOnly).flatMap { _ =>
env.datastores.backOfficeUserDataStore.discardAllSessions() map { _ =>
val event = AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
None,
"DISCARD_SESSIONS",
s"Admin discarded Admin sessions",
ctx.from,
ctx.ua,
Json.obj()
)
Audit.send(event)
Alerts
.send(
SessionsDiscardedAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
fakeBackOfficeUser,
event,
ctx.from,
ctx.ua
)
)
Ok(Json.obj("done" -> true))
}
} recover { case _ =>
Ok(Json.obj("done" -> false))
}
}
}
def privateAppsSessions() =
ApiAction.async { ctx =>
ctx.checkRights(TenantAdminOnly) {
val options = SendAuditAndAlert(
"ACCESS_PRIVATE_APPS_SESSIONS",
s"User accessed private apps session",
None,
Json.obj(),
ctx
)
fetchWithPaginationAndFilteringAsResult(ctx, "filter.".some, (e: PrivateAppsUser) => e.toJson, options) {
env.datastores.privateAppsUserDataStore.findAll().map(_.filter(ctx.canUserRead)).fright[JsonApiError]
}
}
}
def discardPrivateAppsSession(id: String) =
ApiAction.async { ctx =>
ctx.checkRights(TenantAdminOnly) {
env.datastores.globalConfigDataStore.singleton().filter(!_.apiReadOnly).flatMap { _ =>
env.datastores.privateAppsUserDataStore.findById(id).flatMap {
case None => Results.NotFound(Json.obj("error" -> "Session not found")).future
case Some(session) if !ctx.canUserWrite(session) => ctx.fforbidden
case Some(_) => {
env.datastores.privateAppsUserDataStore.delete(id) map { _ =>
val event = AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
None,
"DISCARD_PRIVATE_APPS_SESSION",
s"Admin discarded a private app session",
ctx.from,
ctx.ua,
Json.obj("sessionId" -> id)
)
Audit.send(event)
Alerts
.send(
SessionDiscardedAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
fakeBackOfficeUser,
event,
ctx.from,
ctx.ua
)
)
Ok(Json.obj("done" -> true))
}
}
}
} recover { case _ =>
Ok(Json.obj("done" -> false))
}
}
}
def discardAllPrivateAppsSessions() =
ApiAction.async { ctx =>
ctx.checkRights(SuperAdminOnly) {
env.datastores.globalConfigDataStore.singleton().filter(!_.apiReadOnly).flatMap { _ =>
env.datastores.privateAppsUserDataStore.deleteAll() map { _ =>
val event = AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
None,
"DISCARD_PRIVATE_APPS_SESSIONS",
s"Admin discarded private apps sessions",
ctx.from,
ctx.ua,
Json.obj()
)
Audit.send(event)
Alerts
.send(
SessionsDiscardedAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
fakeBackOfficeUser,
event,
ctx.from,
ctx.ua
)
)
Ok(Json.obj("done" -> true))
}
} recover { case _ =>
Ok(Json.obj("done" -> false))
}
}
}
def checkNewUserRights(ctx: ApiActionContext[_], rights: UserRights)(f: => Future[Result]): Future[Result] = {
if (!ctx.userIsSuperAdmin && rights.superAdmin) {
FastFuture.successful(Forbidden(Json.obj("error" -> "you can't set superadmin rights to an admin")))
} else {
val pass: Boolean = ctx.backOfficeUser match {
case Left(_) =>
true
case Right(None) =>
true
case Right(Some(user)) => {
// println(s"right some ${user.json}")
val tenantAccesses = user.rights.rights.map(_.tenant)
val newTenantAccesses = rights.rights.map(_.tenant)
val hasAccessToTenant = tenantAccesses.map(_.value).contains("*")
val badTenantAccess = newTenantAccesses.exists(v => !tenantAccesses.contains(v))
val badTeamAccess = user.rights.rights.exists { right =>
user.rights.rights.find(_.tenant.value == right.tenant.value) match {
case None => false
case Some(r) =>
val teams = r.teams
val newTeams = right.teams
if (r.teams.map(_.value).contains("*")) {
false
} else {
newTeams.exists(v => !teams.contains(v))
}
}
}
if (hasAccessToTenant) {
!badTeamAccess
} else {
!(badTenantAccess || badTeamAccess)
}
}
}
if (pass) {
f
} else {
FastFuture.successful(Forbidden(Json.obj("error" -> "you can't set superadmin rights to an admin")))
}
}
}
def registerSimpleAdmin =
ApiAction.async(parse.json) { ctx =>
ctx.checkRights(TenantAdminOnly) {
val usernameOpt = (ctx.request.body \ "username").asOpt[String].filterNot(_.isBlank)
val passwordOpt = (ctx.request.body \ "password").asOpt[String].filterNot(_.isBlank)
val labelOpt = (ctx.request.body \ "label").asOpt[String]
val rights = if (ctx.userIsSuperAdmin) {
UserRights(
Seq(UserRight(TenantAccess("*"), Seq(TeamAccess("*"))))
)
} else {
UserRights(
Seq(UserRight(TenantAccess(ctx.currentTenant.value), Seq(TeamAccess("*"))))
)
}
checkNewUserRights(ctx, rights) {
(usernameOpt, passwordOpt, labelOpt) match {
case (Some(username), Some(password), Some(label)) => {
val saltedPassword = BCrypt.hashpw(password, BCrypt.gensalt())
val user = SimpleOtoroshiAdmin(
username = username,
password = saltedPassword,
label = label,
createdAt = DateTime.now(),
typ = OtoroshiAdminType.SimpleAdmin,
metadata = (ctx.request.body \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty),
rights = rights,
adminEntityValidators = Map.empty,
location =
EntityLocation(ctx.currentTenant, Seq(TeamId.all)) // EntityLocation.readFromKey(ctx.request.body)
)
env.datastores.simpleAdminDataStore.findByUsername(username).flatMap {
case Some(_) => FastFuture.successful(BadRequest(Json.obj("error" -> "user already exists")))
case None => {
ctx.validateEntity(user.json, "simple-admin-user") match {
case Left(errs) => FastFuture.successful(BadRequest(Json.obj("error" -> errs)))
case Right(_) =>
env.datastores.simpleAdminDataStore
.registerUser(user)
.map { _ =>
Ok(Json.obj("username" -> username))
}
}
}
}
}
case _ => FastFuture.successful(BadRequest(Json.obj("error" -> "no username or token provided")))
}
}
}
}
def simpleAdmins =
ApiAction.async { ctx =>
ctx.checkRights(TenantAdminOnly) {
val options = SendAuditAndAlert("ACCESS_SIMPLE_ADMINS", s"User accessed simple admins", None, Json.obj(), ctx)
fetchWithPaginationAndFilteringAsResult(ctx, "filter.".some, (e: JsValue) => e, options) {
env.datastores.simpleAdminDataStore.findAll().map(_.filter(ctx.canUserRead).map(_.json)).fright[JsonApiError]
}
}
}
def deleteAdmin(username: String) =
ApiAction.async { ctx =>
ctx.checkRights(TenantAdminOnly) {
env.datastores.simpleAdminDataStore.findByUsername(username).flatMap {
case None => NotFound(Json.obj("error" -> "User not found !")).future
case Some(user) if !ctx.canUserWrite(user) => ctx.fforbidden
case Some(_) => {
env.datastores.simpleAdminDataStore.deleteUser(username).map { d =>
val event = AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
None,
"DELETE_ADMIN",
s"Admin deleted an Admin",
ctx.from,
ctx.ua,
Json.obj("username" -> username)
)
Audit.send(event)
Alerts.send(
U2FAdminDeletedAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
fakeBackOfficeUser,
event,
ctx.from,
ctx.ua
)
)
Ok(Json.obj("done" -> true))
}
}
}
}
}
def findAdmin(username: String) =
ApiAction.async { ctx =>
ctx.checkRights(TenantAdminOnly) {
env.datastores.simpleAdminDataStore.findByUsername(username).flatMap {
case None => NotFound(Json.obj("error" -> "user not found")).future
case Some(user) if !ctx.canUserRead(user) => ctx.fforbidden
case Some(user) => Ok(user.json).future
}
}
}
def updateAdmin(username: String) =
ApiAction.async(parse.json) { ctx =>
ctx.checkRights(TenantAdminOnly) {
env.datastores.simpleAdminDataStore.findByUsername(username).flatMap {
case None => NotFound(Json.obj("error" -> "user not found")).future
case Some(user) if !ctx.canUserWrite(user) => ctx.fforbidden
case Some(_) => {
val body = ctx.request.body
ctx.validateEntity(body, "simple-admin-user") match {
case Left(errs) => FastFuture.successful(BadRequest(Json.obj("error" -> errs)))
case Right(_) =>
val _newUser = SimpleOtoroshiAdmin.fmt.reads(body).get
checkNewUserRights(ctx, _newUser.rights) {
val newUser = _newUser.copy(username = username)
env.datastores.simpleAdminDataStore.registerUser(newUser).map { _ =>
Ok(newUser.json)
}
}
}
}
}
}
}
def findWebAuthnAdmin(username: String) =
ApiAction.async { ctx =>
ctx.checkRights(TenantAdminOnly) {
env.datastores.webAuthnAdminDataStore.findByUsername(username).flatMap {
case None => NotFound(Json.obj("error" -> "user not found")).future
case Some(user) if !ctx.canUserWrite(user) => ctx.fforbidden
case Some(user) => Ok(user.json).future
}
}
}
def updateWebAuthnAdmin(username: String) =
ApiAction.async(parse.json) { ctx =>
ctx.checkRights(TenantAdminOnly) {
env.datastores.webAuthnAdminDataStore.findByUsername(username).flatMap {
case None => NotFound(Json.obj("error" -> "user not found")).future
case Some(user) if !ctx.canUserWrite(user) => ctx.fforbidden
case Some(_) => {
val body = ctx.request.body
ctx.validateEntity(body, "simple-admin-user") match {
case Left(errs) => FastFuture.successful(BadRequest(Json.obj("error" -> errs)))
case Right(_) =>
val _newUser = WebAuthnOtoroshiAdmin.fmt.reads(body).get
checkNewUserRights(ctx, _newUser.rights) {
val newUser = _newUser.copy(username = username)
env.datastores.webAuthnAdminDataStore.registerUser(newUser).map { _ =>
Ok(newUser.json)
}
}
}
}
}
}
}
def webAuthnAdmins() =
ApiAction.async { ctx =>
ctx.checkRights(TenantAdminOnly) {
val options =
SendAuditAndAlert("ACCESS_WEBAUTHN_ADMINS", s"User accessed webauthn admins", None, Json.obj(), ctx)
fetchWithPaginationAndFilteringAsResult(ctx, "filter.".some, (e: JsValue) => e, options) {
env.datastores.webAuthnAdminDataStore
.findAll()
.map(_.filter(ctx.canUserRead).map(_.json))
.fright[JsonApiError]
}
}
}
def registerWebAuthnAdmin() =
ApiAction.async(parse.json) { ctx =>
ctx.checkRights(TenantAdminOnly) {
val usernameOpt = (ctx.request.body \ "username").asOpt[String]
val passwordOpt = (ctx.request.body \ "password").asOpt[String]
val labelOpt = (ctx.request.body \ "label").asOpt[String]
val credentialOpt = (ctx.request.body \ "credential").asOpt[JsValue]
val handleOpt = (ctx.request.body \ "handle").asOpt[String]
val rights = UserRights.readFromObject(ctx.request.body)
checkNewUserRights(ctx, rights) {
(usernameOpt, passwordOpt, labelOpt, handleOpt) match {
case (Some(username), Some(password), Some(label), Some(handle)) => {
val saltedPassword = BCrypt.hashpw(password, BCrypt.gensalt())
val user = WebAuthnOtoroshiAdmin(
username = username,
password = saltedPassword,
label = label,
handle = handle,
credentials = credentialOpt.map(v => Map((v \ "keyId" \ "id").as[String] -> v)).getOrElse(Map.empty),
createdAt = DateTime.now(),
typ = OtoroshiAdminType.WebAuthnAdmin,
metadata = (ctx.request.body \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty),
rights = rights,
adminEntityValidators = Map.empty,
location =
EntityLocation(ctx.currentTenant, Seq(TeamId.all)) // EntityLocation.readFromKey(ctx.request.body)
)
env.datastores.webAuthnAdminDataStore.findByUsername(username).flatMap {
case Some(_) => FastFuture.successful(BadRequest(Json.obj("error" -> "user already exists")))
case None => {
ctx.validateEntity(user.json, "simple-admin-user") match {
case Left(errs) => FastFuture.successful(BadRequest(Json.obj("error" -> errs)))
case Right(_) =>
env.datastores.webAuthnAdminDataStore
.registerUser(user)
.map { _ =>
Ok(Json.obj("username" -> username))
}
}
}
}
}
case _ => FastFuture.successful(BadRequest(Json.obj("error" -> "no username or token provided")))
}
}
}
}
def webAuthnDeleteAdmin(username: String, id: String) =
ApiAction.async { ctx =>
ctx.checkRights(TenantAdminOnly) {
env.datastores.webAuthnAdminDataStore.findByUsername(username).flatMap {
case None => NotFound(Json.obj("error" -> "User not found !")).future
case Some(user) if !ctx.canUserWrite(user) => ctx.fforbidden
case Some(_) => {
env.datastores.webAuthnAdminDataStore.deleteUser(username).map { d =>
val event = AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
None,
"DELETE_WEBAUTHN_ADMIN",
s"Admin deleted a WebAuthn Admin",
ctx.from,
ctx.ua,
Json.obj("username" -> username, "id" -> id)
)
Audit.send(event)
Alerts
.send(
WebAuthnAdminDeletedAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
fakeBackOfficeUser,
event,
ctx.from,
ctx.ua
)
)
Ok(Json.obj("done" -> true))
}
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy