From 15ea00a3e429cc13bdf5b694700fca8b46392a05 Mon Sep 17 00:00:00 2001 From: Quentin AUBERT Date: Wed, 4 Sep 2024 12:29:18 +0200 Subject: [PATCH] WIP api - transfer is broken --- daikoku/app/controllers/ApiController.scala | 113 +++++++++++++------ daikoku/conf/routes | 4 +- daikoku/test/daikoku/ApiControllerSpec.scala | 38 +++---- 3 files changed, 101 insertions(+), 54 deletions(-) diff --git a/daikoku/app/controllers/ApiController.scala b/daikoku/app/controllers/ApiController.scala index 42ad1b53a..62b69c526 100644 --- a/daikoku/app/controllers/ApiController.scala +++ b/daikoku/app/controllers/ApiController.scala @@ -10,25 +10,17 @@ import cats.data.EitherT import cats.implicits.{catsSyntaxOptionId, toTraverseOps} import controllers.AppError import controllers.AppError._ -import fr.maif.otoroshi.daikoku.actions.{ - DaikokuAction, - DaikokuActionContext, - DaikokuActionMaybeWithGuest, - DaikokuActionMaybeWithoutUser -} +import fr.maif.otoroshi.daikoku.actions.{DaikokuAction, DaikokuActionContext, DaikokuActionMaybeWithGuest, DaikokuActionMaybeWithoutUser} import fr.maif.otoroshi.daikoku.audit.AuditTrailEvent import fr.maif.otoroshi.daikoku.audit.config.ElasticAnalyticsConfig import fr.maif.otoroshi.daikoku.ctrls.authorizations.async._ -import fr.maif.otoroshi.daikoku.domain.NotificationAction.{ - ApiAccess, - ApiSubscriptionDemand -} +import fr.maif.otoroshi.daikoku.domain.NotificationAction.{ApiAccess, ApiSubscriptionDemand} import fr.maif.otoroshi.daikoku.domain.UsagePlanVisibility.Private import fr.maif.otoroshi.daikoku.domain._ import fr.maif.otoroshi.daikoku.domain.json._ import fr.maif.otoroshi.daikoku.env.Env import fr.maif.otoroshi.daikoku.logger.AppLogger -import fr.maif.otoroshi.daikoku.utils.Cypher.decrypt +import fr.maif.otoroshi.daikoku.utils.Cypher.{decrypt, encrypt} import fr.maif.otoroshi.daikoku.utils.RequestImplicits.EnhancedRequestHeader import fr.maif.otoroshi.daikoku.utils.StringImplicits.BetterString import fr.maif.otoroshi.daikoku.utils._ @@ -2075,40 +2067,95 @@ class ApiController( } } - def transferSubscription(teamId: String, subscriptionId: String) = + def checkTransferLink() = + DaikokuActionMaybeWithGuest.async { ctx => + PublicUserAccess( + AuditTrailEvent("@{user.name} has check a transfer link for @{subscription.id}") + )(ctx) { + + (for { + cypheredInfos <- EitherT.fromOption[Future][AppError, String](ctx.request.getQueryString("token"), AppError.EntityNotFound("token")) + infosAsString <- EitherT.pure[Future, AppError](decrypt(env.config.cypherSecret, cypheredInfos, ctx.tenant)) + infos <- EitherT.pure[Future, AppError](Json.parse(infosAsString)) + _ <- EitherT.cond[Future][AppError, Unit]((infos \ "createdOn").as(DateTimeFormat).plusDays(1).isBefore(DateTime.now()), (), AppError.ForbiddenAction) //give reason + subscription <- EitherT.fromOptionF[Future, AppError, ApiSubscription](env.dataStore.apiSubscriptionRepo.forTenant(ctx.tenant).findByIdNotDeleted((infos \ "subscription").as[String]), AppError.SubscriptionNotFound) + usagePlan <- EitherT.fromOptionF[Future, AppError, UsagePlan](env.dataStore.usagePlanRepo.forTenant(ctx.tenant).findByIdNotDeleted(subscription.plan), AppError.PlanNotFound) + api <- EitherT.fromOptionF[Future, AppError, Api](env.dataStore.apiRepo.forTenant(ctx.tenant).findByIdNotDeleted(subscription.api), AppError.ApiNotFound) + } yield { + ctx.setCtxValue("subscription.id", subscription.id.value) + Ok(Json.obj( + "subscription" -> subscription.id.asJson, + "api" -> api.id.asJson, + "plan" -> usagePlan.id.asJson + )) + }) + .leftMap(_.render()) + .merge + } + } + + def getTransferLink(teamId: String, subscriptionId: String) = DaikokuAction.async(parse.json) { ctx => TeamAdminOnly( - AuditTrailEvent(s"@{user.name} has ask to transfer subscription @{subscriptionId} to team @{teamId}"))(teamId, ctx) { team => { - val newTeamId = (ctx.request.body \ "team").as[String] + AuditTrailEvent(s"@{user.name} has generated a link to transfer subscription @{subscription.id}"))(teamId, ctx) { team => { + ctx.setCtxValue("subscription.id", subscriptionId) (for { - newTeam <- EitherT.fromOptionF[Future, AppError, Team](env.dataStore.teamRepo.forTenant(ctx.tenant).findByIdOrHrIdNotDeleted(newTeamId), - AppError.TeamNotFound) subscription <- EitherT.fromOptionF[Future, AppError, ApiSubscription](env.dataStore.apiSubscriptionRepo.forTenant(ctx.tenant).findByIdOrHrIdNotDeleted(subscriptionId), AppError.SubscriptionNotFound) _ <- EitherT.cond[Future][AppError, Unit](subscription.parent.isEmpty, (), AppError.EntityConflict("Subscription is part of aggregation")) - _ <- EitherT.liftF[Future, AppError, Boolean](env.dataStore.notificationRepo.forTenant(ctx.tenant).save( - Notification( - id = NotificationId( - IdGenerator.token(32) - ), - tenant = ctx.tenant.id, - sender = ctx.user.asNotificationSender, - action = - NotificationAction.ApiSubscriptionTransfer( - subscription = subscription.id - ), - notificationType = - NotificationType.AcceptOrReject, - team = newTeam.id.some - ) - )) - } yield Ok(Json.obj("done" -> true))) + + json = Json.obj( + "tenant" -> ctx.tenant.id.asJson, + "subscription" -> subscription.id.asJson, + "createdOn" -> DateTime.now().getMillis, + "by" -> ctx.user.id.asJson + ) + cipheredToken = encrypt(env.config.cypherSecret, Json.stringify(json), ctx.tenant) + link <- EitherT.pure[Future, AppError](s"${env.getDaikokuUrl(ctx.tenant, "/api/me/subscription/_retrieve")}?token=$cipheredToken") + } yield Ok(Json.obj("link" -> link))) .leftMap(_.render()) .merge } } } + def transferSubscription(teamId: String, subscriptionId: String) = + DaikokuAction.async(parse.json) { ctx => + TeamAdminOnly( + AuditTrailEvent(s"@{user.name} has ask to transfer subscription @{subscriptionId} to team @{teamId}"))(teamId, ctx) { team => { + val newTeamId = (ctx.request.body \ "team").as[String] + + //FIXME: get token & + //FIXME: check if api, plan are accessible (child also) + //FIXME: check if team has no subscription to plan or childs plan (if security is enable) +// (for { +// newTeam <- EitherT.fromOptionF[Future, AppError, Team](env.dataStore.teamRepo.forTenant(ctx.tenant).findByIdOrHrIdNotDeleted(newTeamId), +// AppError.TeamNotFound) +// subscription <- EitherT.fromOptionF[Future, AppError, ApiSubscription](env.dataStore.apiSubscriptionRepo.forTenant(ctx.tenant).findByIdOrHrIdNotDeleted(subscriptionId), +// AppError.SubscriptionNotFound) +// _ <- EitherT.cond[Future][AppError, Unit](subscription.parent.isEmpty, (), AppError.EntityConflict("Subscription is part of aggregation")) +// _ <- EitherT.liftF[Future, AppError, Boolean](env.dataStore.notificationRepo.forTenant(ctx.tenant).save( +// Notification( +// id = NotificationId( +// IdGenerator.token(32) +// ), +// tenant = ctx.tenant.id, +// sender = ctx.user.asNotificationSender, +// action = +// NotificationAction.ApiSubscriptionTransfer( +// subscription = subscription.id +// ), +// notificationType = +// NotificationType.AcceptOrReject, +// team = newTeam.id.some +// ) +// )) +// } yield Ok(Json.obj("done" -> true))) +// .leftMap(_.render()) +// .merge + } + } + } def makeUniqueSubscription(teamId: String, subscriptionId: String) = DaikokuAction.async { ctx => diff --git a/daikoku/conf/routes b/daikoku/conf/routes index ea6870919..96bb96fed 100644 --- a/daikoku/conf/routes +++ b/daikoku/conf/routes @@ -52,6 +52,7 @@ POST /api/auth/ldap/_check fr.maif.otoroshi.daikoku GET /api/me/teams fr.maif.otoroshi.daikoku.ctrls.ApiController.myTeams() GET /api/me/teams/own fr.maif.otoroshi.daikoku.ctrls.ApiController.myOwnTeam() GET /api/me/teams/:id fr.maif.otoroshi.daikoku.ctrls.ApiController.oneOfMyTeam(id) +GET /api/me/subscription/_retrieve fr.maif.otoroshi.daikoku.ctrls.ApiController.checkTransferLink() GET /api/me fr.maif.otoroshi.daikoku.ctrls.ApiController.me() DELETE /api/me fr.maif.otoroshi.daikoku.ctrls.UsersController.deleteSelfUser() @@ -114,7 +115,8 @@ GET /api/apis/:apiId/plan/:planId/pages/:pageId fr.maif.otoroshi POST /api/teams/:teamId/subscriptions/:id/name fr.maif.otoroshi.daikoku.ctrls.ApiController.updateApiSubscriptionCustomName(teamId, id) PUT /api/teams/:teamId/subscriptions/:id/_archive fr.maif.otoroshi.daikoku.ctrls.ApiController.toggleApiSubscription(teamId, id, enabled: Option[Boolean] ?= Some(false)) PUT /api/teams/:teamId/subscriptions/:id/_archiveByOwner fr.maif.otoroshi.daikoku.ctrls.ApiController.toggleApiSubscriptionByApiOwner(teamId, id, enabled: Option[Boolean] ?= Some(false)) -PUT /api/teams/:teamId/subscriptions/:id/_transfer fr.maif.otoroshi.daikoku.ctrls.ApiController.transferSubscription(teamId, id) +GET /api/teams/:teamId/subscriptions/:id/_transfer fr.maif.otoroshi.daikoku.ctrls.ApiController.getTransferLink(teamId, id) +PUT /api/teams/:teamId/subscriptions/:id/_retrieve fr.maif.otoroshi.daikoku.ctrls.ApiController.transferSubscription(teamId, id) POST /api/teams/:teamId/subscriptions/:id/_makeUnique fr.maif.otoroshi.daikoku.ctrls.ApiController.makeUniqueSubscription(teamId, id) PUT /api/teams/:teamId/subscriptions/:id fr.maif.otoroshi.daikoku.ctrls.ApiController.updateApiSubscription(teamId, id) DELETE /api/teams/:teamId/subscriptions/:id fr.maif.otoroshi.daikoku.ctrls.ApiController.deleteApiSubscription(teamId, id, action: Option[String] ?= Some("promotion"), child: Option[String] ?= None) diff --git a/daikoku/test/daikoku/ApiControllerSpec.scala b/daikoku/test/daikoku/ApiControllerSpec.scala index e6a40af72..7d037468e 100644 --- a/daikoku/test/daikoku/ApiControllerSpec.scala +++ b/daikoku/test/daikoku/ApiControllerSpec.scala @@ -1793,8 +1793,6 @@ class ApiControllerSpec() autoRotation = Some(false), aggregationApiKeysSecurity = Some(true), ) - - val api = defaultApi.api.copy( id = ApiId("test-api-id"), name = "test API", @@ -1802,7 +1800,6 @@ class ApiControllerSpec() possibleUsagePlans = Seq(usagePlan.id), defaultUsagePlan = usagePlan.id.some ) - val subscription = ApiSubscription( id = ApiSubscriptionId("test_sub"), tenant = tenant.id, @@ -1846,29 +1843,23 @@ class ApiControllerSpec() subscriptions = Seq(subscription) ) - //trasferer la souscription + //get transfer link (no need to give team) val session = loginWithBlocking(userAdmin, tenant) - val resp = httpJsonCallBlocking( + val respLink = httpJsonCallBlocking( path = s"/api/teams/${teamConsumer.id.value}/subscriptions/${subscription.id.value}/_transfer", - method = "PUT", - body = Json.obj("team" -> teamOwner.id.asJson).some )(tenant, session) - resp.status mustBe 200 - - //accepter la notif - val getNotif = httpJsonCallBlocking(s"/api/teams/${teamOwner.id.value}/notifications")(tenant, session) - getNotif.status mustBe 200 - val notifications = (getNotif.json \ "notifications").as(json.SeqNotificationFormat) - notifications.length mustBe 1 - val transferNotif = notifications.head + respLink.status mustBe 200 + val link = (respLink.json \ "link").as[String] + val token = link.split("token=").lastOption.getOrElse("") - - val acceptNotif = httpJsonCallBlocking( - path = s"/api/notifications/${transferNotif.id.value}/accept", + //follow link + val respRetrieve = httpJsonCallBlocking( + path = s"/api/teams/${teamOwner.id.value}/subscriptions/${subscription.id.value}/_retrieve", method = "PUT", - body = Some(Json.obj()) + body = Json.obj("token" -> token).some )(tenant, session) - acceptNotif.status mustBe 200 + respRetrieve.status mustBe 200 + val consumerSubsReq = httpJsonCallBlocking(s"/api/subscriptions/teams/${teamConsumer.id.value}")(tenant, session) consumerSubsReq.status mustBe 200 @@ -1887,8 +1878,15 @@ class ApiControllerSpec() ownerSubs.head.id mustBe subscription.id //eventuellement verifier les metadata de l'apk pour voir la team + //tester les cas non passant : + //1: l equipe a deja une souscription et la securité est desactivé + //2: l equipe a deja une souscription (enfant) et la securité est desactivé + //3: l equipe n'a pas acces a l'api + //4: l equipe n'a pas acces a une api enfant + //5: la souscription est enfant } + //verify /api/me/subscriptions/_transfer ==> get apis name & plan "not transfer child subscriptions to another team but parent subscription" in { val parentPlanProd = FreeWithoutQuotas( id = UsagePlanId("parent.dev"),