diff --git a/daikoku/app/controllers/AdminApiController.scala b/daikoku/app/controllers/AdminApiController.scala
index 2abd49911..2a85859b2 100644
--- a/daikoku/app/controllers/AdminApiController.scala
+++ b/daikoku/app/controllers/AdminApiController.scala
@@ -322,7 +322,7 @@ class StateAdminApiController(
}
def reset() =
- DaikokuApiAction.async { _ =>
+ DaikokuApiAction.async { ctx =>
(for {
_ <- EitherT.cond[Future][AppError, Unit](
env.config.isDev || env.config.mode == DaikokuMode.Test,
@@ -330,7 +330,7 @@ class StateAdminApiController(
AppError.SecurityError("Action not avalaible")
)
_ <- EitherT.liftF[Future, AppError, Unit](env.dataStore.clear())
- _ <- EitherT.liftF[Future, AppError, Done](env.initDatastore())
+ _ <- EitherT.liftF[Future, AppError, Done](env.initDatastore(ctx.request.getQueryString("path")))
} yield Ok(Json.obj("done" -> true)))
.leftMap(_.render())
.merge
diff --git a/daikoku/app/controllers/AppError.scala b/daikoku/app/controllers/AppError.scala
index 79753328a..11d46e2c6 100644
--- a/daikoku/app/controllers/AppError.scala
+++ b/daikoku/app/controllers/AppError.scala
@@ -52,6 +52,7 @@ object AppError {
case object SubscriptionAggregationTeamConflict extends AppError
case object SubscriptionAggregationOtoroshiConflict extends AppError
case object SubscriptionAggregationDisabled extends AppError
+ case object EnvironmentSubscriptionAggregationDisabled extends AppError
case object MissingParentSubscription extends AppError
case object TranslationNotFound extends AppError
case object Unauthorized extends AppError
@@ -106,6 +107,7 @@ object AppError {
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))
@@ -115,7 +117,7 @@ object AppError {
case NameAlreadyExists => Conflict(toJson(error))
case ThirdPartyPaymentSettingsNotFound => NotFound(toJson(error))
case SecurityError(security) =>
- play.api.mvc.Results.Unauthorized(toJson(error))
+ play.api.mvc.Results.Forbidden(toJson(error))
case TeamAlreadyVerified => Conflict(toJson(error))
case UnexpectedError => BadRequest(toJson(error))
case InternalServerError(message) =>
@@ -167,6 +169,8 @@ object AppError {
"The subscription already has a subscription parent - it cannot be extended any further"
case SubscriptionAggregationDisabled =>
"Aggregation of api keys is disabled on plan or on tenant"
+ case EnvironmentSubscriptionAggregationDisabled =>
+ "Aggregation of api keys is disabled on plan or on tenant for environment mode"
case SubscriptionAggregationTeamConflict =>
"The new subscription has another team of the parent subscription"
case SubscriptionAggregationOtoroshiConflict =>
diff --git a/daikoku/app/controllers/admin-api-openapi.json b/daikoku/app/controllers/admin-api-openapi.json
index 1dfac1d21..25baf46b9 100644
--- a/daikoku/app/controllers/admin-api-openapi.json
+++ b/daikoku/app/controllers/admin-api-openapi.json
@@ -142,6 +142,10 @@
"type": "boolean",
"nullable": true
},
+ "environmentAggregationApiKeysSecurity": {
+ "type": "boolean",
+ "nullable": true
+ },
"robotTxt": {
"type": "string",
"nullable": true
@@ -2486,7 +2490,12 @@
"aggregationApiKeysSecurity": {
"type": "boolean",
"nullable": false
+ },
+ "environmentAggregationApiKeysSecurity": {
+ "type": "boolean",
+ "nullable": false
}
+
},
"required": [
"_id",
@@ -2581,6 +2590,10 @@
"type": "boolean",
"nullable": true
},
+ "environmentAggregationApiKeysSecurity": {
+ "type": "boolean",
+ "nullable": true
+ },
"swagger": {
"nullable": true,
"$ref": "#/components/schemas/SwaggerAccess"
@@ -2703,6 +2716,10 @@
"type": "boolean",
"nullable": false
},
+ "environmentAggregationApiKeysSecurity": {
+ "type": "boolean",
+ "nullable": false
+ },
"swagger": {
"nullable": true,
"$ref": "#/components/schemas/SwaggerAccess"
@@ -2835,6 +2852,10 @@
"type": "boolean",
"nullable": false
},
+ "environmentAggregationApiKeysSecurity": {
+ "type": "boolean",
+ "nullable": false
+ },
"swagger": {
"nullable": true,
"$ref": "#/components/schemas/SwaggerAccess"
@@ -2973,6 +2994,10 @@
"type": "boolean",
"nullable": false
},
+ "environmentAggregationApiKeysSecurity": {
+ "type": "boolean",
+ "nullable": false
+ },
"swagger": {
"nullable": true,
"$ref": "#/components/schemas/SwaggerAccess"
@@ -3108,6 +3133,10 @@
"type": "boolean",
"nullable": false
},
+ "environmentAggregationApiKeysSecurity": {
+ "type": "boolean",
+ "nullable": false
+ },
"swagger": {
"nullable": true,
"$ref": "#/components/schemas/SwaggerAccess"
diff --git a/daikoku/app/domain/SchemaDefinition.scala b/daikoku/app/domain/SchemaDefinition.scala
index c23495066..64f73e029 100644
--- a/daikoku/app/domain/SchemaDefinition.scala
+++ b/daikoku/app/domain/SchemaDefinition.scala
@@ -245,6 +245,10 @@ object SchemaDefinition {
OptionType(BooleanType),
resolve = _.value.aggregationApiKeysSecurity
),
+ Field(
+ "environmentAggregationApiKeysSecurity",
+ OptionType(BooleanType),
+ resolve = _.value.environmentAggregationApiKeysSecurity),
Field(
"display",
OptionType(StringType),
diff --git a/daikoku/app/domain/json.scala b/daikoku/app/domain/json.scala
index 98afebe58..e632c15b9 100644
--- a/daikoku/app/domain/json.scala
+++ b/daikoku/app/domain/json.scala
@@ -1007,7 +1007,7 @@ object json {
otoroshiTarget =
(json \ "otoroshiTarget").asOpt(OtoroshiTargetFormat),
aggregationApiKeysSecurity =
- (json \ "aggregationApiKeysSecurity").asOpt[Boolean]
+ (json \ "aggregationApiKeysSecurity").asOpt[Boolean],
)
)
} recover {
@@ -2202,6 +2202,8 @@ object json {
tenantMode = (json \ "tenantMode").asOpt(TenantModeFormat),
aggregationApiKeysSecurity = (json \ "aggregationApiKeysSecurity")
.asOpt[Boolean],
+ environmentAggregationApiKeysSecurity = (json \ "environmentAggregationApiKeysSecurity")
+ .asOpt[Boolean],
robotTxt = (json \ "robotTxt").asOpt[String],
thirdPartyPaymentSettings = (json \ "thirdPartyPaymentSettings")
.asOpt(SeqThirdPartyPaymentSettingsFormat)
@@ -2279,6 +2281,10 @@ object json {
.map(JsBoolean)
.getOrElse(JsBoolean(false))
.as[JsValue],
+ "environmentAggregationApiKeysSecurity" -> o.environmentAggregationApiKeysSecurity
+ .map(JsBoolean)
+ .getOrElse(JsBoolean(false))
+ .as[JsValue],
"robotTxt" -> o.robotTxt
.map(JsString.apply)
.getOrElse(JsNull)
diff --git a/daikoku/app/domain/tenantEntities.scala b/daikoku/app/domain/tenantEntities.scala
index bda1581cb..33dad90d9 100644
--- a/daikoku/app/domain/tenantEntities.scala
+++ b/daikoku/app/domain/tenantEntities.scala
@@ -388,6 +388,7 @@ case class Tenant(
defaultMessage: Option[String] = None,
tenantMode: Option[TenantMode] = None,
aggregationApiKeysSecurity: Option[Boolean] = None,
+ environmentAggregationApiKeysSecurity: Option[Boolean] = None,
robotTxt: Option[String] = None,
thirdPartyPaymentSettings: Seq[ThirdPartyPaymentSettings] = Seq.empty,
display: TenantDisplay = TenantDisplay.Default,
@@ -455,6 +456,10 @@ case class Tenant(
.map(JsBoolean)
.getOrElse(JsBoolean(false))
.as[JsValue],
+ "environmentAggregationApiKeysSecurity" -> environmentAggregationApiKeysSecurity
+ .map(JsBoolean)
+ .getOrElse(JsBoolean(false))
+ .as[JsValue],
"display" -> display.name,
"environments" -> JsArray(environments.map(JsString.apply).toSeq),
"loginProvider" -> authProvider.name,
diff --git a/daikoku/app/env/env.scala b/daikoku/app/env/env.scala
index c7d3635cd..76a9e3b01 100644
--- a/daikoku/app/env/env.scala
+++ b/daikoku/app/env/env.scala
@@ -306,7 +306,7 @@ sealed trait Env {
def getDaikokuUrl(tenant: Tenant, path: String): String
- def initDatastore()(implicit ec: ExecutionContext): Future[Done]
+ def initDatastore(path: Option[String]= None)(implicit ec: ExecutionContext): Future[Done]
}
class DaikokuEnv(
@@ -376,14 +376,14 @@ class DaikokuEnv(
}
}
- override def initDatastore()(implicit ec: ExecutionContext): Future[Done] = {
+ override def initDatastore(path: Option[String]=None)(implicit ec: ExecutionContext): Future[Done] = {
def run(isEmpty: Boolean): Future[Unit] = {
if (isEmpty) {
(dataStore match {
case store: PostgresDataStore => store.checkDatabase()
case _ => FastFuture.successful(None)
}).map { _ =>
- config.init.data.from match {
+ path.orElse(config.init.data.from) match {
case Some(path)
if path.startsWith("http://") || path
.startsWith("https://") =>
diff --git a/daikoku/app/utils/ApiService.scala b/daikoku/app/utils/ApiService.scala
index ff3fd391d..2d88f3803 100644
--- a/daikoku/app/utils/ApiService.scala
+++ b/daikoku/app/utils/ApiService.scala
@@ -2328,6 +2328,14 @@ class ApiService(
.findById(subscription.plan),
AppError.PlanNotFound
)
+ _ <- EitherT.cond[Future][AppError, Unit](
+ tenant.display != TenantDisplay.Environment || (tenant.environmentAggregationApiKeysSecurity match {
+ case Some(true) => plan.customName == parentPlan.customName
+ case _ => true
+ }),
+ (),
+ AppError.SecurityError(s"Environment Subscription Aggregation security is enabled, a subscription cannot be extended by another environment")
+ )
_ <- EitherT.cond[Future][AppError, Unit](
parentPlan.otoroshiTarget
.map(_.otoroshiSettings) == plan.otoroshiTarget
diff --git a/daikoku/javascript/src/components/adminbackoffice/otoroshi/InititializeFromOtoroshi.tsx b/daikoku/javascript/src/components/adminbackoffice/otoroshi/InititializeFromOtoroshi.tsx
index d95c5e592..6e4ced022 100644
--- a/daikoku/javascript/src/components/adminbackoffice/otoroshi/InititializeFromOtoroshi.tsx
+++ b/daikoku/javascript/src/components/adminbackoffice/otoroshi/InititializeFromOtoroshi.tsx
@@ -184,7 +184,6 @@ export const InitializeFromOtoroshi = () => {
const orderedApikeys = orderBy(state.context.apikeys, ['clientName']);
const filterApikeys = (entity: { label: string, prefix: string, value: string }) => {
- console.debug({ entity, orderedApikeys })
return orderedApikeys.filter((apikey) => (apikey.authorizedEntities || '').includes(`${entity.prefix}${entity.value}`));
};
diff --git a/daikoku/javascript/src/components/adminbackoffice/tenants/TenantEdit.tsx b/daikoku/javascript/src/components/adminbackoffice/tenants/TenantEdit.tsx
index ed5aac232..348cdf28d 100644
--- a/daikoku/javascript/src/components/adminbackoffice/tenants/TenantEdit.tsx
+++ b/daikoku/javascript/src/components/adminbackoffice/tenants/TenantEdit.tsx
@@ -1,68 +1,103 @@
import { useContext } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
-import { Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
+import {
+ Route,
+ Routes,
+ useLocation,
+ useNavigate,
+ useParams,
+} from 'react-router-dom';
import { useDaikokuBackOffice, useTenantBackOffice } from '../../../contexts';
import { I18nContext } from '../../../contexts/i18n-context';
import * as Services from '../../../services';
import { ITenantFull } from '../../../types/tenant';
import { Spinner } from '../../utils/Spinner';
-import { AuditForm, AuthenticationForm, BucketForm, CustomizationForm, GeneralForm, MailForm } from './forms';
+import {
+ AuditForm,
+ AuthenticationForm,
+ BucketForm,
+ CustomizationForm,
+ GeneralForm,
+ MailForm,
+} from './forms';
import { SecurityForm } from './forms/SecurityForm';
import { ThirdPartyPaymentForm } from './forms/ThirdPartyPaymentForm';
import { ResponseError, isError } from '../../../types';
import { DisplayForm } from './forms/DisplayForm';
-export const TenantEditComponent = ({ tenantId, fromDaikokuAdmin }: { tenantId: string, fromDaikokuAdmin?: boolean }) => {
- const { translate } = useContext(I18nContext)
+export const TenantEditComponent = ({
+ tenantId,
+ fromDaikokuAdmin,
+}: {
+ tenantId: string;
+ fromDaikokuAdmin?: boolean;
+}) => {
+ const { translate } = useContext(I18nContext);
const navigate = useNavigate();
const { state } = useLocation();
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
const { isLoading, data } = useQuery({
queryKey: ['full-tenant'],
queryFn: () => Services.oneTenant(tenantId),
- enabled: !state
- })
+ enabled: !state,
+ });
const updateTenant = useMutation({
- mutationFn: (tenant: ITenantFull) => Services.saveTenant(tenant).then(r => isError(r) ? Promise.reject(r) : r),
+ mutationFn: (tenant: ITenantFull) =>
+ Services.saveTenant(tenant).then((r) =>
+ isError(r) ? Promise.reject(r) : r
+ ),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["full-tenant"] });
- queryClient.invalidateQueries({ queryKey: ["context"] });
- toast.success(translate('Tenant updated successfully'))
+ queryClient.invalidateQueries({ queryKey: ['full-tenant'] });
+ queryClient.invalidateQueries({ queryKey: ['context'] });
+ toast.success(translate('Tenant updated successfully'));
},
onError: (e: ResponseError) => {
- toast.error(translate(e.error))
+ toast.error(translate(e.error));
//todo: reset forms
- }
+ },
});
const createTenant = useMutation({
mutationFn: (tenant: ITenantFull) => Services.createTenant(tenant),
onSuccess: (createdTenant) => {
- navigate(`/settings/tenants/${createdTenant._humanReadableId}/general`)
- queryClient.invalidateQueries({ queryKey: ['tenant'] })
- toast.success(translate({ key: 'tenant.created.success', replacements:[`${createdTenant.name}`]}))
-
+ navigate(`/settings/tenants/${createdTenant._humanReadableId}/general`);
+ queryClient.invalidateQueries({ queryKey: ['tenant'] });
+ toast.success(
+ translate({
+ key: 'tenant.created.success',
+ replacements: [`${createdTenant.name}`],
+ })
+ );
+ },
+ onError: () => {
+ toast.error(translate('Error'));
},
- onError: () => { toast.error(translate('Error')) }
});
if (isLoading && !state) {
- return (
- {tenant.name} - {translate('General')}
}
-
+ {tenant.name} - {translate('General')}
+
+ )}
+ {tenant.name} - {translate('Customization')}
}
+ {fromDaikokuAdmin && (
+
+ {tenant.name} - {translate('Customization')}
+
+ )}
{tenant.name} - {translate('Audit')}
}
+ {fromDaikokuAdmin && (
+
+ {tenant.name} - {translate('Audit')}
+
+ )}
{tenant.name} - {translate('Mail')}
}
+ {fromDaikokuAdmin && (
+
+ {tenant.name} - {translate('Mail')}
+
+ )}
{tenant.name} - {translate('Authentication')}
}
+ {fromDaikokuAdmin && (
+
+ {tenant.name} - {translate('Authentication')}
+
+ )}
{tenant.name} - {translate('Bucket')}
}
+ {fromDaikokuAdmin && (
+
+ {tenant.name} - {translate('Bucket')}
+
+ )}
{tenant.name} - {translate('Security')}
}
-
+ {tenant.name} - {translate('Security')}
+
+ )}
+ {tenant.name} - {translate('Third-Party payment')}
}
+ {fromDaikokuAdmin && (
+
+ {tenant.name} - {translate('Third-Party payment')}
+
+ )}
{tenant.name} - {translate('DisplayMode')}
}
+ {fromDaikokuAdmin && (
+
+ {tenant.name} - {translate('DisplayMode')}
+
+ )}