diff --git a/CHANGELOG.md b/CHANGELOG.md index c95b4107..a1286525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [6.2.0] - 2024-05-24 + +- Adds new class `ConfigFieldInfo` that represents a core config field +- Adds new method `getPluginConfigFieldsInfo` to fetch the plugin config as json in `DashboardStorage` +- Updates `TenantConfig` to support `null` and empty array state for `firstFactors` +- Update `ThirdPartyConfig` to support `null` and empty array state for `providers` + ## [6.1.0] - 2024-04-17 - Adds `addTenantIdentifier` to the storage interface. diff --git a/build.gradle b/build.gradle index 2158e809..48fc30d8 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "6.1.0" +version = "6.2.0" repositories { mavenCentral() diff --git a/src/main/java/io/supertokens/pluginInterface/ConfigFieldInfo.java b/src/main/java/io/supertokens/pluginInterface/ConfigFieldInfo.java new file mode 100644 index 00000000..914506b3 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/ConfigFieldInfo.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ +package io.supertokens.pluginInterface; + +import com.google.gson.JsonElement; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class ConfigFieldInfo { + @Nonnull + public String key; + + @Nonnull + public String valueType; + + Object value; + + @Nonnull + public String description; + + public boolean isDifferentAcrossTenants; + + @Nullable + public String[] possibleValues; + + public boolean isNullable; + + public Object defaultValue; + + public boolean isPluginProperty; + + public boolean isPluginPropertyEditable; + + public ConfigFieldInfo(@Nonnull String key, @Nonnull String valueType, JsonElement value, @Nonnull String description, + boolean isDifferentAcrossTenants, + @Nullable String[] possibleValues, boolean isNullable, Object defaultValue, + boolean isPluginProperty, boolean isPluginPropertyEditable) { + this.key = key; + this.valueType = valueType; + this.value = value; + this.description = description; + this.isDifferentAcrossTenants = isDifferentAcrossTenants; + this.possibleValues = possibleValues; + this.isNullable = isNullable; + this.defaultValue = defaultValue; + this.isPluginProperty = isPluginProperty; + this.isPluginPropertyEditable = isPluginPropertyEditable; + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardStorage.java b/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardStorage.java index 83c85cf6..0fb2e2a5 100644 --- a/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardStorage.java @@ -1,5 +1,6 @@ package io.supertokens.pluginInterface.dashboard; +import io.supertokens.pluginInterface.ConfigFieldInfo; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.dashboard.exceptions.DuplicateUserIdException; @@ -8,6 +9,8 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import java.util.List; + public interface DashboardStorage extends Storage { void createNewDashboardUser(AppIdentifier appIdentifier, DashboardUser userInfo) @@ -35,4 +38,6 @@ DashboardSessionInfo getSessionInfoWithSessionId(AppIdentifier appIdentifier, St // this function removes based on expired time, so we can use this to globally remove from a particular db. void revokeExpiredSessions() throws StorageQueryException; + + List getPluginConfigFieldsInfo(); } diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantConfig.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantConfig.java index 70900423..4de5119b 100644 --- a/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantConfig.java +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantConfig.java @@ -17,6 +17,7 @@ package io.supertokens.pluginInterface.multitenancy; import com.google.gson.Gson; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; import io.supertokens.pluginInterface.Storage; @@ -24,6 +25,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.List; public class TenantConfig { @@ -56,27 +58,34 @@ public class TenantConfig { public TenantConfig(@Nonnull TenantIdentifier tenantIdentifier, @Nonnull EmailPasswordConfig emailPasswordConfig, @Nonnull ThirdPartyConfig thirdPartyConfig, @Nonnull PasswordlessConfig passwordlessConfig, - @Nullable String[] firstFactors, @Nullable String[] requiredSecondaryFactors, + @Nullable String[] firstFactors, + @Nullable String[] requiredSecondaryFactors, @Nullable JsonObject coreConfig) { this.tenantIdentifier = tenantIdentifier; this.coreConfig = coreConfig == null ? new JsonObject() : coreConfig; this.emailPasswordConfig = emailPasswordConfig; this.passwordlessConfig = passwordlessConfig; this.thirdPartyConfig = thirdPartyConfig; - this.firstFactors = firstFactors == null || firstFactors.length == 0 ? null : firstFactors; - this.requiredSecondaryFactors = requiredSecondaryFactors == null || requiredSecondaryFactors.length == 0 ? null : requiredSecondaryFactors; + this.firstFactors = firstFactors; + this.requiredSecondaryFactors = + requiredSecondaryFactors == null || requiredSecondaryFactors.length == 0 ? null : + requiredSecondaryFactors; } public TenantConfig(TenantConfig other) { // copy constructor, that does a deep copy Gson gson = new Gson(); - this.tenantIdentifier = new TenantIdentifier(other.tenantIdentifier.getConnectionUriDomain(), other.tenantIdentifier.getAppId(), other.tenantIdentifier.getTenantId()); + this.tenantIdentifier = new TenantIdentifier(other.tenantIdentifier.getConnectionUriDomain(), + other.tenantIdentifier.getAppId(), other.tenantIdentifier.getTenantId()); this.coreConfig = gson.fromJson(other.coreConfig.toString(), JsonObject.class); this.emailPasswordConfig = new EmailPasswordConfig(other.emailPasswordConfig.enabled); this.passwordlessConfig = new PasswordlessConfig(other.passwordlessConfig.enabled); - this.thirdPartyConfig = new ThirdPartyConfig(other.thirdPartyConfig.enabled, other.thirdPartyConfig.providers.clone()); + this.thirdPartyConfig = new ThirdPartyConfig( + other.thirdPartyConfig.enabled, + other.thirdPartyConfig.providers != null ? other.thirdPartyConfig.providers.clone() : null); this.firstFactors = other.firstFactors == null ? null : other.firstFactors.clone(); - this.requiredSecondaryFactors = other.requiredSecondaryFactors == null ? null : other.requiredSecondaryFactors.clone(); + this.requiredSecondaryFactors = + other.requiredSecondaryFactors == null ? null : other.requiredSecondaryFactors.clone(); } public boolean deepEquals(TenantConfig other) { @@ -106,20 +115,105 @@ public int hashCode() { return tenantIdentifier.hashCode(); } - public JsonObject toJson(boolean shouldProtectDbConfig, Storage storage, String[] protectedCoreConfigs) { + public JsonObject toJson3_0(boolean shouldProtectDbConfig, Storage storage, String[] protectedCoreConfigs) { + JsonObject tenantConfigObject = toJson5_0(shouldProtectDbConfig, storage, protectedCoreConfigs); + + // as per https://github.com/supertokens/supertokens-core/issues/979#issuecomment-2099971371 + tenantConfigObject.get("emailPassword").getAsJsonObject().addProperty( + "enabled", + (this.firstFactors == null && this.emailPasswordConfig.enabled) || + (this.firstFactors != null && List.of(this.firstFactors).contains("emailpassword")) + ); + tenantConfigObject.get("thirdParty").getAsJsonObject().addProperty( + "enabled", + (this.firstFactors == null && this.thirdPartyConfig.enabled) || + (this.firstFactors != null && List.of(this.firstFactors).contains("thirdparty")) + ); + tenantConfigObject.get("passwordless").getAsJsonObject().addProperty( + "enabled", + (this.firstFactors == null && this.passwordlessConfig.enabled) || + (this.firstFactors != null && + (List.of(this.firstFactors).contains("otp-email") || + List.of(this.firstFactors).contains("otp-phone") || + List.of(this.firstFactors).contains("link-email") || + List.of(this.firstFactors).contains("link-phone"))) + ); + + tenantConfigObject.remove("firstFactors"); + tenantConfigObject.remove("requiredSecondaryFactors"); + + return tenantConfigObject; + } + + public JsonObject toJson5_0(boolean shouldProtectDbConfig, Storage storage, String[] protectedCoreConfigs) { + Gson gson = new Gson(); JsonObject tenantConfigObject = gson.toJsonTree(this).getAsJsonObject(); tenantConfigObject.add("thirdParty", this.thirdPartyConfig.toJson()); tenantConfigObject.addProperty("tenantId", this.tenantIdentifier.getTenantId()); + if (shouldProtectDbConfig) { + String[] protectedConfigs = storage.getProtectedConfigsFromSuperTokensSaaSUsers(); + for (String config : protectedConfigs) { + if (tenantConfigObject.get("coreConfig").getAsJsonObject().has(config)) { + tenantConfigObject.get("coreConfig").getAsJsonObject().remove(config); + } + } + + for (String config : protectedCoreConfigs) { + if (tenantConfigObject.get("coreConfig").getAsJsonObject().has(config)) { + tenantConfigObject.get("coreConfig").getAsJsonObject().remove(config); + } + } + } + + if (!tenantConfigObject.get("thirdParty").getAsJsonObject().has("providers")) { + tenantConfigObject.get("thirdParty").getAsJsonObject().add("providers", new JsonArray()); + } + + // as per https://github.com/supertokens/supertokens-core/issues/979#issuecomment-2099971371 + tenantConfigObject.get("emailPassword").getAsJsonObject().addProperty( + "enabled", + (this.firstFactors == null && this.emailPasswordConfig.enabled) || + (this.firstFactors != null && List.of(this.firstFactors).contains("emailpassword")) || + (this.requiredSecondaryFactors != null && List.of(this.requiredSecondaryFactors).contains("emailpassword")) + ); + tenantConfigObject.get("thirdParty").getAsJsonObject().addProperty( + "enabled", + (this.firstFactors == null && this.thirdPartyConfig.enabled) || + (this.firstFactors != null && List.of(this.firstFactors).contains("thirdparty")) || + (this.requiredSecondaryFactors != null && List.of(this.requiredSecondaryFactors).contains("thirdparty")) + ); + tenantConfigObject.get("passwordless").getAsJsonObject().addProperty( + "enabled", + (this.firstFactors == null && this.passwordlessConfig.enabled) || + (this.firstFactors != null && + (List.of(this.firstFactors).contains("otp-email") || + List.of(this.firstFactors).contains("otp-phone") || + List.of(this.firstFactors).contains("link-email") || + List.of(this.firstFactors).contains("link-phone"))) || + (this.requiredSecondaryFactors != null && + (List.of(this.requiredSecondaryFactors).contains("otp-email") || + List.of(this.requiredSecondaryFactors).contains("otp-phone") || + List.of(this.requiredSecondaryFactors).contains("link-email") || + List.of(this.requiredSecondaryFactors).contains("link-phone"))) + ); + if (tenantConfigObject.has("firstFactors") && tenantConfigObject.get("firstFactors").getAsJsonArray().size() == 0) { tenantConfigObject.remove("firstFactors"); } - if (tenantConfigObject.has("requiredSecondaryFactors") && tenantConfigObject.get("requiredSecondaryFactors").getAsJsonArray().size() == 0) { - tenantConfigObject.remove("requiredSecondaryFactors"); - } + return tenantConfigObject; + } + + public JsonObject toJson_v2(boolean shouldProtectDbConfig, Storage storage, String[] protectedCoreConfigs) { + + Gson gson = new Gson(); + JsonObject tenantConfigObject = gson.toJsonTree(this).getAsJsonObject(); + + tenantConfigObject.add("thirdParty", this.thirdPartyConfig.toJson()); + tenantConfigObject.addProperty("tenantId", this.tenantIdentifier.getTenantId()); if (shouldProtectDbConfig) { String[] protectedConfigs = storage.getProtectedConfigsFromSuperTokensSaaSUsers(); @@ -136,6 +230,42 @@ public JsonObject toJson(boolean shouldProtectDbConfig, Storage storage, String[ } } + // as per https://github.com/supertokens/supertokens-core/issues/979#issuecomment-2099971371 + tenantConfigObject.remove("emailPassword"); + tenantConfigObject.remove("passwordless"); + tenantConfigObject.get("thirdParty").getAsJsonObject().remove("enabled"); + return tenantConfigObject; } + + public boolean isEmailPasswordEnabled() { + return this.emailPasswordConfig.enabled || + this.firstFactors == null || + (this.firstFactors != null && List.of(this.firstFactors).contains("emailpassword")) || + (this.requiredSecondaryFactors != null && List.of(this.requiredSecondaryFactors).contains( + "emailpassword")); + } + + public boolean isThirdPartyEnabled() { + return this.thirdPartyConfig.enabled || + this.firstFactors == null || + (this.firstFactors != null && List.of(this.firstFactors).contains("thirdparty")) || + (this.requiredSecondaryFactors != null && List.of(this.requiredSecondaryFactors).contains( + "thirdparty")); + } + + public boolean isPasswordlessEnabled() { + return this.passwordlessConfig.enabled || + this.firstFactors == null || + (this.firstFactors != null && + (List.of(this.firstFactors).contains("otp-email") || + List.of(this.firstFactors).contains("otp-phone") || + List.of(this.firstFactors).contains("link-email") || + List.of(this.firstFactors).contains("link-phone"))) || + (this.requiredSecondaryFactors != null && + (List.of(this.requiredSecondaryFactors).contains("otp-email") || + List.of(this.requiredSecondaryFactors).contains("otp-phone") || + List.of(this.requiredSecondaryFactors).contains("link-email") || + List.of(this.requiredSecondaryFactors).contains("link-phone"))); + } } diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/ThirdPartyConfig.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/ThirdPartyConfig.java index 9ee2352b..8c0fd7f9 100644 --- a/src/main/java/io/supertokens/pluginInterface/multitenancy/ThirdPartyConfig.java +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/ThirdPartyConfig.java @@ -29,21 +29,24 @@ public class ThirdPartyConfig { public final boolean enabled; - @Nonnull + @Nullable public final Provider[] providers; public ThirdPartyConfig(boolean enabled, @Nullable Provider[] providers) { this.enabled = enabled; - this.providers = providers == null ? new Provider[0] : providers; + this.providers = providers; } public JsonObject toJson() { JsonObject result = new JsonObject(); result.addProperty("enabled", this.enabled); - result.add("providers", new JsonArray()); - for (Provider provider : this.providers) { - result.getAsJsonArray("providers").add(provider.toJson()); + if (this.providers != null) { + result.add("providers", new JsonArray()); + + for (Provider provider : this.providers) { + result.getAsJsonArray("providers").add(provider.toJson()); + } } return result;