Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: load only cud config in core #920

Merged
merged 5 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [unreleased]

## [6.0.17] - 2024-02-06

- Adds new config `supertokens_saas_load_only_cud` that makes the core instance load a particular CUD only, irrespective of the CUDs present in the db.
- Fixes connection pool handling when connection pool size changes for a tenant.

## [6.0.16] - 2023-11-03

- Collects requests stats per app
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" }
// }
//}

version = "6.0.16"
version = "6.0.17"


repositories {
Expand Down
4 changes: 4 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,7 @@ core_config_version: 0
# when CDI version is not specified in the request. When set to null, the core will assume the latest version of the
# CDI.
# supertokens_max_cdi_version:

# (OPTIONAL | Default: null) string value. If specified, the supertokens service will only load the specified CUD even
# if there are more CUDs in the database and block all other CUDs from being used from this instance.
# supertokens_saas_load_only_cud:
4 changes: 4 additions & 0 deletions devConfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,7 @@ disable_telemetry: true
# when CDI version is not specified in the request. When set to null, the core will assume the latest version of the
# CDI.
# supertokens_max_cdi_version:

# (OPTIONAL | Default: null) string value. If specified, the supertokens service will only load the specified CUD even
# if there are more CUDs in the database and block all other CUDs from being used from this instance.
# supertokens_saas_load_only_cud:
19 changes: 19 additions & 0 deletions src/main/java/io/supertokens/config/CoreConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
import io.supertokens.pluginInterface.LOG_LEVEL;
import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
import io.supertokens.utils.SemVer;
import io.supertokens.webserver.Utils;
import io.supertokens.webserver.WebserverAPI;
import jakarta.servlet.ServletException;
import org.apache.catalina.filters.RemoteAddrFilter;
import org.jetbrains.annotations.TestOnly;

Expand Down Expand Up @@ -197,6 +199,10 @@ public class CoreConfig {
@JsonProperty
private String supertokens_max_cdi_version = null;

@ConfigYamlOnly
@JsonProperty
private String supertokens_saas_load_only_cud = null;

@IgnoreForAnnotationCheck
private Set<LOG_LEVEL> allowedLogLevels = null;

Expand Down Expand Up @@ -254,6 +260,10 @@ public String getBasePath() {
return base_path;
}

public String getSuperTokensLoadOnlyCUD() {
return supertokens_saas_load_only_cud;
}

public enum PASSWORD_HASHING_ALG {
ARGON2, BCRYPT, FIREBASE_SCRYPT
}
Expand Down Expand Up @@ -663,6 +673,15 @@ void normalizeAndValidate(Main main) throws InvalidConfigException {
host = cliHost;
}

if (supertokens_saas_load_only_cud != null) {
try {
supertokens_saas_load_only_cud =
Utils.normalizeAndValidateConnectionUriDomain(supertokens_saas_load_only_cud, true);
} catch (ServletException e) {
throw new InvalidConfigException("supertokens_saas_load_only_cud is invalid");
}
}

access_token_validity = access_token_validity * 1000;
access_token_dynamic_signing_key_update_interval = access_token_dynamic_signing_key_update_interval * 3600 * 1000;
refresh_token_validity = refresh_token_validity * 60 * 1000;
Expand Down
50 changes: 42 additions & 8 deletions src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,20 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
private Main main;
private TenantConfig[] tenantConfigs;

// when the core has `supertokens_saas_load_only_cud` set, the tenantConfigs array will be filtered
// based on the config value. However, we need to keep all the list of CUDs from the db to be able
// to check if the CUD is present in the DB or not, while processing the requests.
private final Set<String> dangerous_allCUDsFromDb = new HashSet<>();

private MultitenancyHelper(Main main) throws StorageQueryException {
this.main = main;
this.tenantConfigs = getAllTenantsFromDb();
TenantConfig[] allTenantsFromDb = getAllTenantsFromDb();
this.tenantConfigs = this.getFilteredTenantConfigs(allTenantsFromDb);
this.dangerous_allCUDsFromDb.clear();

for (TenantConfig config : allTenantsFromDb) {
this.dangerous_allCUDsFromDb.add(config.tenantIdentifier.getConnectionUriDomain());
}
}

public static MultitenancyHelper getInstance(Main main) {
Expand Down Expand Up @@ -81,7 +92,7 @@ public static void init(Main main) throws StorageQueryException, IOException {
// instance of eeFeatureFlag. This is applicable only when the core is starting on
// an empty database as no tenants are loaded from the db yet.
} catch (CannotModifyBaseConfigException | BadPermissionException | FeatureNotEnabledException |
InvalidConfigException | InvalidProviderConfigException | TenantOrAppNotFoundException e) {
InvalidConfigException | InvalidProviderConfigException | TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
}
Expand All @@ -108,10 +119,11 @@ public List<TenantIdentifier> refreshTenantsInCoreBasedOnChangesInCoreConfigOrIf
return main.getResourceDistributor().withResourceDistributorLock(() -> {
try {
TenantConfig[] tenantsFromDb = getAllTenantsFromDb();
TenantConfig[] filteredTenantsFromDb = this.getFilteredTenantConfigs(tenantsFromDb);

Map<ResourceDistributor.KeyClass, JsonObject> normalizedTenantsFromDb =
Config.getNormalisedConfigsForAllTenants(
tenantsFromDb, Config.getBaseConfigAsJsonObject(main));
filteredTenantsFromDb, Config.getBaseConfigAsJsonObject(main));

Map<ResourceDistributor.KeyClass, JsonObject> normalizedTenantsFromMemory =
Config.getNormalisedConfigsForAllTenants(
Expand All @@ -129,9 +141,14 @@ public List<TenantIdentifier> refreshTenantsInCoreBasedOnChangesInCoreConfigOrIf
}
}

boolean sameNumberOfTenants = tenantsFromDb.length == this.tenantConfigs.length;
boolean sameNumberOfTenants =
filteredTenantsFromDb.length == this.tenantConfigs.length;

this.tenantConfigs = tenantsFromDb;
this.dangerous_allCUDsFromDb.clear();
for (TenantConfig tenant : tenantsFromDb) {
this.dangerous_allCUDsFromDb.add(tenant.tenantIdentifier.getConnectionUriDomain());
}
this.tenantConfigs = filteredTenantsFromDb;
if (tenantsThatChanged.size() == 0 && sameNumberOfTenants) {
return tenantsThatChanged;
}
Expand Down Expand Up @@ -190,7 +207,7 @@ public void loadStorageLayer() throws IOException, InvalidConfigException {
public void loadFeatureFlag(List<TenantIdentifier> tenantsThatChanged) {
List<AppIdentifier> apps = new ArrayList<>();
Set<AppIdentifier> appsSet = new HashSet<>();
for (TenantConfig t : tenantConfigs) {
for (TenantConfig t : this.tenantConfigs) {
if (appsSet.contains(t.tenantIdentifier.toAppIdentifier())) {
continue;
}
Expand All @@ -204,7 +221,7 @@ public void loadSigningKeys(List<TenantIdentifier> tenantsThatChanged)
throws UnsupportedJWTSigningAlgorithmException {
List<AppIdentifier> apps = new ArrayList<>();
Set<AppIdentifier> appsSet = new HashSet<>();
for (TenantConfig t : tenantConfigs) {
for (TenantConfig t : this.tenantConfigs) {
if (appsSet.contains(t.tenantIdentifier.toAppIdentifier())) {
continue;
}
Expand Down Expand Up @@ -238,4 +255,21 @@ public TenantConfig[] getAllTenants() {
throw new IllegalStateException(e);
}
}
}

private TenantConfig[] getFilteredTenantConfigs(TenantConfig[] inputTenantConfigs) {
String loadOnlyCUD = Config.getBaseConfig(main).getSuperTokensLoadOnlyCUD();

if (loadOnlyCUD == null) {
return inputTenantConfigs;
}

return Arrays.stream(inputTenantConfigs)
.filter(tenantConfig -> tenantConfig.tenantIdentifier.getConnectionUriDomain().equals(loadOnlyCUD)
|| tenantConfig.tenantIdentifier.getConnectionUriDomain().equals(TenantIdentifier.DEFAULT_CONNECTION_URI))
.toArray(TenantConfig[]::new);
}

public boolean isConnectionUriDomainPresentInDb(String cud) {
return this.dangerous_allCUDsFromDb.contains(cud);
}
}
11 changes: 8 additions & 3 deletions src/main/java/io/supertokens/storageLayer/StorageLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ public static void loadAllTenantStorage(Main main, TenantConfig[] tenants)
}
main.getResourceDistributor().clearAllResourcesWithResourceKey(RESOURCE_KEY);

Set<String> userPoolsInUse = new HashSet<>();
Set<String> uniquePoolsInUse = new HashSet<>();

for (ResourceDistributor.KeyClass key : resourceKeyToStorageMap.keySet()) {
Storage currStorage = resourceKeyToStorageMap.get(key);
Expand All @@ -259,11 +259,16 @@ public static void loadAllTenantStorage(Main main, TenantConfig[] tenants)
main.getResourceDistributor().setResource(key.getTenantIdentifier(), RESOURCE_KEY,
new StorageLayer(resourceKeyToStorageMap.get(key)));

userPoolsInUse.add(userPoolId);
uniquePoolsInUse.add(uniqueId);
}

for (ResourceDistributor.KeyClass key : existingStorageMap.keySet()) {
if (!userPoolsInUse.contains(((StorageLayer) existingStorageMap.get(key)).storage.getUserPoolId())) {
Storage existingStorage = ((StorageLayer) existingStorageMap.get(key)).storage;
String userPoolId = existingStorage.getUserPoolId();
String connectionPoolId = existingStorage.getConnectionPoolId();
String uniqueId = userPoolId + "~" + connectionPoolId;

if (!uniquePoolsInUse.contains(uniqueId)) {
((StorageLayer) existingStorageMap.get(key)).storage.close();
((StorageLayer) existingStorageMap.get(key)).storage.stopLogging();
}
Expand Down
28 changes: 17 additions & 11 deletions src/main/java/io/supertokens/webserver/WebserverAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.supertokens.config.CoreConfig;
import io.supertokens.exceptions.QuitProgramException;
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
import io.supertokens.multitenancy.MultitenancyHelper;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.output.Logging;
import io.supertokens.pluginInterface.Storage;
Expand Down Expand Up @@ -286,15 +287,18 @@ private String getConnectionUriDomain(HttpServletRequest req) throws ServletExce
String connectionUriDomain = req.getServerName();
connectionUriDomain = Utils.normalizeAndValidateConnectionUriDomain(connectionUriDomain, false);

try {
if (Config.getConfig(new TenantIdentifier(connectionUriDomain, null, null), main) ==
Config.getConfig(new TenantIdentifier(null, null, null), main)) {
return null;
if (MultitenancyHelper.getInstance(main).isConnectionUriDomainPresentInDb(connectionUriDomain)) {
CoreConfig baseConfig = Config.getBaseConfig(main);
if (baseConfig.getSuperTokensLoadOnlyCUD() != null) {
if (!connectionUriDomain.equals(baseConfig.getSuperTokensLoadOnlyCUD())) {
throw new ServletException(new BadRequestException("Connection URI domain is disallowed"));
}
}
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);

return connectionUriDomain;
}
return connectionUriDomain;

return null;
}

@TestOnly
Expand Down Expand Up @@ -480,10 +484,12 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
}
Logging.info(main, tenantIdentifier, "API ended: " + req.getRequestURI() + ". Method: " + req.getMethod(),
false);
try {
RequestStats.getInstance(main, tenantIdentifier.toAppIdentifier()).updateRequestStats();
} catch (TenantOrAppNotFoundException e) {
// Ignore the error as we would have already sent the response for tenantNotFound
if (tenantIdentifier != null) {
try {
RequestStats.getInstance(main, tenantIdentifier.toAppIdentifier()).updateRequestStats();
} catch (TenantOrAppNotFoundException e) {
// Ignore the error as we would have already sent the response for tenantNotFound
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.config.Config;
import io.supertokens.config.CoreConfig;
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.multitenancy.exception.BadPermissionException;
Expand Down Expand Up @@ -49,6 +51,14 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent
HttpServletResponse resp)
throws ServletException, IOException {

CoreConfig baseConfig = Config.getBaseConfig(main);
if (baseConfig.getSuperTokensLoadOnlyCUD() != null) {
if (!(targetTenantIdentifier.getConnectionUriDomain().equals(TenantIdentifier.DEFAULT_CONNECTION_URI) || targetTenantIdentifier.getConnectionUriDomain().equals(baseConfig.getSuperTokensLoadOnlyCUD()))) {
throw new ServletException(new BadRequestException("Creation of connection uri domain or app or " +
"tenant is disallowed"));
}
}

TenantConfig tenantConfig = Multitenancy.getTenantInfo(main,
new TenantIdentifier(targetTenantIdentifier.getConnectionUriDomain(), targetTenantIdentifier.getAppId(),
targetTenantIdentifier.getTenantId()));
Expand Down
48 changes: 37 additions & 11 deletions src/test/java/io/supertokens/test/PathRouterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1533,7 +1533,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
public void tenantNotFoundTest3()
throws InterruptedException, IOException, io.supertokens.httpRequest.HttpResponseException,
InvalidConfigException,
io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException {
io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException,
InvalidProviderConfigException, StorageQueryException, FeatureNotEnabledException,
CannotModifyBaseConfigException, BadPermissionException {
String[] args = {"../"};

Utils.setValueInConfig("host", "\"0.0.0.0\"");
Expand All @@ -1556,15 +1558,26 @@ public void tenantNotFoundTest3()
StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())
.modifyConfigToAddANewUserPoolForTesting(tenantConfig, 2);

Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{
new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false),
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantConfig(
new TenantIdentifier("localhost", null, null),
new EmailPasswordConfig(false),
new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]),
new PasswordlessConfig(false),
tenantConfig),
new TenantConfig(new TenantIdentifier("localhost", null, "t1"), new EmailPasswordConfig(false),
false
);
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantConfig(
new TenantIdentifier("localhost", null, "t1"),
new EmailPasswordConfig(false),
new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]),
new PasswordlessConfig(false),
tenantConfig)}, new ArrayList<>());
tenantConfig),
false
);

Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") {

Expand Down Expand Up @@ -2788,7 +2801,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
public void tenantNotFoundWithAppIdTest3()
throws InterruptedException, IOException, io.supertokens.httpRequest.HttpResponseException,
InvalidConfigException,
io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException {
io.supertokens.test.httpRequest.HttpResponseException, TenantOrAppNotFoundException,
InvalidProviderConfigException, StorageQueryException, FeatureNotEnabledException,
CannotModifyBaseConfigException, BadPermissionException {
String[] args = {"../"};

Utils.setValueInConfig("host", "\"0.0.0.0\"");
Expand All @@ -2811,15 +2826,26 @@ public void tenantNotFoundWithAppIdTest3()
StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())
.modifyConfigToAddANewUserPoolForTesting(tenantConfig, 2);

Config.loadAllTenantConfig(process.getProcess(), new TenantConfig[]{
new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false),
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantConfig(
new TenantIdentifier("localhost", null, null),
new EmailPasswordConfig(false),
new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]),
new PasswordlessConfig(false),
tenantConfig),
new TenantConfig(new TenantIdentifier("localhost", "app1", "t1"), new EmailPasswordConfig(false),
false
);
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantConfig(
new TenantIdentifier("localhost", "app1", "t1"),
new EmailPasswordConfig(false),
new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]),
new PasswordlessConfig(false),
tenantConfig)}, new ArrayList<>());
tenantConfig),
false
);

Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") {

Expand Down Expand Up @@ -2875,4 +2901,4 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
}
}
Loading
Loading