Skip to content

Commit

Permalink
Merge branch 'master' into feat/#728
Browse files Browse the repository at this point in the history
  • Loading branch information
quentinovega committed Sep 16, 2024
2 parents 543a792 + d274de6 commit 93bfff0
Show file tree
Hide file tree
Showing 67 changed files with 4,021 additions and 1,238 deletions.
4 changes: 3 additions & 1 deletion daikoku/app/controllers/AdminApiController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,9 @@ class StateAdminApiController(
AppError.SecurityError("Action not avalaible")
)
_ <- EitherT.liftF[Future, AppError, Unit](env.dataStore.clear())
_ <- EitherT.liftF[Future, AppError, Done](env.initDatastore(ctx.request.getQueryString("path")))
_ <- EitherT.liftF[Future, AppError, Done](
env.initDatastore(ctx.request.getQueryString("path"))
)
} yield Ok(Json.obj("done" -> true)))
.leftMap(_.render())
.merge
Expand Down
96 changes: 94 additions & 2 deletions 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 @@ -2077,6 +2077,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 Expand Up @@ -3312,13 +3396,21 @@ class ApiController(
if (ctx.user.isDaikokuAdmin) Json.obj()
else Json.obj("users.userId" -> ctx.user.id.value)

val typeFilter = if (ctx.tenant.subscriptionSecurity.isDefined
&& ctx.tenant.subscriptionSecurity.exists(identity)) {
Json.obj(
"type" -> Json.obj("$ne" -> TeamType.Personal.name)
)
} else {
Json.obj()
}
for {
myTeams <- env.dataStore.teamRepo.myTeams(ctx.tenant, ctx.user)
teams <-
env.dataStore.teamRepo
.forTenant(ctx.tenant.id)
.findNotDeleted(
Json.obj("name" -> searchAsRegex) ++ teamUsersFilter,
Json.obj("name" -> searchAsRegex) ++ teamUsersFilter ++ typeFilter,
5,
Json.obj("name" -> 1).some
)
Expand Down
25 changes: 13 additions & 12 deletions daikoku/app/controllers/AppError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,19 @@ object AppError {
case ApiNotLinked => BadRequest(toJson(error))
case UserNotTeamAdmin(userId, teamId) =>
play.api.mvc.Results.Unauthorized(toJson(error))
case OtoroshiError(e) => BadRequest(e)
case PaymentError(_) => BadRequest(toJson(error))
case SubscriptionConflict => Conflict(toJson(error))
case ApiKeyRotationConflict => Conflict(toJson(error))
case EntityConflict(_) => Conflict(toJson(error))
case ApiKeyRotationError(e) => BadRequest(e)
case ForbiddenAction => Forbidden(toJson(error))
case ApiKeyCustomMetadataNotPrivided => BadRequest(toJson(error))
case SubscriptionNotFound => NotFound(toJson(error))
case SubscriptionParentExisted => Conflict(toJson(error))
case SubscriptionAggregationDisabled => BadRequest(toJson(error))
case EnvironmentSubscriptionAggregationDisabled => BadRequest(toJson(error))
case OtoroshiError(e) => BadRequest(e)
case PaymentError(_) => BadRequest(toJson(error))
case SubscriptionConflict => Conflict(toJson(error))
case ApiKeyRotationConflict => Conflict(toJson(error))
case EntityConflict(_) => Conflict(toJson(error))
case ApiKeyRotationError(e) => BadRequest(e)
case ForbiddenAction => Forbidden(toJson(error))
case ApiKeyCustomMetadataNotPrivided => BadRequest(toJson(error))
case SubscriptionNotFound => NotFound(toJson(error))
case SubscriptionParentExisted => Conflict(toJson(error))
case SubscriptionAggregationDisabled => BadRequest(toJson(error))
case EnvironmentSubscriptionAggregationDisabled =>
BadRequest(toJson(error))
case SubscriptionAggregationTeamConflict => Conflict(toJson(error))
case SubscriptionAggregationOtoroshiConflict => Conflict(toJson(error))
case MissingParentSubscription => NotFound(toJson(error))
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
34 changes: 30 additions & 4 deletions daikoku/app/domain/CommonServices.scala
Original file line number Diff line number Diff line change
Expand Up @@ -648,19 +648,34 @@ object CommonServices {
env: Env,
ec: ExecutionContext
) = {

val typeFilter =
if (
ctx.tenant.subscriptionSecurity.isDefined
&& ctx.tenant.subscriptionSecurity.exists(identity)
) {
Json.obj(
"type" -> Json.obj("$ne" -> TeamType.Personal.name)
)
} else {
Json.obj()
}
_UberPublicUserAccess(
AuditTrailEvent("@{user.name} has accessed his team list")
)(ctx) {
(if (ctx.user.isDaikokuAdmin)
env.dataStore.teamRepo
.forTenant(ctx.tenant)
.findAllNotDeleted()
.findNotDeleted(typeFilter)
else
env.dataStore.teamRepo
.forTenant(ctx.tenant)
.findNotDeleted(Json.obj("users.userId" -> ctx.user.id.value)))
.findNotDeleted(
Json.obj("users.userId" -> ctx.user.id.value) ++ typeFilter
))
.map(teams =>
teams.sortWith((a, b) => a.name.compareToIgnoreCase(b.name) < 0)
teams
.sortWith((a, b) => a.name.compareToIgnoreCase(b.name) < 0)
)
}
}
Expand All @@ -673,6 +688,17 @@ object CommonServices {
_TenantAdminAccessTenant(
AuditTrailEvent("@{user.name} has accessed to all teams list")
)(ctx) {
val typeFilter =
if (
ctx.tenant.subscriptionSecurity.isDefined
&& ctx.tenant.subscriptionSecurity.exists(identity)
) {
Json.obj(
"type" -> TeamType.Organization.name
)
} else {
Json.obj()
}
for {
teams <-
env.dataStore.teamRepo
Expand All @@ -681,7 +707,7 @@ object CommonServices {
Json.obj(
"_deleted" -> false,
"name" -> Json.obj("$regex" -> research)
),
) ++ typeFilter,
offset,
limit,
Some(Json.obj("_humanReadableId" -> 1))
Expand Down
3 changes: 2 additions & 1 deletion daikoku/app/domain/SchemaDefinition.scala
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ object SchemaDefinition {
Field(
"environmentAggregationApiKeysSecurity",
OptionType(BooleanType),
resolve = _.value.environmentAggregationApiKeysSecurity),
resolve = _.value.environmentAggregationApiKeysSecurity
),
Field(
"display",
OptionType(StringType),
Expand Down
12 changes: 12 additions & 0 deletions daikoku/app/domain/apikeyEntities.scala
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,15 @@ 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)
}
67 changes: 64 additions & 3 deletions daikoku/app/domain/json.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,7 @@ object json {
otoroshiTarget =
(json \ "otoroshiTarget").asOpt(OtoroshiTargetFormat),
aggregationApiKeysSecurity =
(json \ "aggregationApiKeysSecurity").asOpt[Boolean],
(json \ "aggregationApiKeysSecurity").asOpt[Boolean]
)
)
} recover {
Expand Down Expand Up @@ -2205,8 +2205,9 @@ object json {
tenantMode = (json \ "tenantMode").asOpt(TenantModeFormat),
aggregationApiKeysSecurity = (json \ "aggregationApiKeysSecurity")
.asOpt[Boolean],
environmentAggregationApiKeysSecurity = (json \ "environmentAggregationApiKeysSecurity")
.asOpt[Boolean],
environmentAggregationApiKeysSecurity =
(json \ "environmentAggregationApiKeysSecurity")
.asOpt[Boolean],
robotTxt = (json \ "robotTxt").asOpt[String],
thirdPartyPaymentSettings = (json \ "thirdPartyPaymentSettings")
.asOpt(SeqThirdPartyPaymentSettingsFormat)
Expand Down Expand Up @@ -3230,6 +3231,8 @@ 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 @@ -3249,6 +3252,11 @@ 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 @@ -3492,6 +3500,27 @@ 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 @@ -4749,6 +4778,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
Loading

0 comments on commit 93bfff0

Please sign in to comment.