Skip to content

Commit

Permalink
WIP api - transfer is broken
Browse files Browse the repository at this point in the history
  • Loading branch information
quentinovega committed Sep 4, 2024
1 parent b90ed07 commit 15ea00a
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 54 deletions.
113 changes: 80 additions & 33 deletions daikoku/app/controllers/ApiController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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 =>
Expand Down
4 changes: 3 additions & 1 deletion daikoku/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
38 changes: 18 additions & 20 deletions daikoku/test/daikoku/ApiControllerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1793,16 +1793,13 @@ class ApiControllerSpec()
autoRotation = Some(false),
aggregationApiKeysSecurity = Some(true),
)


val api = defaultApi.api.copy(
id = ApiId("test-api-id"),
name = "test API",
team = teamOwnerId,
possibleUsagePlans = Seq(usagePlan.id),
defaultUsagePlan = usagePlan.id.some
)

val subscription = ApiSubscription(
id = ApiSubscriptionId("test_sub"),
tenant = tenant.id,
Expand Down Expand Up @@ -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
Expand All @@ -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"),
Expand Down

0 comments on commit 15ea00a

Please sign in to comment.