Skip to content

Commit

Permalink
Merge pull request #753 from MAIF/feat/#708
Browse files Browse the repository at this point in the history
Feat/#708
  • Loading branch information
quentinovega authored Sep 12, 2024
2 parents c695a9e + 5aca1db commit d567118
Show file tree
Hide file tree
Showing 45 changed files with 2,875 additions and 768 deletions.
86 changes: 85 additions & 1 deletion daikoku/app/controllers/ApiController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ 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,6 +2075,90 @@ class ApiController(
}
}

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"))
transferToken <- EitherT.pure[Future, AppError](decrypt(env.config.cypherSecret, cypheredInfos, ctx.tenant))
transfer <- EitherT.fromOptionF[Future, AppError, ApiSubscriptionTransfer](env.dataStore.apiSubscriptionTransferRepo.forTenant(ctx.tenant).findOneNotDeleted(Json.obj("token" -> transferToken)),
AppError.Unauthorized)
_ <- EitherT.cond[Future][AppError, Unit](transfer.date.plusDays(1).isAfter(DateTime.now()), (), AppError.ForbiddenAction) //give reason
subscription <- EitherT.fromOptionF[Future, AppError, ApiSubscription](env.dataStore.apiSubscriptionRepo.forTenant(ctx.tenant).findByIdNotDeleted(transfer.subscription), AppError.SubscriptionNotFound)
team <- EitherT.fromOptionF[Future, AppError, Team](env.dataStore.teamRepo.forTenant(ctx.tenant).findByIdNotDeleted(subscription.team), AppError.TeamNotFound)
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.asJson,
"api" -> api.asJson,
"plan" -> usagePlan.asJson,
"ownerTeam" -> team.asJson
))
})
.leftMap(_.render())
.merge
}
}

def getTransferLink(teamId: String, subscriptionId: String) =
DaikokuAction.async { ctx =>
TeamAdminOnly(
AuditTrailEvent(s"@{user.name} has generated a link to transfer subscription @{subscription.id}"))(teamId, ctx) { team => {

ctx.setCtxValue("subscription.id", subscriptionId)
(for {
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"))

transfer = ApiSubscriptionTransfer(
id = DatastoreId(IdGenerator.token(16)),
tenant = ctx.tenant.id,
token = IdGenerator.token,
subscription = subscription.id,
by = ctx.user.id,
date = DateTime.now()
)
cipheredToken = encrypt(env.config.cypherSecret, transfer.token, ctx.tenant)
_ <- EitherT.liftF[Future, AppError, Boolean](env.dataStore.apiSubscriptionTransferRepo.forTenant(ctx.tenant).delete(Json.obj("subscription" -> subscription.id.asJson)))
_ <- EitherT.liftF[Future, AppError, Boolean](env.dataStore.apiSubscriptionTransferRepo.forTenant(ctx.tenant).save(transfer))
link <- EitherT.pure[Future, AppError](s"${env.getDaikokuUrl(ctx.tenant, "/subscriptions/_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 => {

(for {
extract <- apiService.checkAndExtractTransferLink(ctx.tenant, subscriptionId, (ctx.request.body \ "token").as[String], team)
otoroshiSettings <- EitherT.fromOption[Future][AppError, OtoroshiSettings](extract.plan.otoroshiTarget.flatMap(target => ctx.tenant.otoroshiSettings.find(_.id == target.otoroshiSettings)), AppError.EntityNotFound("Otoroshi settings"))
_ <- apiService.transferSubscription(
newTeam = team,
subscription = extract.subscription,
childs = extract.childSubscriptions,
tenant = ctx.tenant,
user = ctx.user,
api = extract.api,
plan = extract.plan,
otoroshiSettings = otoroshiSettings)
} yield Ok(Json.obj("done" -> true)))
.leftMap(_.render())
.merge
}
}
}

def makeUniqueSubscription(teamId: String, subscriptionId: String) =
DaikokuAction.async { ctx =>
TeamApiKeyAction(
Expand Down
2 changes: 1 addition & 1 deletion daikoku/app/controllers/NotificationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ class NotificationController(
AuditTrailEvent(
s"@{user.name} has rejected a notifications for team @{team.name} - @{team.id} => @{notification.id}"
)
)(teamId.value, ctx) { _ =>
)(teamId.value, ctx) { team =>
{

ctx.setCtxValue("notification.id", notification.id)
Expand Down
11 changes: 11 additions & 0 deletions daikoku/app/domain/apikeyEntities.scala
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,14 @@ case class StepValidator(
) extends CanJson[StepValidator] {
override def asJson: JsValue = json.StepValidatorFormat.writes(this)
}

case class ApiSubscriptionTransfer(
id: DatastoreId,
tenant: TenantId,
deleted: Boolean = false,
token: String,
subscription: ApiSubscriptionId,
date: DateTime,
by: UserId) extends CanJson[ApiSubscriptionTransfer] {
override def asJson: JsValue = json.ApiSubscriptionTransferFormat.writes(this)
}
55 changes: 55 additions & 0 deletions daikoku/app/domain/json.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3219,6 +3219,7 @@ object json {
case "NewIssueOpen" => NewIssueOpenFormat.reads(json)
case "NewCommentOnIssue" => NewCommentOnIssueFormat.reads(json)
case "TransferApiOwnership" => TransferApiOwnershipFormat.reads(json)
case "ApiSubscriptionTransferSuccess" => ApiSubscriptionTransferSuccessFormat.reads(json)
case "CheckoutForSubscription" =>
CheckoutForSubscriptionFormat.reads(json)
case str => JsError(s"Bad notification value: $str")
Expand All @@ -3238,6 +3239,10 @@ object json {
ApiSubscriptionDemandFormat.writes(p).as[JsObject] ++ Json.obj(
"type" -> "ApiSubscription"
)
case p: ApiSubscriptionTransferSuccess =>
ApiSubscriptionTransferSuccessFormat.writes(p).as[JsObject] ++ Json.obj(
"type" -> "ApiSubscriptionTransferSuccess"
)
case p: ApiSubscriptionReject =>
ApiSubscriptionRejectFormat.writes(p).as[JsObject] ++ Json.obj(
"type" -> "ApiSubscriptionReject"
Expand Down Expand Up @@ -3481,6 +3486,24 @@ object json {
.as[JsValue]
)
}
val ApiSubscriptionTransferSuccessFormat = new Format[ApiSubscriptionTransferSuccess] {

override def reads(json: JsValue): JsResult[ApiSubscriptionTransferSuccess] =
Try {
JsSuccess(
ApiSubscriptionTransferSuccess(
subscription = (json \ "subscription").as(ApiSubscriptionIdFormat)
)
)
} recover {
case e => JsError(e.getMessage)
} get

override def writes(o: ApiSubscriptionTransferSuccess): JsValue =
Json.obj(
"subscription" -> o.subscription.asJson
)
}
val ApiSubscriptionRejectFormat = new Format[ApiSubscriptionReject] {
override def reads(json: JsValue): JsResult[ApiSubscriptionReject] =
Try {
Expand Down Expand Up @@ -4738,6 +4761,38 @@ object json {
)
}

val ApiSubscriptionTransferFormat = new Format[ApiSubscriptionTransfer] {

override def reads(json: JsValue): JsResult[ApiSubscriptionTransfer] =
Try {
ApiSubscriptionTransfer(
id = (json \ "_id").as(DatastoreIdFormat),
tenant = (json \ "_tenant").as(TenantIdFormat),
deleted = (json \ "_deleted").as[Boolean],
token = (json \ "token").as[String],
subscription = (json \ "subscription").as(ApiSubscriptionIdFormat),
by = (json \ "by").as(UserIdFormat),
date = (json \ "date").as(DateTimeFormat)
)
} match {
case Failure(e) =>
AppLogger.error(e.getMessage, e)
JsError(e.getMessage)
case Success(value) => JsSuccess(value)
}

override def writes(o: ApiSubscriptionTransfer): JsValue =
Json.obj(
"_id" -> o.id.asJson,
"_tenant" -> o.tenant.asJson,
"_deleted" -> o.deleted,
"token" -> o.token,
"subscription" -> o.subscription.asJson,
"by" -> o.by.asJson,
"date" -> DateTimeFormat.writes(o.date)
)
}

val SetOtoroshiServicesIdFormat =
Format(
Reads.set(OtoroshiServiceIdFormat),
Expand Down
4 changes: 4 additions & 0 deletions daikoku/app/domain/teamEntities.scala
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ object NotificationAction {
motivation: Option[String]
) extends NotificationAction

case class ApiSubscriptionTransferSuccess(
subscription: ApiSubscriptionId,
) extends NotificationAction

case class OtoroshiSyncSubscriptionError(
subscription: ApiSubscription,
message: String
Expand Down
4 changes: 4 additions & 0 deletions daikoku/app/storage/api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@ trait UserRepo extends Repo[User, UserId]
trait EvolutionRepo extends Repo[Evolution, DatastoreId]
trait ReportsInfoRepo extends Repo[ReportsInfo, DatastoreId]

trait ApiSubscriptionTransferRepo extends TenantCapableRepo[ApiSubscriptionTransfer, DatastoreId]

trait TeamRepo extends TenantCapableRepo[Team, TeamId] {
def myTeams(tenant: Tenant, user: User)(implicit
env: Env,
Expand Down Expand Up @@ -571,6 +573,8 @@ trait DataStore {

def reportsInfoRepo: ReportsInfoRepo

def apiSubscriptionTransferRepo: ApiSubscriptionTransferRepo

def exportAsStream(pretty: Boolean, exportAuditTrail: Boolean = true)(implicit
ec: ExecutionContext,
mat: Materializer,
Expand Down
50 changes: 48 additions & 2 deletions daikoku/app/storage/drivers/postgres/PostgresDataStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,19 @@ case class PostgresTenantCapableUsagePlanRepo(
_tenantRepo(tenant)
}

case class PostgresTenantCapableApiSubscriptionTransferRepo(
_repo: () => PostgresRepo[ApiSubscriptionTransfer, DatastoreId],
_tenantRepo: TenantId => PostgresTenantAwareRepo[ApiSubscriptionTransfer, DatastoreId]
) extends PostgresTenantCapableRepo[ApiSubscriptionTransfer, DatastoreId]
with ApiSubscriptionTransferRepo {
override def repo(): PostgresRepo[ApiSubscriptionTransfer, DatastoreId] = _repo()

override def tenantRepo(
tenant: TenantId
): PostgresTenantAwareRepo[ApiSubscriptionTransfer, DatastoreId] =
_tenantRepo(tenant)
}

case class PostgresTenantCapableConsumptionRepo(
_repo: () => PostgresRepo[ApiKeyConsumption, DatastoreId],
_tenantRepo: TenantId => PostgresTenantAwareRepo[
Expand Down Expand Up @@ -413,7 +426,8 @@ class PostgresDataStore(configuration: Configuration, env: Env, pgPool: PgPool)
"step_validators" -> true,
"usage_plans" -> true,
"assets" -> true,
"reports_info" -> true
"reports_info" -> true,
"api_subscription_transfers" -> true
)

private lazy val poolOptions: PoolOptions = new PoolOptions()
Expand Down Expand Up @@ -614,6 +628,12 @@ class PostgresDataStore(configuration: Configuration, env: Env, pgPool: PgPool)
t => new PostgresTenantUsagePlanRepo(env, reactivePg, t)
)

private val _apiSubscriptionTransferRepo: ApiSubscriptionTransferRepo =
PostgresTenantCapableApiSubscriptionTransferRepo(
() => new PostgresApiSubscriptionTransferRepo(env, reactivePg),
t => new PostgresTenantApiSubscriptionTransferRepo(env, reactivePg, t)
)

override def tenantRepo: TenantRepo = _tenantRepo

override def userRepo: UserRepo = _userRepo
Expand Down Expand Up @@ -666,6 +686,8 @@ class PostgresDataStore(configuration: Configuration, env: Env, pgPool: PgPool)

override def usagePlanRepo: UsagePlanRepo = _usagePlanRepo

override def apiSubscriptionTransferRepo: ApiSubscriptionTransferRepo = _apiSubscriptionTransferRepo

override def start(): Future[Unit] = {
Future.successful(())
}
Expand Down Expand Up @@ -788,7 +810,8 @@ class PostgresDataStore(configuration: Configuration, env: Env, pgPool: PgPool)
assetRepo.forAllTenant(),
stepValidatorRepo.forAllTenant(),
subscriptionDemandRepo.forAllTenant(),
usagePlanRepo.forAllTenant()
usagePlanRepo.forAllTenant(),
apiSubscriptionTransferRepo.forAllTenant()
)

if (exportAuditTrail) {
Expand Down Expand Up @@ -912,6 +935,10 @@ class PostgresDataStore(configuration: Configuration, env: Env, pgPool: PgPool)
emailVerificationRepo
.forAllTenant()
.save(json.EmailVerificationFormat.reads(payload).get)
case ("apisubscriptiontransfers", payload) =>
apiSubscriptionTransferRepo
.forAllTenant()
.save(json.ApiSubscriptionTransferFormat.reads(payload).get)
case (typ, _) =>
logger.error(s"Unknown type: $typ")
FastFuture.successful(false)
Expand Down Expand Up @@ -1108,6 +1135,16 @@ class PostgresTenantUsagePlanRepo(
override def extractId(value: UsagePlan): String = value.id.value
}

class PostgresTenantApiSubscriptionTransferRepo(env: Env, reactivePg: ReactivePg, tenant: TenantId)
extends PostgresTenantAwareRepo[ApiSubscriptionTransfer, DatastoreId](env, reactivePg, tenant) {

override def tableName: String = "api_subscription_transfers"

override def format: Format[ApiSubscriptionTransfer] = json.ApiSubscriptionTransferFormat

override def extractId(value: ApiSubscriptionTransfer): String = value.id.value
}

class PostgresTenantCmsPageRepo(
env: Env,
reactivePg: ReactivePg,
Expand Down Expand Up @@ -1372,6 +1409,15 @@ class PostgresUsagePlanRepo(env: Env, reactivePg: ReactivePg)
override def extractId(value: UsagePlan): String = value.id.value
}

class PostgresApiSubscriptionTransferRepo(env: Env, reactivePg: ReactivePg)
extends PostgresRepo[ApiSubscriptionTransfer, DatastoreId](env, reactivePg) {
override def tableName: String = "api_subscription_transfers"

override def format: Format[ApiSubscriptionTransfer] = json.ApiSubscriptionTransferFormat

override def extractId(value: ApiSubscriptionTransfer): String = value.id.value
}

class PostgresApiRepo(env: Env, reactivePg: ReactivePg)
extends PostgresRepo[Api, ApiId](env, reactivePg) {
override def tableName: String = "apis"
Expand Down
Loading

0 comments on commit d567118

Please sign in to comment.