From 61a12de3440955bc3131fa7d695bee53b0d5acdb Mon Sep 17 00:00:00 2001 From: Kumar Shivendu Date: Mon, 27 Mar 2023 19:20:40 +0530 Subject: [PATCH 01/12] feat: Define TOTP recipe plugin interface (#53) * feat: Define TOTP recipe plugin interface * fix: Improve TOTPStorage * fix: Changes to TOTPStorage * chores: Mention TOTP recipe in CHANGELOG * fix(totp): Inherit from Exception instead of EmailPasswordException * fix: Remove TotpNotEnabledException wherever its not possible to throw it * fix: Update the order of init params for TOTPDevice * feat: Add optional deviceName to TOTPUsedCode * refactor: markDevicesAsVerified should return boolean * feat: Introduce equals for clean comparison in tests and rename getUsedCodes for clarity * feat: Add javadocs for TOTPStorage and createdTime for TOTPUsedCode * feat: Add method to delete all the data for user * refactor: Improve interfaces and javadocs * feat: Improve TOTP interface - Split device deletion into SQL transactions - Improve variables and return types - Make the storage to return all used codes * refactor: Remove deleteAllTotpDataForUser of TOTPStorage * refactor: Update getAllUsedCodes method name * feat: Improved TOTP interface These changes will support the following features in the core: - Remove expired codes only after cooldown - Allow retries while inserting used codes * refactor: insertUsedCode and getAllusedCodes should be part of a transaction * refactor: Improve TOTP related transactions * feat: Add plugin interface for active users storage layer (#62) * feat: Add plugin interface for active users storage layer * chores: Mention active users storage interface in CHANGELOG * refactor: Throw error in storage Layer and later suppress them directly in API layer * feat: Add more methods to active user storage interface for usage stats --- CHANGELOG.md | 3 ++ .../pluginInterface/ActiveUsersStorage.java | 17 +++++++++ .../pluginInterface/RECIPE_ID.java | 2 +- .../pluginInterface/totp/TOTPDevice.java | 36 +++++++++++++++++++ .../pluginInterface/totp/TOTPStorage.java | 29 +++++++++++++++ .../pluginInterface/totp/TOTPUsedCode.java | 34 ++++++++++++++++++ .../DeviceAlreadyExistsException.java | 5 +++ .../exception/TotpNotEnabledException.java | 5 +++ .../exception/UnknownDeviceException.java | 5 +++ .../UsedCodeAlreadyExistsException.java | 5 +++ .../totp/sqlStorage/TOTPSQLStorage.java | 34 ++++++++++++++++++ 11 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java create mode 100644 src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java create mode 100644 src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java create mode 100644 src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java create mode 100644 src/main/java/io/supertokens/pluginInterface/totp/exception/DeviceAlreadyExistsException.java create mode 100644 src/main/java/io/supertokens/pluginInterface/totp/exception/TotpNotEnabledException.java create mode 100644 src/main/java/io/supertokens/pluginInterface/totp/exception/UnknownDeviceException.java create mode 100644 src/main/java/io/supertokens/pluginInterface/totp/exception/UsedCodeAlreadyExistsException.java create mode 100644 src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d389d895..568e70ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Introduce TOTP Recipe plugin interface +- Introduce Active users storage plugin interface + ## [2.20.0] - 2023-02-21 - Dashboard Recipe Interface diff --git a/src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java b/src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java new file mode 100644 index 00000000..ed979ef8 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java @@ -0,0 +1,17 @@ +package io.supertokens.pluginInterface; + +import io.supertokens.pluginInterface.exceptions.StorageQueryException; + +public interface ActiveUsersStorage extends Storage { + /* Update the last active time of a user to now */ + void updateLastActive(String userId) throws StorageQueryException; + + /* Count the number of users who did some activity after given timestamp */ + int countUsersActiveSince(long time) throws StorageQueryException; + + /* Count the number of users who have enabled TOTP */ + int countUsersEnabledTotp() throws StorageQueryException; + + /* Count the number of users who have enabled TOTP and are active */ + int countUsersEnabledTotpAndActiveSince(long time) throws StorageQueryException; +} diff --git a/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java b/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java index 2e4aa263..7456f85f 100644 --- a/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java +++ b/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java @@ -21,7 +21,7 @@ public enum RECIPE_ID { EMAIL_PASSWORD("emailpassword"), THIRD_PARTY("thirdparty"), SESSION("session"), EMAIL_VERIFICATION("emailverification"), JWT("jwt"), PASSWORDLESS("passwordless"), USER_METADATA("usermetadata"), - USER_ROLES("userroles"), USER_ID_MAPPING("useridmapping"), DASHBOARD("dashboard"); + USER_ROLES("userroles"), USER_ID_MAPPING("useridmapping"), DASHBOARD("dashboard"), TOTP("totp"); private final String name; diff --git a/src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java b/src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java new file mode 100644 index 00000000..5709efc4 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java @@ -0,0 +1,36 @@ +package io.supertokens.pluginInterface.totp; + +public class TOTPDevice { + public final String deviceName; + public final String userId; + public final String secretKey; + public final int period; + public final int skew; + public final boolean verified; + + public TOTPDevice(String userId, String deviceName, String secretKey, int period, int skew, boolean verified) { + this.userId = userId; + this.deviceName = deviceName; + this.secretKey = secretKey; + this.period = period; + this.skew = skew; + this.verified = verified; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof TOTPDevice)) { + return false; + } + TOTPDevice other = (TOTPDevice) obj; + return this.userId.equals(other.userId) && this.deviceName.equals(other.deviceName) + && this.secretKey.equals(other.secretKey) && this.period == other.period && this.skew == other.skew + && this.verified == other.verified; + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java b/src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java new file mode 100644 index 00000000..3618de21 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java @@ -0,0 +1,29 @@ +package io.supertokens.pluginInterface.totp; + +import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; +import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; + +public interface TOTPStorage extends NonAuthRecipeStorage { + /** Create a new device and a new user if the user does not exist: */ + void createDevice(TOTPDevice device) + throws StorageQueryException, DeviceAlreadyExistsException; + + /** Verify a user's device with the given name: */ + void markDeviceAsVerified(String userId, String deviceName) + throws StorageQueryException, UnknownDeviceException; + + /** Update device name of a device: */ + void updateDeviceName(String userId, String oldDeviceName, String newDeviceName) + throws StorageQueryException, DeviceAlreadyExistsException, + UnknownDeviceException; + + /** Get the devices for a user */ + TOTPDevice[] getDevices(String userId) + throws StorageQueryException; + + /** Remove expired codes from totp used codes for all users: */ + int removeExpiredCodes(long expiredBefore) + throws StorageQueryException; +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java b/src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java new file mode 100644 index 00000000..d2fb9a74 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java @@ -0,0 +1,34 @@ +package io.supertokens.pluginInterface.totp; + +public class TOTPUsedCode { + public final String userId; + public final String code; + public final boolean isValid; + public final long expiryTime; + public final long createdTime; + + public TOTPUsedCode(String userId, String code, Boolean isValidCode, long expiryTime, long createdTime) { + this.userId = userId; + this.code = code; + this.isValid = isValidCode; + this.expiryTime = expiryTime; + this.createdTime = createdTime; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof TOTPUsedCode)) { + return false; + } + TOTPUsedCode other = (TOTPUsedCode) obj; + return this.userId.equals(other.userId) && this.code.equals(other.code) + && this.isValid == other.isValid && this.expiryTime == other.expiryTime + && this.createdTime == other.createdTime; + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/exception/DeviceAlreadyExistsException.java b/src/main/java/io/supertokens/pluginInterface/totp/exception/DeviceAlreadyExistsException.java new file mode 100644 index 00000000..edf61122 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/exception/DeviceAlreadyExistsException.java @@ -0,0 +1,5 @@ +package io.supertokens.pluginInterface.totp.exception; + +public class DeviceAlreadyExistsException extends Exception { + private static final long serialVersionUID = 6848053563771647272L; +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/exception/TotpNotEnabledException.java b/src/main/java/io/supertokens/pluginInterface/totp/exception/TotpNotEnabledException.java new file mode 100644 index 00000000..bb702ef5 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/exception/TotpNotEnabledException.java @@ -0,0 +1,5 @@ +package io.supertokens.pluginInterface.totp.exception; + +public class TotpNotEnabledException extends Exception { + private static final long serialVersionUID = 6848053563771647272L; +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/exception/UnknownDeviceException.java b/src/main/java/io/supertokens/pluginInterface/totp/exception/UnknownDeviceException.java new file mode 100644 index 00000000..86bf659a --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/exception/UnknownDeviceException.java @@ -0,0 +1,5 @@ +package io.supertokens.pluginInterface.totp.exception; + +public class UnknownDeviceException extends Exception { + private static final long serialVersionUID = 6848053563771647272L; +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/exception/UsedCodeAlreadyExistsException.java b/src/main/java/io/supertokens/pluginInterface/totp/exception/UsedCodeAlreadyExistsException.java new file mode 100644 index 00000000..7ada2a2f --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/exception/UsedCodeAlreadyExistsException.java @@ -0,0 +1,5 @@ +package io.supertokens.pluginInterface.totp.exception; + +public class UsedCodeAlreadyExistsException extends Exception { + +} diff --git a/src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java new file mode 100644 index 00000000..d9261de4 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java @@ -0,0 +1,34 @@ +package io.supertokens.pluginInterface.totp.sqlStorage; + +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.sqlStorage.SQLStorage; +import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; +import io.supertokens.pluginInterface.totp.TOTPDevice; +import io.supertokens.pluginInterface.totp.TOTPStorage; +import io.supertokens.pluginInterface.totp.TOTPUsedCode; +import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; +import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; + +public interface TOTPSQLStorage extends TOTPStorage, SQLStorage { + public int deleteDevice_Transaction(TransactionConnection con, String userId, String deviceName) + throws StorageQueryException; + + public TOTPDevice[] getDevices_Transaction(TransactionConnection con, String userId) + throws StorageQueryException; + + public void removeUser_Transaction(TransactionConnection con, String userId) + throws StorageQueryException; + + /** + * Get totp used codes for user (expired/non-expired) yet (sorted by descending + * order of created time): + */ + public TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(TransactionConnection con, + String userId) + throws StorageQueryException; + + /** Insert a used TOTP code for an existing user: */ + void insertUsedCode_Transaction(TransactionConnection con, TOTPUsedCode code) + throws StorageQueryException, TotpNotEnabledException, UsedCodeAlreadyExistsException; + +} From 431fdf710beede15b8a169fe984e462d8aa18da5 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 27 Mar 2023 21:53:30 +0530 Subject: [PATCH 02/12] updates version --- CHANGELOG.md | 2 ++ build.gradle | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568e70ec..f3c479e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [2.21.0] - 2023-03-27 + - Introduce TOTP Recipe plugin interface - Introduce Active users storage plugin interface diff --git a/build.gradle b/build.gradle index 575d0fdd..f7299704 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "2.20.0" +version = "2.21.0" repositories { mavenCentral() From 054d04d9e4d37ff25ebd418538aa66cf09a47ac8 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 27 Mar 2023 22:07:47 +0530 Subject: [PATCH 03/12] adding dev-v2.21.0 tag to this commit to ensure building --- ...2.20.0.jar => plugin-interface-2.21.0.jar} | Bin 58735 -> 64624 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{plugin-interface-2.20.0.jar => plugin-interface-2.21.0.jar} (65%) diff --git a/jar/plugin-interface-2.20.0.jar b/jar/plugin-interface-2.21.0.jar similarity index 65% rename from jar/plugin-interface-2.20.0.jar rename to jar/plugin-interface-2.21.0.jar index 5e1be93f3d953f73d506cbfe5352a7f75e2bf3bc..98a3a9f2d19a440493ada68523c673e31478b21a 100644 GIT binary patch delta 10418 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 delta 5677 zcmZu#2|ShA8|L1~F6Bx-L}QuA9zu$ewQI`~5}J%HTcyo%Ll}~!Gbu%uNTlpF)1pYI&5xzqplyPdD+Jn!=^=bZO^x5-b`i54oqwFPwv4;vd77n}BR z`&528DmiGG%V{0AcVL4b3i%iOP~q1nP=(62XaFgxm)9YK?v3OCUKpx^b}A3BWap#8 z*$P+oadtF8r&^O^MK(1T0TtUg;_yQoXEdhh$6(uJu9b*d$Tf#2uJ9Nj;0;eQ1&M|8 zk0Zce;1*`wb5{j|ErMX92_Kp`vg{-l=UgsGMJA#&I-2pOJz6wTc_;{+XxJofAPf8 zF)}IFjUSqRBq#z<(&3?>Qe*8BHns;`U`j!Y;o`W$QTSWzpydeHD=97>=R$L?Jw@HS zmlUQhSw(QQ*7tfV>dR0CA99`^0M;OxievAS>0ac&+pB}^3cz1?Fmfd*p!!R{(&Vxp+4VsKiRZ(gkebBe-Mg}eI2Zbaai!II!^KH7)U3Cgc+#$(!Z+_389UIBL z6GZPFvJ2kQJMw$6+Tp5mO3fc{=a0x(+I*H*?5jO>JI8(Q<2M4_`_Wb{!&WV}GRpMY zaIYNgS24xnsgR34L7|#EcJ0>i+cbNz#)!|$e($8#hnK74Za)pM<$QCyL+hj>kMiXk znUL6aVUTfIHLR!HQU0dMvL&bgxt(x0$k6a$OSuxm*^uEoV-{Y#MuFkj5PBp_r2YlH z#c5#toK%U0!nLQuYAs_MIaU;w{nIVQ(W$Vhp4Kn*XP?wiU-q>iN3|7OEw@Ql@SN6@ zF!dY?d>30#(x2q_C@A^y+ks2>%3Sz_I5S1=@7sU1BbOntDQNJP>5Csaawt!8s>Ldn z+Q&#gvd*Kwn{W=OZgBlaI=aO=H~#45QKzKV_PU_xkIu#}+OB>LMZs>)>tn6FJ9{KQ z@rCo<^m(A&>C^5L%O0zjw=eYa!20mrqno@owA}OA5%V~tDS23QYvyYqv8zLWUlZ}X z9~2eYzr5tY#9Kyrowh=yU2%MTgJnR{x{ZHz724}_JkVO%-|E?^^zJ#Q&*d54p>J`D zX^n+b#xZ}SN%8Hh>sp&o)UF+{^QzX&z*`2rXp5_hW_a1XitC+&;+5swtHd^yJiRb_ z?RXl!Tdc+*&UWNkR}!IG_0PkCwKFG%E1#a%@#CI;t2E$bc#QE}TyvVQdVjZK^vDP9 zl*FaB{ClmWsti7I?3YS0p&nhAB7Z_A@K%Vy^e;yymL&N+1k0l*=6`vI})yxnv(RPZI9uaF~FER@H0O+c^<&slZw-rkdo-NpwVAP}Eq_Wl4 zC@GIo-;h~&YM+JD^7e*df7*=ZQ4>>J&(*?>Zo38Of`Z}6!&AfW&elc`d;OLX5j%GC z7lGK-j1{&KpWX;Mg_#<^JS%T?>FiMZ{nXr@1+R4O*c+&vYAg69m+`)G9h-Nc@SNC` zs4>m+nU&FL?%?#Ou(zgx?po=eXy%$-hD}#u_1^b5o;v8ARn#Hi@2~m9@bQ&$fn%q9 zPuqQ!G1WGW`gNRs4X2g54*2oNp?Q}^|l^fn; z^krx5i4(?liWWz#Yt6U+;jq@jwpO7tz^qqsxS;vU$MpKI%T;85>K|P$Xns3muQPo_ zI=*%9E|=6#pLhqu6H?_}JiW{Hzn;72;oHw9X=CPE;}8}ZXB#oAy?9-ajZ;i@74DaW zo+>C+=l!m;9#?P2jd90%*pQ(Kus@>;QndKLW4deogb8Hpg#@77#R~*B;MOa#AqC5n z>Ud)op=W~ntV*6`k_$Tz)t@0A5X-Mkgqdh^$XvVIS?;@X^!ZUaDG0x3NLEY9J@ z2Br+AQE)E|jnTcLvPB(t_`OCcI6?9{b06dZS$(|Bd&$BCVEtj!`S1>!$6#d6;xv9p zrGtM9&_LT!oKmQD1!kz(Hez3&?V>Q%!^a#&aouMK1IhdB5iq*%S8Njz024wB5Ofwl zR0bE}2af}3_~G-xX8h22sCq$W6R;fRWri?xl!7P7HE|2$2U1};HkB|>WD3Y-&5r(l zS%F4o7PX;IEKD#)F^ZPSr2jXhx z&cCkoU&_F`NAu@{TUl_0otwP4B{rG!DFQ~mIZzy+fua0W6$XXknCw^hM!;XQZy7eRg8tOKauS;NQy)qg%)axQVq#kWWP+M<@@Kp`I2#;;pjmFW2 z7&Hbp9zo;l#zSbdc};+b`$p)w;Xa--XhKB-sZE{;pKaQUM)&3bGO68m={T3Ag=*QgP6?Ls3YzH$y?w) z*up}VT0gH$1GA6CP~dMy3Cgf0gk{0bP8x!OIthxB7J`1&Lb5p3M+60EBlxqnFi`q{ zz1^t2uwxDM_85BG!Q$;TCMR^^9|g@_1SMPt1@KHqT+rHrogz$S{^UTJztjaePu78b zf8k;zbQ2VHJqT%l{%%E72%xT`5dghaLvNNW-o9)^wm$kYz`c=gHCcHNA=4loNL~)f z+gQlmAo~g4m~ina!3HQQ*x*sY;eK}Ly`%g%1)6lhuL+CkodxoG6c9NF<fauy+-z?nyYMX2i6(Xtd(H3c<6%UJr!&wj#&e z?jla|Fu5hryA9JZxg|tRm@-*?ILaqWLycwrFZsIZzsS7cYcMxx4-jUSFiHA-l_WH* zh2QNgUJOBSKR#b={RE}i;yamVe@mmxtOf|mZaQ|t6Lg>C7f)wnR1GA^N^5imacLdVURY2wl zhKeDSj-fFh0r0wM{`5eH3k$k|>6~29W6scCm$^Gb{~{=lF{J7*PF4&B@D;*7yC8Gx zLLm`+{BMGt156FA0S2Q2!1}o`fb~HOIjPcCr3ACpgI@PA9g+)~f zg&@g}g|yuRls(^rNQSDC3Z9U(1Cq>GNYD46)Hj5PgNPS6&QmW4O4(in_q~uq_M{0k zMIYAGgS`;Za&#pk7Vf*_e;PXa2!E*B&l`|KUS#tEkr&(@U5hd&JIo^=v?AoYLc$oX zvv9kPJP*_mg&qx9G{k)8$)<%=bC%2{U zk@+;x0Ya|R#|TRDB0N5Z{yPV?SfyjYCUG5b8^`4d7$+!)F{JZW1hkGzA%RH61viKW9uxSGUYsB(iP50riZU2Fvv|(V zo*f?2hX;I`#9{ATfBt$F1qt~vt3c8hY&Wvl{(K2JtFGc@UJukzdaN}p*$mu0u1qr842$Ha zpYXQjpCu@#@Q1PEQ5crXV>S zw!FpcNV4n|QXP`<(IMMLU=f1Icj|?9CudcY(*f=Ogw9wuMco2f35a&!QV!JX_?JTR z7<9(E)i|%sD+`0bFSsfFyLRf#a*&1*qH!PaW$O5wb{btyw8z VZNb4wiXDZ2Ml{&i4wsSt`X96G51jx2 From dcf7a9155a78db4036d35ac85ba2546a00438625 Mon Sep 17 00:00:00 2001 From: Joel Coutinho Date: Fri, 31 Mar 2023 11:58:45 +0530 Subject: [PATCH 04/12] feat: adds dashboard search support (#60) * updates * fixes * fixes * changes * fixes * imeplementation changes * implementation changes * CHANGELOG and version updates --------- Co-authored-by: Rishabh Poddar --- CHANGELOG.md | 4 + build.gradle | 2 +- .../authRecipe/AuthRecipeStorage.java | 3 +- .../dashboard/DashboardSearchTags.java | 96 +++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 src/main/java/io/supertokens/pluginInterface/dashboard/DashboardSearchTags.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f3c479e5..16d02302 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [2.22.0] - 2023-03-30 + +- Adds Support for Dashboard Search + ## [2.21.0] - 2023-03-27 - Introduce TOTP Recipe plugin interface diff --git a/build.gradle b/build.gradle index f7299704..e4b086a7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "2.21.0" +version = "2.22.0" repositories { mavenCentral() diff --git a/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeStorage.java b/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeStorage.java index 48fbc962..591619b3 100644 --- a/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeStorage.java @@ -19,6 +19,7 @@ import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import javax.annotation.Nonnull; @@ -29,7 +30,7 @@ public interface AuthRecipeStorage extends Storage { long getUsersCount(@Nullable RECIPE_ID[] includeRecipeIds) throws StorageQueryException; AuthRecipeUserInfo[] getUsers(@Nonnull Integer limit, @Nonnull String timeJoinedOrder, - @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, @Nullable Long timeJoined) + @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, @Nullable Long timeJoined, @Nullable DashboardSearchTags dashboardSearchTags) throws StorageQueryException; boolean doesUserIdExist(String userId) throws StorageQueryException; diff --git a/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardSearchTags.java b/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardSearchTags.java new file mode 100644 index 00000000..833b1be8 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardSearchTags.java @@ -0,0 +1,96 @@ +package io.supertokens.pluginInterface.dashboard; + +import java.util.ArrayList; + +import javax.annotation.Nullable; + +public class DashboardSearchTags { + + public ArrayList emails; + public ArrayList phoneNumbers; + public ArrayList providers; + + public DashboardSearchTags(@Nullable ArrayList emails, @Nullable ArrayList phones, + @Nullable ArrayList providers) { + this.emails = emails; + this.phoneNumbers = phones; + this.providers = providers; + } + + public boolean shouldEmailPasswordTableBeSearched() { + + ArrayList nonNullSearchTags = getNonNullSearchFields(); + return nonNullSearchTags.contains(SUPPORTED_SEARCH_TAGS.EMAIL) && nonNullSearchTags.size() == 1; + + } + + public boolean shouldThirdPartyTableBeSearched() { + + ArrayList nonNullSearchTags = getNonNullSearchFields(); + if(nonNullSearchTags.contains(SUPPORTED_SEARCH_TAGS.EMAIL) && nonNullSearchTags.contains(SUPPORTED_SEARCH_TAGS.PROVIDER)){ + return nonNullSearchTags.size() == 2; + } + + if(nonNullSearchTags.contains(SUPPORTED_SEARCH_TAGS.EMAIL) || nonNullSearchTags.contains(SUPPORTED_SEARCH_TAGS.PROVIDER)){ + return nonNullSearchTags.size() == 1; + } + + return false; + } + + public boolean shouldPasswordlessTableBeSearched() { + ArrayList nonNullSearchTags = getNonNullSearchFields(); + if(nonNullSearchTags.contains(SUPPORTED_SEARCH_TAGS.EMAIL) && nonNullSearchTags.contains(SUPPORTED_SEARCH_TAGS.PHONE)){ + return nonNullSearchTags.size() == 2; + } + + if(nonNullSearchTags.contains(SUPPORTED_SEARCH_TAGS.EMAIL) || nonNullSearchTags.contains(SUPPORTED_SEARCH_TAGS.PHONE)){ + return nonNullSearchTags.size() == 1; + } + + return false; + } + + private ArrayList getNonNullSearchFields() { + ArrayList nonNullSearchTags = new ArrayList<>(); + + if (this.emails != null) { + nonNullSearchTags.add(SUPPORTED_SEARCH_TAGS.EMAIL); + } + + if (this.phoneNumbers != null) { + nonNullSearchTags.add(SUPPORTED_SEARCH_TAGS.PHONE); + } + + if (this.providers != null) { + nonNullSearchTags.add(SUPPORTED_SEARCH_TAGS.PROVIDER); + } + + return nonNullSearchTags; + } + + public enum SUPPORTED_SEARCH_TAGS { + EMAIL("email"), PHONE("phone"), PROVIDER("provider"); + + private String tag; + + SUPPORTED_SEARCH_TAGS(String tag) { + this.tag = tag; + } + + public static SUPPORTED_SEARCH_TAGS fromString(String text) { + for (SUPPORTED_SEARCH_TAGS t : SUPPORTED_SEARCH_TAGS.values()) { + if (t.tag.equalsIgnoreCase(text)) { + return t; + } + } + return null; + } + + @Override + public String toString() { + return tag; + } + } + +} From 1ad9c605b19851e32a887fa70d3b837bd6106c3a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 31 Mar 2023 12:11:50 +0530 Subject: [PATCH 05/12] adding dev-v2.22.0 tag to this commit to ensure building --- ...2.21.0.jar => plugin-interface-2.22.0.jar} | Bin 64624 -> 67132 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{plugin-interface-2.21.0.jar => plugin-interface-2.22.0.jar} (70%) diff --git a/jar/plugin-interface-2.21.0.jar b/jar/plugin-interface-2.22.0.jar similarity index 70% rename from jar/plugin-interface-2.21.0.jar rename to jar/plugin-interface-2.22.0.jar index 98a3a9f2d19a440493ada68523c673e31478b21a..2b9893e8eb79cc29c3bb2cc711c729c3de193c24 100644 GIT binary patch delta 7711 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 MX;pz$z7hNSKPBA5rT_o{ delta 5174 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 PlEnU1vhy(U4FBnWEdwCE From 089aff4c71b49def3fcea13a978d86245d232c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20Lengyel?= Date: Wed, 5 Apr 2023 16:02:06 +0200 Subject: [PATCH 06/12] feat: access token to jwt (#59) * feat: support changing access token to jwt * chore: udpate changelog and version * feat: mark useStaticKey as transient * fix: handle migrated static keys * chore: remove accidentally added file * chore: updated release date in changelog --- CHANGELOG.md | 5 +++++ build.gradle | 2 +- .../pluginInterface/jwt/JWTAsymmetricSigningKeyInfo.java | 2 +- .../pluginInterface/jwt/JWTSigningKeyInfo.java | 9 +++++++++ .../supertokens/pluginInterface/session/SessionInfo.java | 4 +++- .../pluginInterface/session/SessionStorage.java | 2 +- .../session/noSqlStorage/SessionInfoWithLastUpdated.java | 4 ++-- 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16d02302..0b6cfb59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [2.23.0] - 2023-04-05 + +- Added `useStaticKey ` into session info classes +- Added `hashCode` override for JWTSigningKeyInfo + ## [2.22.0] - 2023-03-30 - Adds Support for Dashboard Search diff --git a/build.gradle b/build.gradle index e4b086a7..96d08997 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "2.22.0" +version = "2.23.0" repositories { mavenCentral() diff --git a/src/main/java/io/supertokens/pluginInterface/jwt/JWTAsymmetricSigningKeyInfo.java b/src/main/java/io/supertokens/pluginInterface/jwt/JWTAsymmetricSigningKeyInfo.java index 0af9505d..f407c079 100644 --- a/src/main/java/io/supertokens/pluginInterface/jwt/JWTAsymmetricSigningKeyInfo.java +++ b/src/main/java/io/supertokens/pluginInterface/jwt/JWTAsymmetricSigningKeyInfo.java @@ -28,6 +28,6 @@ public JWTAsymmetricSigningKeyInfo(String keyId, long createdAtTime, String algo } public JWTAsymmetricSigningKeyInfo(String keyId, long createdAtTime, String algorithm, String keyString) { - this(keyId, createdAtTime, algorithm, keyString.split("\\|")[0], keyString.split("\\|")[1]); + this(keyId, createdAtTime, algorithm, keyString.split("[|;]")[0], keyString.split("[|;]")[1]); } } diff --git a/src/main/java/io/supertokens/pluginInterface/jwt/JWTSigningKeyInfo.java b/src/main/java/io/supertokens/pluginInterface/jwt/JWTSigningKeyInfo.java index a4f9495c..37c86755 100644 --- a/src/main/java/io/supertokens/pluginInterface/jwt/JWTSigningKeyInfo.java +++ b/src/main/java/io/supertokens/pluginInterface/jwt/JWTSigningKeyInfo.java @@ -45,4 +45,13 @@ public boolean equals(Object obj) { return false; } + + @Override + public int hashCode() { + int result = keyId.hashCode(); + result = 31 * result + (int) (createdAtTime ^ (createdAtTime >>> 32)); + result = 31 * result + algorithm.hashCode(); + result = 31 * result + keyString.hashCode(); + return result; + } } diff --git a/src/main/java/io/supertokens/pluginInterface/session/SessionInfo.java b/src/main/java/io/supertokens/pluginInterface/session/SessionInfo.java index 00b1f563..50b670e8 100644 --- a/src/main/java/io/supertokens/pluginInterface/session/SessionInfo.java +++ b/src/main/java/io/supertokens/pluginInterface/session/SessionInfo.java @@ -26,9 +26,10 @@ public class SessionInfo { public long expiry; public JsonObject userDataInJWT; public long timeCreated; + public transient boolean useStaticKey; public SessionInfo(String sessionHandle, String userId, String refreshTokenHash2, JsonObject userDataInDatabase, - long expiry, JsonObject userDataInJWT, long timeCreated) { + long expiry, JsonObject userDataInJWT, long timeCreated, boolean useStaticKey) { this.sessionHandle = sessionHandle; this.userId = userId; this.refreshTokenHash2 = refreshTokenHash2; @@ -36,5 +37,6 @@ public SessionInfo(String sessionHandle, String userId, String refreshTokenHash2 this.expiry = expiry; this.userDataInJWT = userDataInJWT; this.timeCreated = timeCreated; + this.useStaticKey = useStaticKey; } } diff --git a/src/main/java/io/supertokens/pluginInterface/session/SessionStorage.java b/src/main/java/io/supertokens/pluginInterface/session/SessionStorage.java index 638f2462..2449ca7c 100644 --- a/src/main/java/io/supertokens/pluginInterface/session/SessionStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/session/SessionStorage.java @@ -25,7 +25,7 @@ public interface SessionStorage extends NonAuthRecipeStorage { void createNewSession(String sessionHandle, String userId, String refreshTokenHash2, JsonObject userDataInDatabase, - long expiry, JsonObject userDataInJWT, long createdAtTime) throws StorageQueryException; + long expiry, JsonObject userDataInJWT, long createdAtTime, boolean useStaticKey) throws StorageQueryException; void deleteSessionsOfUser(String userId) throws StorageQueryException; diff --git a/src/main/java/io/supertokens/pluginInterface/session/noSqlStorage/SessionInfoWithLastUpdated.java b/src/main/java/io/supertokens/pluginInterface/session/noSqlStorage/SessionInfoWithLastUpdated.java index 0dddba71..d96be4d5 100644 --- a/src/main/java/io/supertokens/pluginInterface/session/noSqlStorage/SessionInfoWithLastUpdated.java +++ b/src/main/java/io/supertokens/pluginInterface/session/noSqlStorage/SessionInfoWithLastUpdated.java @@ -23,9 +23,9 @@ public class SessionInfoWithLastUpdated extends SessionInfo { public String lastUpdatedSign; public SessionInfoWithLastUpdated(String sessionHandle, String userId, String refreshTokenHash2, - JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, long timeCreated, + JsonObject userDataInDatabase, long expiry, JsonObject userDataInJWT, long timeCreated, boolean useStaticKey, String lastUpdatedSign) { - super(sessionHandle, userId, refreshTokenHash2, userDataInDatabase, expiry, userDataInJWT, timeCreated); + super(sessionHandle, userId, refreshTokenHash2, userDataInDatabase, expiry, userDataInJWT, timeCreated, useStaticKey); this.lastUpdatedSign = lastUpdatedSign; } } From e92a0a0694414c88183d055a3d2a543bb20563cd Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 5 Apr 2023 19:43:24 +0530 Subject: [PATCH 07/12] adding dev-v2.23.0 tag to this commit to ensure building --- ...2.22.0.jar => plugin-interface-2.23.0.jar} | Bin 67132 -> 67290 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename jar/{plugin-interface-2.22.0.jar => plugin-interface-2.23.0.jar} (70%) diff --git a/jar/plugin-interface-2.22.0.jar b/jar/plugin-interface-2.23.0.jar similarity index 70% rename from jar/plugin-interface-2.22.0.jar rename to jar/plugin-interface-2.23.0.jar index 2b9893e8eb79cc29c3bb2cc711c729c3de193c24..7edb243ecff36fc52e276504b1595b98e94074b6 100644 GIT binary patch delta 8382 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 delta 8344 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 Date: Fri, 2 Jun 2023 17:47:29 +0530 Subject: [PATCH 08/12] feat: Adds interface for multi tenancy (#50) * adds interface for multi tenancy * changes storage layer to take json instead of config file path * adds new interface to indentify a user pool * exception throwing change * adds function to get connection pool ID * changes to interface * changes to initstorage interface function * adds function so that the core can create multiple user pools during testing across dbs * adds tenantidentifier class * adds more functions to interface * removes unused exception * small change * adds new functions * adds deletion functions for multitenancy * few changes * updates exception class * simplifies delete of app and connectionuridomain * adds getters for certain tenant config props * adds equals functions for tenantconfig types * adds constructors for thirdparty config objects * changes equals for tenantconfig * removes nullable annotation from primitive type * adds tenantIdentifier for emailpassword and useridmapping recipes * adds extra comment * adds comment * changes to incorporate tenantIndetifier for key value storage * changes to session receipe to add tenantIdentifier * introduces the concept of appIdentifier vs tenantIdentifier * adds a few more functions * adds appidentifier to user metadata functions * modifes user roles functions to add tenantidentifier and appidentifiers * changes to emailpassword functions * changes to a few functions * adds appidentifier to email verfication * adds tenant identifier to third party * adds tenantidentifier to passwordless * function name changes * fix: changes for multi-tenancy impl (#55) * fix: pr comments * fix: pr comments * fix: new exceptions (#56) * fix: pr comments * fix: pr comments * fix: added few more exceptions * fix: pr comments * fix: userInfoMap consistent with db * fix: non null * fix: changes for random test (#57) * fix: pr comments * fix: pr comments * fix: added few more exceptions * fix: pr comments * fix: userInfoMap consistent with db * fix: non null * fix: update for random test * makes dashboard interface per app * fix: storage in AppIdentifier and TenantIdentifier (#61) * fix: storage in AppIdentifier and TenantIdentifier * fix: pr comments * fix: changes for delete user * fix: changes for delete user * fix: revert * fix: updated exception msg * fix: pr comment * fix: adding tenant or app not found exceptions * fix: adding tenant or app not found exceptions * fix: fixed pless interface (#64) * fix: to support PR comments on core (#65) * fix: from core pr comments * fix: TenantIdentifierWithStorage and AppIdentifierWithStorage * fix: made storage private * fix: added storages to appIdentifierWithStorage (#66) * fix: Multitenant userroles (#67) * fix: user roles impl * fix: handling fkey * Update src/main/java/io/supertokens/pluginInterface/userroles/sqlStorage/UserRolesSQLStorage.java --------- Co-authored-by: Rishabh Poddar * fix: Multitenant usermetadata (#68) * fix: user roles impl * fix: handling fkey * fix: usermetadata impl * fix: user metadata impl * fix: ep storage (#69) * fix: Multitenant uidmapping storage (#70) * fix: uid mapping storage * fix: SQL check * fix: Multitenant passwordless storage (#71) * fix: passwordless storage * fix: passwordless storage * fix: thirdparty storage (#72) * fix: Multitenant thirdparty changes for update email (#73) * fix: thirdparty storage * fix: thirdparty changes * fix: Multitenant emailverification storage (#74) * fix: thirdparty storage * fix: emailverification storage * fix: making tokens tenant specific (#75) * comment modification * fix: Multitenant session (#76) * fix: session changes * fix: session changes * fix: adding tenant or app not found exceptions * merges with latest (#77) * small change * fix: jwt changes (#78) * fix: Multitenant Authrecipe changes (#79) * fix: auth recipe storage * fix: added session storage * fix: Multitenant dashboard (#80) * fix: dashboard storage * fix: updated exception * fix: Multitenant totp (#81) * fix: removed unused throw * fix: handling fk * merges (#82) * adds new config (#83) * fix: multitenancy changes (#84) * fix: multitenancy * fix: interface update * fix: added db protection flag * fix: add userId to tenant * fix: updated as per CDI * fix: removed unused func * fix: protected fields * fix: pr comment * fix: better serialization * fix: base tenant (#85) * fix: Tenantid in userobjects (#86) * fix: adding tenant ids to user objects * fix: create user type * fix: refactor ep and tp * fix: refactor pless * fix: tenant id in loadConfig (#88) * fix: delete non auth user (#89) * fix: nonAuthRecipeuserData to take tenantIdentifier (#90) * fix: pr comment * fix: config validation (#91) * fix: reload resources (#93) * fix: added setLogLevels (#95) * fix: Active user storage to extend NonAuthRecipeStorage (#97) * fix: fixed base class * fix: added throws * fix: version update * fix: jar * fix: jar * fix: version update * fix: update version * fix: added date --------- Co-authored-by: Sattvik Chakravarthy Co-authored-by: Sattvik Chakravarthy --- CHANGELOG.md | 6 +- build.gradle | 2 +- jar/plugin-interface-2.23.0.jar | Bin 67290 -> 0 bytes jar/plugin-interface-3.0.0.jar | Bin 0 -> 88223 bytes .../pluginInterface/ActiveUsersStorage.java | 16 +- .../pluginInterface/RECIPE_ID.java | 3 +- .../supertokens/pluginInterface/Storage.java | 46 +++- .../authRecipe/AuthRecipeStorage.java | 18 +- .../authRecipe/AuthRecipeUserInfo.java | 5 +- .../dashboard/DashboardStorage.java | 30 ++- .../sqlStorage/DashboardSQLStorage.java | 11 +- .../emailpassword/EmailPasswordStorage.java | 43 ++-- .../emailpassword/UserInfo.java | 4 +- .../sqlStorage/EmailPasswordSQLStorage.java | 19 +- .../EmailVerificationStorage.java | 24 +- .../EmailVerificationSQLStorage.java | 18 +- ...ginException.java => DbInitException.java} | 18 +- .../exceptions/InvalidConfigException.java | 31 +++ .../jwt/sqlstorage/JWTRecipeSQLStorage.java | 9 +- .../multitenancy/AppIdentifier.java | 87 +++++++ .../AppIdentifierWithStorage.java | 162 ++++++++++++ .../multitenancy/EmailPasswordConfig.java | 35 +++ .../multitenancy/MultitenancyStorage.java | 61 +++++ .../multitenancy/PasswordlessConfig.java | 34 +++ .../multitenancy/TenantConfig.java | 108 ++++++++ .../multitenancy/TenantIdentifier.java | 95 +++++++ .../TenantIdentifierWithStorage.java | 134 ++++++++++ .../multitenancy/ThirdPartyConfig.java | 238 ++++++++++++++++++ .../DuplicateClientTypeException.java | 20 ++ .../exceptions/DuplicateTenantException.java | 21 ++ .../DuplicateThirdPartyIdException.java | 20 ++ .../TenantOrAppNotFoundException.java | 33 +++ .../passwordless/PasswordlessStorage.java | 50 ++-- .../passwordless/UserInfo.java | 4 +- .../sqlStorage/PasswordlessSQLStorage.java | 52 ++-- .../session/SessionStorage.java | 31 ++- .../session/sqlStorage/SessionSQLStorage.java | 31 ++- .../sqlStorage/SQLStorage.java | 8 +- .../thirdparty/ThirdPartyStorage.java | 28 +-- .../pluginInterface/thirdparty/UserInfo.java | 4 +- .../sqlStorage/ThirdPartySQLStorage.java | 9 +- .../pluginInterface/totp/TOTPDevice.java | 6 +- .../pluginInterface/totp/TOTPStorage.java | 47 ++-- .../pluginInterface/totp/TOTPUsedCode.java | 4 +- .../totp/sqlStorage/TOTPSQLStorage.java | 24 +- .../useridmapping/UserIdMappingStorage.java | 20 +- .../usermetadata/UserMetadataStorage.java | 6 +- .../sqlStorage/UserMetadataSQLStorage.java | 11 +- .../userroles/UserRolesStorage.java | 28 ++- .../sqlStorage/UserRolesSQLStorage.java | 24 +- 50 files changed, 1509 insertions(+), 229 deletions(-) delete mode 100644 jar/plugin-interface-2.23.0.jar create mode 100644 jar/plugin-interface-3.0.0.jar rename src/main/java/io/supertokens/pluginInterface/exceptions/{QuitProgramFromPluginException.java => DbInitException.java} (69%) create mode 100644 src/main/java/io/supertokens/pluginInterface/exceptions/InvalidConfigException.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/AppIdentifier.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/AppIdentifierWithStorage.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/EmailPasswordConfig.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/MultitenancyStorage.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/PasswordlessConfig.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/TenantConfig.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/TenantIdentifier.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/TenantIdentifierWithStorage.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/ThirdPartyConfig.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateClientTypeException.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateTenantException.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateThirdPartyIdException.java create mode 100644 src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/TenantOrAppNotFoundException.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6cfb59..0aa28c7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [3.0.0] - 2023-06-02 + +- Adds support for multi-tenancy + ## [2.23.0] - 2023-04-05 - Added `useStaticKey ` into session info classes @@ -109,4 +113,4 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Added RowMapper interface for db queries - Email verification related changes -- User pagination related queries \ No newline at end of file +- User pagination related queries diff --git a/build.gradle b/build.gradle index 96d08997..179e4313 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "2.23.0" +version = "3.0.0" repositories { mavenCentral() diff --git a/jar/plugin-interface-2.23.0.jar b/jar/plugin-interface-2.23.0.jar deleted file mode 100644 index 7edb243ecff36fc52e276504b1595b98e94074b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67290 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 literal 0 HcmV?d00001 diff --git a/src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java b/src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java index ed979ef8..4e2340cb 100644 --- a/src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java @@ -1,17 +1,21 @@ package io.supertokens.pluginInterface; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; -public interface ActiveUsersStorage extends Storage { +public interface ActiveUsersStorage extends NonAuthRecipeStorage { /* Update the last active time of a user to now */ - void updateLastActive(String userId) throws StorageQueryException; + void updateLastActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException; /* Count the number of users who did some activity after given timestamp */ - int countUsersActiveSince(long time) throws StorageQueryException; + int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException; /* Count the number of users who have enabled TOTP */ - int countUsersEnabledTotp() throws StorageQueryException; - + int countUsersEnabledTotp(AppIdentifier appIdentifier) throws StorageQueryException; + /* Count the number of users who have enabled TOTP and are active */ - int countUsersEnabledTotpAndActiveSince(long time) throws StorageQueryException; + int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException; + + void deleteUserActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException; } diff --git a/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java b/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java index 7456f85f..c7fb396f 100644 --- a/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java +++ b/src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java @@ -21,7 +21,8 @@ public enum RECIPE_ID { EMAIL_PASSWORD("emailpassword"), THIRD_PARTY("thirdparty"), SESSION("session"), EMAIL_VERIFICATION("emailverification"), JWT("jwt"), PASSWORDLESS("passwordless"), USER_METADATA("usermetadata"), - USER_ROLES("userroles"), USER_ID_MAPPING("useridmapping"), DASHBOARD("dashboard"), TOTP("totp"); + USER_ROLES("userroles"), USER_ID_MAPPING("useridmapping"), DASHBOARD("dashboard"), TOTP("totp"), + MULTITENANCY("multitenancy"); private final String name; diff --git a/src/main/java/io/supertokens/pluginInterface/Storage.java b/src/main/java/io/supertokens/pluginInterface/Storage.java index 2eb896a4..68c68f05 100644 --- a/src/main/java/io/supertokens/pluginInterface/Storage.java +++ b/src/main/java/io/supertokens/pluginInterface/Storage.java @@ -17,7 +17,13 @@ package io.supertokens.pluginInterface; +import com.google.gson.JsonObject; +import io.supertokens.pluginInterface.exceptions.DbInitException; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import java.util.Set; @@ -26,14 +32,27 @@ public interface Storage { // if silent is true, do not log anything out on the console void constructor(String processId, boolean silent); - void loadConfig(String configFilePath, Set logLevels); + void loadConfig(JsonObject jsonConfig, Set logLevels, TenantIdentifier tenantIdentifier) throws InvalidConfigException; + + // this returns a unique ID based on the db's connection URI and table prefix such that + // two different user pool IDs imply that the data for those two user pools are completely isolated. + String getUserPoolId(); + + // this returns a unique ID based on the db's connection pool config. This can be different + // even if the getUserPoolId returns the same ID - based on the config provided by the user. + // So two different db connection pools may point to the same end user pool. + String getConnectionPoolId(); + + // if the input otherConfig has different values set for the same properties as this user pool's config, + // then this function should throw an error since this is a misconfig from ther user's side. + void assertThatConfigFromSameUserPoolIsNotConflicting(JsonObject otherConfig) throws InvalidConfigException; void initFileLogging(String infoLogPath, String errorLogPath); void stopLogging(); // load tables and create connection pools - void initStorage(); + void initStorage(boolean shouldWait) throws DbInitException; // used by the core to do transactions the right way. STORAGE_TYPE getType(); @@ -43,19 +62,32 @@ public interface Storage { void close(); - KeyValueInfo getKeyValue(String key) throws StorageQueryException; + KeyValueInfo getKeyValue(TenantIdentifier tenantIdentifier, String key) throws StorageQueryException; - void setKeyValue(String key, KeyValueInfo info) throws StorageQueryException; + void setKeyValue(TenantIdentifier tenantIdentifier, String key, KeyValueInfo info) throws StorageQueryException, + TenantOrAppNotFoundException; void setStorageLayerEnabled(boolean enabled); - boolean canBeUsed(String configFilePath); + boolean canBeUsed(JsonObject configJson); // this function will be used in the createUserIdMapping and deleteUserIdMapping functions to check if the userId is // being used in NonAuth recipes. - boolean isUserIdBeingUsedInNonAuthRecipe(String className, String userId) throws StorageQueryException; + boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, String className, String userId) + throws StorageQueryException; // to be used for testing purposes only. This function will add dummy data to non-auth tables. - void addInfoToNonAuthRecipesBasedOnUserId(String className, String userId) throws StorageQueryException; + void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifier, String className, String userId) throws StorageQueryException; + + // this function is used during testing in the core so that the core can + // create multiple user pools across any plugin being used. + void modifyConfigToAddANewUserPoolForTesting(JsonObject config, int poolNumber); + + // this function returns a list of protected configs which users of supertokens saas can't read or modify + // when they are operating on tenantsm unless the supertokens_saas_secret key is used in the API request. + String[] getProtectedConfigsFromSuperTokensSaaSUsers(); + + Set getValidFieldsInConfig(); + void setLogLevels(Set logLevels); } diff --git a/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeStorage.java b/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeStorage.java index 591619b3..0d33e6eb 100644 --- a/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeStorage.java @@ -21,17 +21,27 @@ import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import javax.annotation.Nonnull; import javax.annotation.Nullable; public interface AuthRecipeStorage extends Storage { - long getUsersCount(@Nullable RECIPE_ID[] includeRecipeIds) throws StorageQueryException; + long getUsersCount(TenantIdentifier tenantIdentifier, @Nullable RECIPE_ID[] includeRecipeIds) + throws StorageQueryException; + + long getUsersCount(AppIdentifier appIdentifier, @Nullable RECIPE_ID[] includeRecipeIds) + throws StorageQueryException; - AuthRecipeUserInfo[] getUsers(@Nonnull Integer limit, @Nonnull String timeJoinedOrder, - @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, @Nullable Long timeJoined, @Nullable DashboardSearchTags dashboardSearchTags) + AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @Nonnull Integer limit, + @Nonnull String timeJoinedOrder, + @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, + @Nullable Long timeJoined, @Nullable DashboardSearchTags dashboardSearchTags) throws StorageQueryException; - boolean doesUserIdExist(String userId) throws StorageQueryException; + boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throws StorageQueryException; + + boolean doesUserIdExist(TenantIdentifier tenantIdentifierIdentifier, String userId) throws StorageQueryException; } diff --git a/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeUserInfo.java b/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeUserInfo.java index 6ae64cf3..29a60aee 100644 --- a/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeUserInfo.java +++ b/src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeUserInfo.java @@ -24,9 +24,12 @@ public abstract class AuthRecipeUserInfo { public long timeJoined; - public AuthRecipeUserInfo(String id, long timeJoined) { + public final String[] tenantIds; + + public AuthRecipeUserInfo(String id, long timeJoined, String[] tenantIds) { this.id = id; this.timeJoined = timeJoined; + this.tenantIds = tenantIds; } public abstract RECIPE_ID getRecipeId(); diff --git a/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardStorage.java b/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardStorage.java index f24028b8..83c85cf6 100644 --- a/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/dashboard/DashboardStorage.java @@ -1,30 +1,38 @@ package io.supertokens.pluginInterface.dashboard; import io.supertokens.pluginInterface.Storage; -import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.dashboard.exceptions.DuplicateUserIdException; import io.supertokens.pluginInterface.dashboard.exceptions.UserIdNotFoundException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; public interface DashboardStorage extends Storage { - void createNewDashboardUser(DashboardUser userInfo) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException; + void createNewDashboardUser(AppIdentifier appIdentifier, DashboardUser userInfo) + throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException, + TenantOrAppNotFoundException; + + DashboardUser getDashboardUserByEmail(AppIdentifier appIdentifier, String email) throws StorageQueryException; - DashboardUser getDashboardUserByEmail(String email) throws StorageQueryException; + DashboardUser getDashboardUserByUserId(AppIdentifier appIdentifier, String userId) throws StorageQueryException; - DashboardUser getDashboardUserByUserId(String userId) throws StorageQueryException; + DashboardUser[] getAllDashboardUsers(AppIdentifier appIdentifier) throws StorageQueryException; - DashboardUser[] getAllDashboardUsers() throws StorageQueryException; + boolean deleteDashboardUserWithUserId(AppIdentifier appIdentifier, String userId) throws StorageQueryException; - boolean deleteDashboardUserWithUserId(String userId) throws StorageQueryException; - - void createNewDashboardUserSession(String userId, String sessionId, long timeCreated, long expiry) throws StorageQueryException, UserIdNotFoundException; + void createNewDashboardUserSession(AppIdentifier appIdentifier, String userId, String sessionId, long timeCreated, + long expiry) throws StorageQueryException, UserIdNotFoundException; - DashboardSessionInfo[] getAllSessionsForUserId(String userId) throws StorageQueryException; + DashboardSessionInfo[] getAllSessionsForUserId(AppIdentifier appIdentifier, String userId) + throws StorageQueryException; - DashboardSessionInfo getSessionInfoWithSessionId(String sessionId) throws StorageQueryException; + DashboardSessionInfo getSessionInfoWithSessionId(AppIdentifier appIdentifier, String sessionId) + throws StorageQueryException; - boolean revokeSessionWithSessionId(String sessionId) throws StorageQueryException; + boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String sessionId) throws StorageQueryException; + // this function removes based on expired time, so we can use this to globally remove from a particular db. void revokeExpiredSessions() throws StorageQueryException; } diff --git a/src/main/java/io/supertokens/pluginInterface/dashboard/sqlStorage/DashboardSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/dashboard/sqlStorage/DashboardSQLStorage.java index 1237ea15..2790b590 100644 --- a/src/main/java/io/supertokens/pluginInterface/dashboard/sqlStorage/DashboardSQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/dashboard/sqlStorage/DashboardSQLStorage.java @@ -4,13 +4,18 @@ import io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.dashboard.exceptions.UserIdNotFoundException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; public interface DashboardSQLStorage extends DashboardStorage, SQLStorage { - void updateDashboardUsersEmailWithUserId_Transaction(TransactionConnection con, String userId, String newEmail) throws StorageQueryException, DuplicateEmailException, UserIdNotFoundException; + void updateDashboardUsersEmailWithUserId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String userId, String newEmail) + throws StorageQueryException, DuplicateEmailException, UserIdNotFoundException; + + void updateDashboardUsersPasswordWithUserId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String userId, String newPassword) + throws StorageQueryException, UserIdNotFoundException; - void updateDashboardUsersPasswordWithUserId_Transaction(TransactionConnection con, String userId, String newPassword) throws StorageQueryException, UserIdNotFoundException; - } \ No newline at end of file diff --git a/src/main/java/io/supertokens/pluginInterface/emailpassword/EmailPasswordStorage.java b/src/main/java/io/supertokens/pluginInterface/emailpassword/EmailPasswordStorage.java index 1bb1c05d..13e789aa 100644 --- a/src/main/java/io/supertokens/pluginInterface/emailpassword/EmailPasswordStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/emailpassword/EmailPasswordStorage.java @@ -22,36 +22,39 @@ import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; - -import javax.annotation.Nonnull; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; public interface EmailPasswordStorage extends AuthRecipeStorage { - void signUp(UserInfo userInfo) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException; - - void deleteEmailPasswordUser(String userId) throws StorageQueryException; + // we pass tenantIdentifier here cause this also adds to the userId <-> tenantId mapping + UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String email, String passwordHash, long timeJoined) + throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException, + TenantOrAppNotFoundException; - UserInfo getUserInfoUsingId(String id) throws StorageQueryException; + // this deletion of a user is app wide since the same user ID can be shared across tenants + void deleteEmailPasswordUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException; + + UserInfo getUserInfoUsingId(AppIdentifier appIdentifier, String id) throws StorageQueryException; - UserInfo getUserInfoUsingEmail(String email) throws StorageQueryException; + // Here we pass in TenantIdentifier cause the same email can be shared across tenants, but yield different + // user IDs + UserInfo getUserInfoUsingEmail(TenantIdentifier tenantIdentifier, String email) throws StorageQueryException; - void addPasswordResetToken(PasswordResetTokenInfo passwordResetTokenInfo) + // password reset stuff is app wide cause changing the password for a user affects all the tenants + // across which it's shared. + void addPasswordResetToken(AppIdentifier appIdentifier, PasswordResetTokenInfo passwordResetTokenInfo) throws StorageQueryException, UnknownUserIdException, DuplicatePasswordResetTokenException; - PasswordResetTokenInfo getPasswordResetTokenInfo(String token) throws StorageQueryException; + PasswordResetTokenInfo getPasswordResetTokenInfo(AppIdentifier appIdentifier, String token) + throws StorageQueryException; + // we purposely do not add TenantIdentifier to this query cause + // this is called from a cronjob that runs per user pool ID void deleteExpiredPasswordResetTokens() throws StorageQueryException; - PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(String userId) throws StorageQueryException; - - @Deprecated - UserInfo[] getUsers(@Nonnull String userId, @Nonnull Long timeJoined, @Nonnull Integer limit, - @Nonnull String timeJoinedOrder) throws StorageQueryException; - - @Deprecated - UserInfo[] getUsers(@Nonnull Integer limit, @Nonnull String timeJoinedOrder) throws StorageQueryException; - - @Deprecated - long getUsersCount() throws StorageQueryException; + PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(AppIdentifier appIdentifier, String userId) + throws StorageQueryException; } \ No newline at end of file diff --git a/src/main/java/io/supertokens/pluginInterface/emailpassword/UserInfo.java b/src/main/java/io/supertokens/pluginInterface/emailpassword/UserInfo.java index 03aaf2c8..bc65964a 100644 --- a/src/main/java/io/supertokens/pluginInterface/emailpassword/UserInfo.java +++ b/src/main/java/io/supertokens/pluginInterface/emailpassword/UserInfo.java @@ -26,8 +26,8 @@ public class UserInfo extends AuthRecipeUserInfo { // using transient, we tell Gson not to include this when creating a JSON public transient final String passwordHash; - public UserInfo(String id, String email, String passwordHash, long timeJoined) { - super(id, timeJoined); + public UserInfo(String id, String email, String passwordHash, long timeJoined, String[] tenantIds) { + super(id, timeJoined, tenantIds); this.email = email; this.passwordHash = passwordHash; } diff --git a/src/main/java/io/supertokens/pluginInterface/emailpassword/sqlStorage/EmailPasswordSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/emailpassword/sqlStorage/EmailPasswordSQLStorage.java index fd926299..78b5ea68 100644 --- a/src/main/java/io/supertokens/pluginInterface/emailpassword/sqlStorage/EmailPasswordSQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/emailpassword/sqlStorage/EmailPasswordSQLStorage.java @@ -21,22 +21,31 @@ import io.supertokens.pluginInterface.emailpassword.UserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; public interface EmailPasswordSQLStorage extends EmailPasswordStorage, SQLStorage { - PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction(TransactionConnection con, String userId) + // all password reset related stuff is app wide cause the same user ID can be shared across tenants, + // and updating / resetting a user's password should apply to all those tenants. + + PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, String userId) throws StorageQueryException; - void deleteAllPasswordResetTokensForUser_Transaction(TransactionConnection con, String userId) + void deleteAllPasswordResetTokensForUser_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String userId) throws StorageQueryException; - void updateUsersPassword_Transaction(TransactionConnection con, String userId, String newPassword) + void updateUsersPassword_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + String newPassword) throws StorageQueryException; - void updateUsersEmail_Transaction(TransactionConnection conn, String userId, String email) + void updateUsersEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection conn, String userId, + String email) throws StorageQueryException, DuplicateEmailException; - UserInfo getUserInfoUsingId_Transaction(TransactionConnection con, String userId) throws StorageQueryException; + UserInfo getUserInfoUsingId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId) + throws StorageQueryException; } diff --git a/src/main/java/io/supertokens/pluginInterface/emailverification/EmailVerificationStorage.java b/src/main/java/io/supertokens/pluginInterface/emailverification/EmailVerificationStorage.java index c9df0b26..1d436ef5 100644 --- a/src/main/java/io/supertokens/pluginInterface/emailverification/EmailVerificationStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/emailverification/EmailVerificationStorage.java @@ -16,29 +16,35 @@ package io.supertokens.pluginInterface.emailverification; -import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.emailverification.exception.DuplicateEmailVerificationTokenException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; public interface EmailVerificationStorage extends NonAuthRecipeStorage { - void addEmailVerificationToken(EmailVerificationTokenInfo emailVerificationInfo) - throws StorageQueryException, DuplicateEmailVerificationTokenException; + void addEmailVerificationToken(TenantIdentifier tenantIdentifier, EmailVerificationTokenInfo emailVerificationInfo) + throws StorageQueryException, DuplicateEmailVerificationTokenException, TenantOrAppNotFoundException; - EmailVerificationTokenInfo getEmailVerificationTokenInfo(String token) throws StorageQueryException; + EmailVerificationTokenInfo getEmailVerificationTokenInfo(TenantIdentifier tenantIdentifier, String token) + throws StorageQueryException; + + void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String userId) throws StorageQueryException; - void deleteEmailVerificationUserInfo(String userId) throws StorageQueryException; + boolean deleteEmailVerificationUserInfo(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException; - void revokeAllTokens(String userId, String email) throws StorageQueryException; + void revokeAllTokens(TenantIdentifier tenantIdentifier, String userId, String email) throws StorageQueryException; - void unverifyEmail(String userId, String email) throws StorageQueryException; + void unverifyEmail(AppIdentifier appIdentifier, String userId, String email) throws StorageQueryException; void deleteExpiredEmailVerificationTokens() throws StorageQueryException; - EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(String userId, String email) + EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(TenantIdentifier tenantIdentifier, String userId, + String email) throws StorageQueryException; - boolean isEmailVerified(String userId, String email) throws StorageQueryException; + boolean isEmailVerified(AppIdentifier appIdentifier, String userId, String email) throws StorageQueryException; } \ No newline at end of file diff --git a/src/main/java/io/supertokens/pluginInterface/emailverification/sqlStorage/EmailVerificationSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/emailverification/sqlStorage/EmailVerificationSQLStorage.java index 7c21f312..14ceb117 100644 --- a/src/main/java/io/supertokens/pluginInterface/emailverification/sqlStorage/EmailVerificationSQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/emailverification/sqlStorage/EmailVerificationSQLStorage.java @@ -19,18 +19,26 @@ import io.supertokens.pluginInterface.emailverification.EmailVerificationStorage; import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; public interface EmailVerificationSQLStorage extends EmailVerificationStorage, SQLStorage { - EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(TransactionConnection con, - String userId, String email) throws StorageQueryException; + EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(TenantIdentifier tenantIdentifier, + TransactionConnection con, + String userId, String email) + throws StorageQueryException; - void deleteAllEmailVerificationTokensForUser_Transaction(TransactionConnection con, String userId, String email) + void deleteAllEmailVerificationTokensForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String userId, String email) throws StorageQueryException; - void updateIsEmailVerified_Transaction(TransactionConnection con, String userId, String email, - boolean isEmailVerified) throws StorageQueryException; + void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + String email, + boolean isEmailVerified) + throws StorageQueryException, TenantOrAppNotFoundException; } diff --git a/src/main/java/io/supertokens/pluginInterface/exceptions/QuitProgramFromPluginException.java b/src/main/java/io/supertokens/pluginInterface/exceptions/DbInitException.java similarity index 69% rename from src/main/java/io/supertokens/pluginInterface/exceptions/QuitProgramFromPluginException.java rename to src/main/java/io/supertokens/pluginInterface/exceptions/DbInitException.java index 7022fd63..f7d9de2a 100644 --- a/src/main/java/io/supertokens/pluginInterface/exceptions/QuitProgramFromPluginException.java +++ b/src/main/java/io/supertokens/pluginInterface/exceptions/DbInitException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 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. @@ -12,20 +12,20 @@ * 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.exceptions; -public class QuitProgramFromPluginException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - public QuitProgramFromPluginException(String msg) { - super(msg); +public class DbInitException extends Exception { + public DbInitException(Exception e) { + super(e); } - public QuitProgramFromPluginException(Exception e) { + public DbInitException(String e) { super(e); } + + public DbInitException() { + super(); + } } diff --git a/src/main/java/io/supertokens/pluginInterface/exceptions/InvalidConfigException.java b/src/main/java/io/supertokens/pluginInterface/exceptions/InvalidConfigException.java new file mode 100644 index 00000000..3dfdcd45 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/exceptions/InvalidConfigException.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023, 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.exceptions; + +public class InvalidConfigException extends Exception { + public InvalidConfigException(String message) { + super(message); + } + + public InvalidConfigException() { + super(); + } + + public InvalidConfigException(Exception e) { + super(e); + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/jwt/sqlstorage/JWTRecipeSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/jwt/sqlstorage/JWTRecipeSQLStorage.java index 6c421302..3ab19c79 100644 --- a/src/main/java/io/supertokens/pluginInterface/jwt/sqlstorage/JWTRecipeSQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/jwt/sqlstorage/JWTRecipeSQLStorage.java @@ -20,14 +20,17 @@ import io.supertokens.pluginInterface.jwt.JWTRecipeStorage; import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; import java.util.List; public interface JWTRecipeSQLStorage extends JWTRecipeStorage, SQLStorage { - List getJWTSigningKeys_Transaction(TransactionConnection con) throws StorageQueryException; + List getJWTSigningKeys_Transaction(AppIdentifier appIdentifier, TransactionConnection con) + throws StorageQueryException; - void setJWTSigningKey_Transaction(TransactionConnection con, JWTSigningKeyInfo info) - throws StorageQueryException, DuplicateKeyIdException; + void setJWTSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con, JWTSigningKeyInfo info) + throws StorageQueryException, DuplicateKeyIdException, TenantOrAppNotFoundException; } diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/AppIdentifier.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/AppIdentifier.java new file mode 100644 index 00000000..0aa4aedf --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/AppIdentifier.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023, 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.multitenancy; + +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; +import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; +import io.supertokens.pluginInterface.emailverification.sqlStorage.EmailVerificationSQLStorage; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.session.SessionStorage; +import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; +import io.supertokens.pluginInterface.usermetadata.sqlStorage.UserMetadataSQLStorage; +import io.supertokens.pluginInterface.userroles.sqlStorage.UserRolesSQLStorage; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class AppIdentifier { + public static final String DEFAULT_APP_ID = "public"; + public static final String DEFAULT_CONNECTION_URI = ""; + + @Nullable + private final String connectionUriDomain; + + @Nullable + private final String appId; + + public AppIdentifier(@Nullable String connectionUriDomain, @Nullable String appId) { + this.connectionUriDomain = connectionUriDomain; + this.appId = appId; + } + + @Nonnull + public String getAppId() { + if (this.appId == null || this.appId.equals("")) { + return DEFAULT_APP_ID; + } + return this.appId.trim().toLowerCase(); + } + + @Nonnull + public String getConnectionUriDomain() { + if (this.connectionUriDomain == null) { + return DEFAULT_CONNECTION_URI; + } + return this.connectionUriDomain.trim().toLowerCase(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof AppIdentifier) { + AppIdentifier otherAppIdentifier = (AppIdentifier) other; + return otherAppIdentifier.getConnectionUriDomain().equals(this.getConnectionUriDomain()) && + otherAppIdentifier.getAppId().equals(this.getAppId()); + } + return false; + } + + @Override + public int hashCode() { + return (this.getConnectionUriDomain() + "|" + + this.getAppId()).hashCode(); + } + + public TenantIdentifier getAsPublicTenantIdentifier() { + return new TenantIdentifier(this.getConnectionUriDomain(), this.getAppId(), null); + } + + public AppIdentifierWithStorage withStorage(Storage storage) { + return new AppIdentifierWithStorage(this.getConnectionUriDomain(), this.getAppId(), storage); + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/AppIdentifierWithStorage.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/AppIdentifierWithStorage.java new file mode 100644 index 00000000..1134d51d --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/AppIdentifierWithStorage.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2023, 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.multitenancy; + +import io.supertokens.pluginInterface.ActiveUsersStorage; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; +import io.supertokens.pluginInterface.dashboard.sqlStorage.DashboardSQLStorage; +import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; +import io.supertokens.pluginInterface.emailverification.sqlStorage.EmailVerificationSQLStorage; +import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage; +import io.supertokens.pluginInterface.session.SessionStorage; +import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage; +import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; +import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; +import io.supertokens.pluginInterface.usermetadata.sqlStorage.UserMetadataSQLStorage; +import io.supertokens.pluginInterface.userroles.sqlStorage.UserRolesSQLStorage; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class AppIdentifierWithStorage extends AppIdentifier { + + @Nonnull + private final Storage storage; + + private final Storage[] storages; + + public AppIdentifierWithStorage(@Nullable String connectionUriDomain, @Nullable String appId, @Nonnull + Storage storage) { + super(connectionUriDomain, appId); + this.storage = storage; + this.storages = new Storage[]{storage}; + } + + public AppIdentifierWithStorage(@Nullable String connectionUriDomain, @Nullable String appId, @Nonnull + Storage storage, @Nonnull Storage[] storages) { + super(connectionUriDomain, appId); + this.storage = storage; + this.storages = storages; + } + + @Nonnull + public Storage getStorage() { + return storage; + } + + @Nonnull + public Storage[] getStorages() { + return storages; + } + + public AuthRecipeStorage getAuthRecipeStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + + return (AuthRecipeStorage) this.storage; + } + + public EmailPasswordSQLStorage getEmailPasswordStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (EmailPasswordSQLStorage) this.storage; + } + + public PasswordlessSQLStorage getPasswordlessStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (PasswordlessSQLStorage) this.storage; + } + + public ThirdPartySQLStorage getThirdPartyStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (ThirdPartySQLStorage) this.storage; + } + + public EmailVerificationSQLStorage getEmailVerificationStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (EmailVerificationSQLStorage) this.storage; + } + + public SessionStorage getSessionStorage() { + return (SessionStorage) this.storage; + } + + public UserMetadataSQLStorage getUserMetadataStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + + return (UserMetadataSQLStorage) this.storage; + } + + public UserIdMappingStorage getUserIdMappingStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + + return (UserIdMappingStorage) this.storage; + } + + public UserRolesSQLStorage getUserRolesStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (UserRolesSQLStorage) this.storage; + } + + public DashboardSQLStorage getDashboardStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (DashboardSQLStorage) this.storage; + } + + public TOTPSQLStorage getTOTPStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (TOTPSQLStorage) this.storage; + } + + public ActiveUsersStorage getActiveUsersStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (ActiveUsersStorage) this.storage; + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/EmailPasswordConfig.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/EmailPasswordConfig.java new file mode 100644 index 00000000..aac0c636 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/EmailPasswordConfig.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023, 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.multitenancy; + +public class EmailPasswordConfig { + public final boolean enabled; + + public EmailPasswordConfig(boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean equals(Object other) { + if (other instanceof EmailPasswordConfig) { + EmailPasswordConfig otherEmailPasswordConfig = (EmailPasswordConfig) other; + return otherEmailPasswordConfig.enabled == this.enabled; + } + return false; + } + +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/MultitenancyStorage.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/MultitenancyStorage.java new file mode 100644 index 00000000..5b3e05ce --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/MultitenancyStorage.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020, 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.multitenancy; + +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException; +import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; +import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; +import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; +import io.supertokens.pluginInterface.userroles.exception.UnknownRoleException; + +public interface MultitenancyStorage extends Storage { + + void createTenant(TenantConfig config) throws DuplicateTenantException, DuplicateThirdPartyIdException, + DuplicateClientTypeException, StorageQueryException; + + // this adds tenantId to the target user pool + void addTenantIdInTargetStorage(TenantIdentifier tenantIdentifier) + throws DuplicateTenantException, StorageQueryException; + + // this also deletes all tenant info from all tables. + void deleteTenantIdInTargetStorage(TenantIdentifier tenantIdentifier) + throws TenantOrAppNotFoundException, StorageQueryException; + + void overwriteTenantConfig(TenantConfig config) throws TenantOrAppNotFoundException, + DuplicateThirdPartyIdException, DuplicateClientTypeException, StorageQueryException; + + boolean deleteTenantInfoInBaseStorage(TenantIdentifier tenantIdentifier) throws StorageQueryException; + + boolean deleteAppInfoInBaseStorage(AppIdentifier appIdentifier) throws StorageQueryException; + + boolean deleteConnectionUriDomainInfoInBaseStorage(String connectionUriDomain) throws StorageQueryException; + + TenantConfig[] getAllTenants() throws StorageQueryException; + + boolean addUserIdToTenant(TenantIdentifier tenantIdentifier, String userId) throws TenantOrAppNotFoundException, + UnknownUserIdException, StorageQueryException, DuplicateEmailException, DuplicateThirdPartyUserException, + DuplicatePhoneNumberException; + + boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException; +} \ No newline at end of file diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/PasswordlessConfig.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/PasswordlessConfig.java new file mode 100644 index 00000000..66db1977 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/PasswordlessConfig.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023, 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.multitenancy; + +public class PasswordlessConfig { + public boolean enabled; + + public PasswordlessConfig(boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean equals(Object other) { + if (other instanceof PasswordlessConfig) { + PasswordlessConfig otherPasswordlessConfig = (PasswordlessConfig) other; + return otherPasswordlessConfig.enabled == this.enabled; + } + return false; + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantConfig.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantConfig.java new file mode 100644 index 00000000..d5b00481 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantConfig.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023, 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.multitenancy; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; +import io.supertokens.pluginInterface.Storage; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class TenantConfig { + + @Nonnull + public transient final TenantIdentifier tenantIdentifier; + + @Nonnull + @SerializedName("emailPassword") + public final EmailPasswordConfig emailPasswordConfig; + + @Nonnull + @SerializedName("thirdParty") + public final ThirdPartyConfig thirdPartyConfig; + + @Nonnull + @SerializedName("passwordless") + public final PasswordlessConfig passwordlessConfig; + + @Nonnull + public final JsonObject coreConfig; + + public TenantConfig(@Nonnull TenantIdentifier tenantIdentifier, @Nonnull EmailPasswordConfig emailPasswordConfig, + @Nonnull ThirdPartyConfig thirdPartyConfig, + @Nonnull PasswordlessConfig passwordlessConfig, @Nullable JsonObject coreConfig) { + this.tenantIdentifier = tenantIdentifier; + this.coreConfig = coreConfig == null ? new JsonObject() : coreConfig; + this.emailPasswordConfig = emailPasswordConfig; + this.passwordlessConfig = passwordlessConfig; + this.thirdPartyConfig = thirdPartyConfig; + } + + 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.coreConfig = gson.fromJson(other.coreConfig.toString(), JsonObject.class); + this.emailPasswordConfig = new EmailPasswordConfig(other.emailPasswordConfig.enabled); + this.passwordlessConfig = new PasswordlessConfig(other.passwordlessConfig.enabled); + this.thirdPartyConfig = gson.fromJson(gson.toJsonTree(other.thirdPartyConfig).getAsJsonObject(), ThirdPartyConfig.class); + } + + public boolean deepEquals(TenantConfig other) { + if (other == null) { + return false; + } + return this.tenantIdentifier.equals(other.tenantIdentifier) && + this.emailPasswordConfig.equals(other.emailPasswordConfig) && + this.passwordlessConfig.equals(other.passwordlessConfig) && + this.thirdPartyConfig.equals(other.thirdPartyConfig) && + this.coreConfig.equals(other.coreConfig); + } + + @Override + public boolean equals(Object other) { + if (other instanceof TenantConfig) { + TenantConfig otherTenantConfig = (TenantConfig) other; + return otherTenantConfig.tenantIdentifier.equals(this.tenantIdentifier); + } + return false; + } + + @Override + public int hashCode() { + return tenantIdentifier.hashCode(); + } + + public JsonObject toJson(boolean shouldProtectDbConfig, Storage storage) { + Gson gson = new Gson(); + JsonObject tenantConfigObject = gson.toJsonTree(this).getAsJsonObject(); + 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); + } + } + } + + return tenantConfigObject; + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantIdentifier.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantIdentifier.java new file mode 100644 index 00000000..9abac35a --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantIdentifier.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023, 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.multitenancy; + +import io.supertokens.pluginInterface.Storage; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class TenantIdentifier { + public static final String DEFAULT_TENANT_ID = "public"; + public static final String DEFAULT_APP_ID = "public"; + public static final String DEFAULT_CONNECTION_URI = ""; + + public static TenantIdentifier BASE_TENANT = new TenantIdentifier(null, null, null); + + @Nullable + private final String connectionUriDomain; + + @Nullable + private final String tenantId; + + @Nullable + private final String appId; + + public TenantIdentifier(@Nullable String connectionUriDomain, @Nullable String appId, @Nullable String tenantId) { + this.connectionUriDomain = connectionUriDomain; + this.tenantId = tenantId; + this.appId = appId; + } + + @Nonnull + public String getTenantId() { + if (this.tenantId == null || this.tenantId.equals("")) { + return DEFAULT_TENANT_ID; + } + return this.tenantId.trim().toLowerCase(); + } + + @Nonnull + public String getAppId() { + if (this.appId == null || this.appId.equals("")) { + return DEFAULT_APP_ID; + } + return this.appId.trim().toLowerCase(); + } + + @Nonnull + public String getConnectionUriDomain() { + if (this.connectionUriDomain == null) { + return DEFAULT_CONNECTION_URI; + } + return this.connectionUriDomain.trim().toLowerCase(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof TenantIdentifier) { + TenantIdentifier otherTenantIdentifier = (TenantIdentifier) other; + return otherTenantIdentifier.getTenantId().equals(this.getTenantId()) && + otherTenantIdentifier.getConnectionUriDomain().equals(this.getConnectionUriDomain()) && + otherTenantIdentifier.getAppId().equals(this.getAppId()); + } + return false; + } + + @Override + public int hashCode() { + return (this.getTenantId() + "|" + this.getConnectionUriDomain() + "|" + + this.getAppId()).hashCode(); + } + + public AppIdentifier toAppIdentifier() { + return new AppIdentifier(this.getConnectionUriDomain(), this.getAppId()); + } + + public TenantIdentifierWithStorage withStorage(Storage storage) { + return new TenantIdentifierWithStorage(this.getConnectionUriDomain(), this.getAppId(), this.getTenantId(), + storage); + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantIdentifierWithStorage.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantIdentifierWithStorage.java new file mode 100644 index 00000000..82002f0c --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/TenantIdentifierWithStorage.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023, 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.multitenancy; + +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; +import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; +import io.supertokens.pluginInterface.emailverification.sqlStorage.EmailVerificationSQLStorage; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage; +import io.supertokens.pluginInterface.session.SessionStorage; +import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage; +import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; +import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; +import io.supertokens.pluginInterface.userroles.sqlStorage.UserRolesSQLStorage; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class TenantIdentifierWithStorage extends TenantIdentifier { + + @Nonnull + private final Storage storage; + + public TenantIdentifierWithStorage(@Nullable String connectionUriDomain, @Nullable String appId, + @Nullable String tenantId, @Nonnull + Storage storage) { + super(connectionUriDomain, appId, tenantId); + this.storage = storage; + } + + @Nonnull + public Storage getStorage() { + return storage; + } + + public AppIdentifierWithStorage toAppIdentifierWithStorage() { + return new AppIdentifierWithStorage(this.getConnectionUriDomain(), this.getAppId(), this.getStorage()); + } + + public AuthRecipeStorage getAuthRecipeStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + + return (AuthRecipeStorage) this.storage; + } + + public UserIdMappingStorage getUserIdMappingStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + + return (UserIdMappingStorage) this.storage; + } + + public EmailPasswordSQLStorage getEmailPasswordStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + + return (EmailPasswordSQLStorage) this.storage; + } + + public PasswordlessSQLStorage getPasswordlessStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (PasswordlessSQLStorage) this.storage; + } + + public ThirdPartySQLStorage getThirdPartyStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (ThirdPartySQLStorage) this.storage; + } + + public EmailVerificationSQLStorage getEmailVerificationStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (EmailVerificationSQLStorage) this.storage; + } + + public SessionStorage getSessionStorage() { + return (SessionStorage) this.storage; + } + + public UserRolesSQLStorage getUserRolesStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (UserRolesSQLStorage) this.storage; + } + + public TOTPSQLStorage getTOTPStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (TOTPSQLStorage) this.storage; + } + + public MultitenancyStorage getMultitenancyStorageWithTargetStorage() { + if (this.storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (MultitenancyStorage) this.storage; + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/ThirdPartyConfig.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/ThirdPartyConfig.java new file mode 100644 index 00000000..456dc464 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/ThirdPartyConfig.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2023, 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.multitenancy; + +import com.google.gson.JsonObject; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Objects; + +public class ThirdPartyConfig { + public final boolean enabled; + + @Nonnull + public final Provider[] providers; + + public ThirdPartyConfig(boolean enabled, @Nullable Provider[] providers) { + this.enabled = enabled; + this.providers = providers == null ? new Provider[0] : providers; + } + + public static class Provider { + + @Nonnull + public final String thirdPartyId; + + @Nonnull + public final String name; + + @Nonnull + public final ProviderClient[] clients; + + @Nullable + public final String authorizationEndpoint; + + @Nullable + public final JsonObject authorizationEndpointQueryParams; + + @Nullable + public final String tokenEndpoint; + + @Nullable + public final JsonObject tokenEndpointBodyParams; + + @Nullable + public final String userInfoEndpoint; + + @Nullable + public JsonObject userInfoEndpointQueryParams; + + @Nullable + public final JsonObject userInfoEndpointHeaders; + + @Nullable + public final String jwksURI; + + @Nullable + public final String oidcDiscoveryEndpoint; + + @Nullable + public final Boolean requireEmail; + + @Nonnull + public final UserInfoMap userInfoMap; + + public Provider(@Nonnull String thirdPartyId, @Nonnull String name, @Nullable ProviderClient[] clients, + @Nullable String authorizationEndpoint, + @Nullable JsonObject authorizationEndpointQueryParams, @Nullable String tokenEndpoint, + @Nullable JsonObject tokenEndpointBodyParams, + @Nullable String userInfoEndpoint, @Nullable JsonObject userInfoEndpointQueryParams, + @Nullable JsonObject userInfoEndpointHeaders, + @Nullable String jwksURI, @Nullable String oidcDiscoveryEndpoint, @Nullable Boolean requireEmail, + @Nullable UserInfoMap userInfoMap) { + this.thirdPartyId = thirdPartyId; + this.name = name; + this.clients = clients == null ? new ProviderClient[0] : clients; + this.authorizationEndpoint = authorizationEndpoint; + this.authorizationEndpointQueryParams = authorizationEndpointQueryParams; + this.tokenEndpoint = tokenEndpoint; + this.tokenEndpointBodyParams = tokenEndpointBodyParams; + this.userInfoEndpoint = userInfoEndpoint; + this.userInfoEndpointQueryParams = userInfoEndpointQueryParams; + this.userInfoEndpointHeaders = userInfoEndpointHeaders; + this.jwksURI = jwksURI; + this.oidcDiscoveryEndpoint = oidcDiscoveryEndpoint; + this.requireEmail = requireEmail; + this.userInfoMap = userInfoMap == null ? new UserInfoMap(null, null) : userInfoMap; + } + + @Override + public boolean equals(Object other) { + if (other instanceof Provider) { + Provider otherProvider = (Provider) other; + return otherProvider.thirdPartyId.equals(this.thirdPartyId) && + otherProvider.name.equals(this.name) && + Arrays.equals(otherProvider.clients, this.clients) && + Objects.equals(otherProvider.authorizationEndpoint, this.authorizationEndpoint) && + Objects.equals(otherProvider.authorizationEndpointQueryParams, + this.authorizationEndpointQueryParams) && + Objects.equals(otherProvider.tokenEndpoint, this.tokenEndpoint) && + Objects.equals(otherProvider.tokenEndpointBodyParams, this.tokenEndpointBodyParams) && + Objects.equals(otherProvider.userInfoEndpoint, this.userInfoEndpoint) && + Objects.equals(otherProvider.userInfoEndpointQueryParams, this.userInfoEndpointQueryParams) && + Objects.equals(otherProvider.userInfoEndpointHeaders, this.userInfoEndpointHeaders) && + Objects.equals(otherProvider.jwksURI, this.jwksURI) && + Objects.equals(otherProvider.oidcDiscoveryEndpoint, this.oidcDiscoveryEndpoint) && + otherProvider.requireEmail == this.requireEmail && + Objects.equals(otherProvider.userInfoMap, this.userInfoMap); + } + return false; + } + } + + public static class ProviderClient { + + @Nullable + public final String clientType; + + @Nonnull + public final String clientId; + + @Nullable + public final String clientSecret; + + @Nullable + public final String[] scope; + + @Nullable + public final Boolean forcePKCE; + + @Nullable + public final JsonObject additionalConfig; + + public ProviderClient(@Nullable String clientType, @Nonnull String clientId, @Nullable String clientSecret, + @Nullable String[] scope, + @Nullable Boolean forcePKCE, @Nullable JsonObject additionalConfig) { + this.clientType = clientType; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.scope = scope; + this.forcePKCE = forcePKCE; + this.additionalConfig = additionalConfig; + } + + @Override + public boolean equals(Object other) { + if (other instanceof ProviderClient) { + ProviderClient otherProviderClient = (ProviderClient) other; + return Objects.equals(otherProviderClient.clientType, this.clientType) && + otherProviderClient.clientId.equals(this.clientId) && + Objects.equals(otherProviderClient.clientSecret, this.clientSecret) && + Arrays.equals(otherProviderClient.scope, this.scope) && + otherProviderClient.forcePKCE == this.forcePKCE && + Objects.equals(otherProviderClient.additionalConfig, this.additionalConfig); + } + return false; + } + } + + public static class UserInfoMap { + @Nonnull + public UserInfoMapKeyValue fromIdTokenPayload; + + @Nonnull + public UserInfoMapKeyValue fromUserInfoAPI; + + public UserInfoMap(@Nullable UserInfoMapKeyValue fromIdTokenPayload, + @Nullable UserInfoMapKeyValue fromUserInfoAPI) { + this.fromIdTokenPayload = fromIdTokenPayload == null ? new UserInfoMapKeyValue(null, null, null) : fromIdTokenPayload; + this.fromUserInfoAPI = fromUserInfoAPI == null ? new UserInfoMapKeyValue(null, null, null) : fromUserInfoAPI; + } + + @Override + public boolean equals(Object other) { + if (other instanceof UserInfoMap) { + UserInfoMap otherUserInfoMap = (UserInfoMap) other; + return Objects.equals(otherUserInfoMap.fromUserInfoAPI, this.fromUserInfoAPI) && + Objects.equals(otherUserInfoMap.fromIdTokenPayload, this.fromIdTokenPayload); + } + return false; + } + } + + public static class UserInfoMapKeyValue { + @Nullable + public String userId; + + @Nullable + public String email; + + @Nullable + public String emailVerified; + + public UserInfoMapKeyValue(@Nullable String userId, @Nullable String email, @Nullable String emailVerified) { + this.userId = userId; + this.email = email; + this.emailVerified = emailVerified; + } + + @Override + public boolean equals(Object other) { + if (other instanceof UserInfoMapKeyValue) { + UserInfoMapKeyValue otherUserInfoMapKeyValue = (UserInfoMapKeyValue) other; + return Objects.equals(otherUserInfoMapKeyValue.userId, this.userId) && + Objects.equals(otherUserInfoMapKeyValue.email, this.email) && + Objects.equals(otherUserInfoMapKeyValue.emailVerified, this.emailVerified); + } + return false; + } + } + + @Override + public boolean equals(Object other) { + if (other instanceof ThirdPartyConfig) { + ThirdPartyConfig otherThirdPartyConfig = (ThirdPartyConfig) other; + return otherThirdPartyConfig.enabled == this.enabled && + Arrays.equals(otherThirdPartyConfig.providers, this.providers); + } + return false; + } + +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateClientTypeException.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateClientTypeException.java new file mode 100644 index 00000000..733ef82e --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateClientTypeException.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, 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.multitenancy.exceptions; + +public class DuplicateClientTypeException extends Exception { +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateTenantException.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateTenantException.java new file mode 100644 index 00000000..4555fa72 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateTenantException.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020, 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.multitenancy.exceptions; + +public class DuplicateTenantException extends Exception { + +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateThirdPartyIdException.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateThirdPartyIdException.java new file mode 100644 index 00000000..416c60e3 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/DuplicateThirdPartyIdException.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, 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.multitenancy.exceptions; + +public class DuplicateThirdPartyIdException extends Exception { +} diff --git a/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/TenantOrAppNotFoundException.java b/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/TenantOrAppNotFoundException.java new file mode 100644 index 00000000..0dc258ad --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/multitenancy/exceptions/TenantOrAppNotFoundException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020, 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.multitenancy.exceptions; + +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; + +public class TenantOrAppNotFoundException extends Exception { + public TenantOrAppNotFoundException(TenantIdentifier tenantIdentifier) { + super("Tenant with the following connectionURIDomain, appId and tenantId combination not found: (" + + tenantIdentifier.getConnectionUriDomain() + + ", " + tenantIdentifier.getAppId() + ", " + tenantIdentifier.getTenantId() + ")"); + } + + public TenantOrAppNotFoundException(AppIdentifier appIdentifier) { + super("App with the following connectionURIDomain and appId combination not found: (" + + appIdentifier.getConnectionUriDomain() + ", " + appIdentifier.getAppId() + ")"); + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/passwordless/PasswordlessStorage.java b/src/main/java/io/supertokens/pluginInterface/passwordless/PasswordlessStorage.java index 19e6da46..e9e6eae1 100644 --- a/src/main/java/io/supertokens/pluginInterface/passwordless/PasswordlessStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/passwordless/PasswordlessStorage.java @@ -20,45 +20,51 @@ import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; -import io.supertokens.pluginInterface.passwordless.exception.DuplicateCodeIdException; -import io.supertokens.pluginInterface.passwordless.exception.DuplicateDeviceIdHashException; -import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; -import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; -import io.supertokens.pluginInterface.passwordless.exception.UnknownDeviceIdHash; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.passwordless.exception.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; public interface PasswordlessStorage extends AuthRecipeStorage { - void createDeviceWithCode(@Nullable String email, @Nullable String phoneNumber, @Nonnull String linkCodeSalt, - PasswordlessCode code) throws StorageQueryException, DuplicateDeviceIdHashException, - DuplicateCodeIdException, DuplicateLinkCodeHashException; + void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable String email, @Nullable String phoneNumber, + @Nonnull String linkCodeSalt, + PasswordlessCode code) throws StorageQueryException, DuplicateDeviceIdHashException, + DuplicateCodeIdException, DuplicateLinkCodeHashException, TenantOrAppNotFoundException; - void createCode(PasswordlessCode code) + void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) throws StorageQueryException, UnknownDeviceIdHash, DuplicateCodeIdException, DuplicateLinkCodeHashException; - void createUser(UserInfo user) throws StorageQueryException, DuplicateEmailException, DuplicatePhoneNumberException, - DuplicateUserIdException; + UserInfo createUser(TenantIdentifier tenantIdentifier, String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined) + throws StorageQueryException, DuplicateEmailException, DuplicatePhoneNumberException, + DuplicateUserIdException, TenantOrAppNotFoundException; - void deletePasswordlessUser(String userId) throws StorageQueryException; + void deletePasswordlessUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException; - PasswordlessDevice getDevice(String deviceIdHash) throws StorageQueryException; + PasswordlessDevice getDevice(TenantIdentifier tenantIdentifier, String deviceIdHash) throws StorageQueryException; - PasswordlessDevice[] getDevicesByEmail(@Nonnull String email) throws StorageQueryException; + PasswordlessDevice[] getDevicesByEmail(TenantIdentifier tenantIdentifier, @Nonnull String email) + throws StorageQueryException; - PasswordlessDevice[] getDevicesByPhoneNumber(@Nonnull String phoneNumber) throws StorageQueryException; + PasswordlessDevice[] getDevicesByPhoneNumber(TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber) + throws StorageQueryException; - PasswordlessCode[] getCodesOfDevice(String deviceIdHash) throws StorageQueryException; + PasswordlessCode[] getCodesOfDevice(TenantIdentifier tenantIdentifier, String deviceIdHash) + throws StorageQueryException; - PasswordlessCode[] getCodesBefore(long time) throws StorageQueryException; + PasswordlessCode[] getCodesBefore(TenantIdentifier tenantIdentifier, long time) throws StorageQueryException; - PasswordlessCode getCode(String codeId) throws StorageQueryException; + PasswordlessCode getCode(TenantIdentifier tenantIdentifier, String codeId) throws StorageQueryException; - PasswordlessCode getCodeByLinkCodeHash(String linkCode) throws StorageQueryException; + PasswordlessCode getCodeByLinkCodeHash(TenantIdentifier tenantIdentifier, String linkCode) + throws StorageQueryException; - UserInfo getUserById(String userId) throws StorageQueryException; + UserInfo getUserById(AppIdentifier appIdentifier, String userId) throws StorageQueryException; - UserInfo getUserByEmail(@Nonnull String email) throws StorageQueryException; + UserInfo getUserByEmail(TenantIdentifier tenantIdentifier, @Nonnull String email) throws StorageQueryException; - UserInfo getUserByPhoneNumber(@Nonnull String phoneNumber) throws StorageQueryException; + UserInfo getUserByPhoneNumber(TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber) + throws StorageQueryException; } \ No newline at end of file diff --git a/src/main/java/io/supertokens/pluginInterface/passwordless/UserInfo.java b/src/main/java/io/supertokens/pluginInterface/passwordless/UserInfo.java index 0bd392db..9e8579b6 100644 --- a/src/main/java/io/supertokens/pluginInterface/passwordless/UserInfo.java +++ b/src/main/java/io/supertokens/pluginInterface/passwordless/UserInfo.java @@ -25,8 +25,8 @@ public class UserInfo extends AuthRecipeUserInfo { public final String email; public final String phoneNumber; - public UserInfo(String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined) { - super(id, timeJoined); + public UserInfo(String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined, String[] tenantIds) { + super(id, timeJoined, tenantIds); if (email == null && phoneNumber == null) { throw new IllegalArgumentException("Both email and phoneNumber cannot be null"); diff --git a/src/main/java/io/supertokens/pluginInterface/passwordless/sqlStorage/PasswordlessSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/passwordless/sqlStorage/PasswordlessSQLStorage.java index b703a075..ef47754f 100644 --- a/src/main/java/io/supertokens/pluginInterface/passwordless/sqlStorage/PasswordlessSQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/passwordless/sqlStorage/PasswordlessSQLStorage.java @@ -16,12 +16,11 @@ package io.supertokens.pluginInterface.passwordless.sqlStorage; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.PasswordlessStorage; @@ -29,32 +28,57 @@ import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + public interface PasswordlessSQLStorage extends PasswordlessStorage, SQLStorage { - PasswordlessDevice getDevice_Transaction(TransactionConnection con, String deviceIdHash) + PasswordlessDevice getDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String deviceIdHash) + throws StorageQueryException; + + void incrementDeviceFailedAttemptCount_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String deviceIdHash) throws StorageQueryException; - void incrementDeviceFailedAttemptCount_Transaction(TransactionConnection con, String deviceIdHash) + PasswordlessCode[] getCodesOfDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String deviceIdHash) throws StorageQueryException; - PasswordlessCode[] getCodesOfDevice_Transaction(TransactionConnection con, String deviceIdHash) + void deleteDevice_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String deviceIdHash) throws StorageQueryException; - void deleteDevice_Transaction(TransactionConnection con, String deviceIdHash) throws StorageQueryException; + // we have deleteDevicesBy* for tenantIdentifier and for appIdentifier cause the tenantIdentifier version is + // used when trying to log into one specific tenant. But if the user's detail is updated, then this + // would affect all the tenants that share that userId. + + void deleteDevicesByPhoneNumber_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String phoneNumber) + throws StorageQueryException; - void deleteDevicesByPhoneNumber_Transaction(TransactionConnection con, String phoneNumber) + void deleteDevicesByEmail_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String email) throws StorageQueryException; - void deleteDevicesByEmail_Transaction(TransactionConnection con, String email) throws StorageQueryException; + void deleteDevicesByPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String phoneNumber, String userId) + throws StorageQueryException; - PasswordlessCode getCodeByLinkCodeHash_Transaction(TransactionConnection con, String linkCodeHash) + void deleteDevicesByEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String email, + String userId) throws StorageQueryException; - void deleteCode_Transaction(TransactionConnection con, String codeId) throws StorageQueryException; + PasswordlessCode getCodeByLinkCodeHash_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String linkCodeHash) + throws StorageQueryException; + + void deleteCode_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String codeId) + throws StorageQueryException; - void updateUserEmail_Transaction(TransactionConnection con, @Nonnull String userId, @Nullable String email) + void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, @Nonnull String userId, + @Nullable String email) throws StorageQueryException, UnknownUserIdException, DuplicateEmailException; - void updateUserPhoneNumber_Transaction(TransactionConnection con, @Nonnull String userId, - @Nullable String phoneNumber) + void updateUserPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + @Nonnull String userId, + @Nullable String phoneNumber) throws StorageQueryException, UnknownUserIdException, DuplicatePhoneNumberException; } diff --git a/src/main/java/io/supertokens/pluginInterface/session/SessionStorage.java b/src/main/java/io/supertokens/pluginInterface/session/SessionStorage.java index 2449ca7c..02df9077 100644 --- a/src/main/java/io/supertokens/pluginInterface/session/SessionStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/session/SessionStorage.java @@ -17,31 +17,42 @@ package io.supertokens.pluginInterface.session; import com.google.gson.JsonObject; -import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; import javax.annotation.Nullable; public interface SessionStorage extends NonAuthRecipeStorage { - void createNewSession(String sessionHandle, String userId, String refreshTokenHash2, JsonObject userDataInDatabase, - long expiry, JsonObject userDataInJWT, long createdAtTime, boolean useStaticKey) throws StorageQueryException; + void createNewSession(TenantIdentifier tenantIdentifier, String sessionHandle, String userId, + String refreshTokenHash2, JsonObject userDataInDatabase, + long expiry, JsonObject userDataInJWT, long createdAtTime, boolean useStaticKey) + throws StorageQueryException, + TenantOrAppNotFoundException; - void deleteSessionsOfUser(String userId) throws StorageQueryException; + void deleteSessionsOfUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException; + + boolean deleteSessionsOfUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException; // return number of rows else throw UnsupportedOperationException - int getNumberOfSessions() throws StorageQueryException; + int getNumberOfSessions(TenantIdentifier tenantIdentifier) throws StorageQueryException; - int deleteSession(String[] sessionHandles) throws StorageQueryException; + int deleteSession(TenantIdentifier tenantIdentifier, String[] sessionHandles) throws StorageQueryException; - String[] getAllNonExpiredSessionHandlesForUser(String userId) throws StorageQueryException; + String[] getAllNonExpiredSessionHandlesForUser(TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException; + // we purposely do not add TenantIdentifier to this query cause + // this is called from a cronjob that runs per user pool ID void deleteAllExpiredSessions() throws StorageQueryException; - SessionInfo getSession(String sessionHandle) throws StorageQueryException; + SessionInfo getSession(TenantIdentifier tenantIdentifier, String sessionHandle) throws StorageQueryException; - int updateSession(String sessionHandle, @Nullable JsonObject sessionData, @Nullable JsonObject jwtPayload) + int updateSession(TenantIdentifier tenantIdentifier, String sessionHandle, @Nullable JsonObject sessionData, + @Nullable JsonObject jwtPayload) throws StorageQueryException; - void removeAccessTokenSigningKeysBefore(long time) throws StorageQueryException; + void removeAccessTokenSigningKeysBefore(AppIdentifier appIdentifier, long time) throws StorageQueryException; } diff --git a/src/main/java/io/supertokens/pluginInterface/session/sqlStorage/SessionSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/session/sqlStorage/SessionSQLStorage.java index 003b92c7..c84bc3a2 100644 --- a/src/main/java/io/supertokens/pluginInterface/session/sqlStorage/SessionSQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/session/sqlStorage/SessionSQLStorage.java @@ -18,6 +18,9 @@ import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.session.SessionInfo; import io.supertokens.pluginInterface.session.SessionStorage; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; @@ -25,23 +28,31 @@ public interface SessionSQLStorage extends SessionStorage, SQLStorage { - void removeLegacyAccessTokenSigningKey_Transaction(TransactionConnection con) throws StorageQueryException; - - KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(TransactionConnection con) throws StorageQueryException; + void removeLegacyAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con) + throws StorageQueryException; - KeyValueInfo[] getAccessTokenSigningKeys_Transaction(TransactionConnection con) throws StorageQueryException; + KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, + TransactionConnection con) throws StorageQueryException; - void addAccessTokenSigningKey_Transaction(TransactionConnection con, KeyValueInfo info) + KeyValueInfo[] getAccessTokenSigningKeys_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException; - KeyValueInfo getRefreshTokenSigningKey_Transaction(TransactionConnection con) throws StorageQueryException; + void addAccessTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + KeyValueInfo info) + throws StorageQueryException, TenantOrAppNotFoundException; - void setRefreshTokenSigningKey_Transaction(TransactionConnection con, KeyValueInfo info) + KeyValueInfo getRefreshTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con) throws StorageQueryException; - SessionInfo getSessionInfo_Transaction(TransactionConnection con, String sessionHandle) + void setRefreshTokenSigningKey_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + KeyValueInfo info) + throws StorageQueryException, TenantOrAppNotFoundException; + + SessionInfo getSessionInfo_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String sessionHandle) throws StorageQueryException; - void updateSessionInfo_Transaction(TransactionConnection con, String sessionHandle, String refreshTokenHash2, - long expiry) throws StorageQueryException; + void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String sessionHandle, String refreshTokenHash2, + long expiry) throws StorageQueryException; } diff --git a/src/main/java/io/supertokens/pluginInterface/sqlStorage/SQLStorage.java b/src/main/java/io/supertokens/pluginInterface/sqlStorage/SQLStorage.java index c4b33ec2..d1351b30 100644 --- a/src/main/java/io/supertokens/pluginInterface/sqlStorage/SQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/sqlStorage/SQLStorage.java @@ -21,6 +21,8 @@ import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; public interface SQLStorage extends Storage { T startTransaction(TransactionLogic logic, TransactionIsolationLevel isolationLevel) @@ -30,9 +32,11 @@ T startTransaction(TransactionLogic logic, TransactionIsolationLevel isol void commitTransaction(TransactionConnection con) throws StorageQueryException; - void setKeyValue_Transaction(TransactionConnection con, String key, KeyValueInfo info) throws StorageQueryException; + void setKeyValue_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String key, + KeyValueInfo info) throws StorageQueryException, TenantOrAppNotFoundException; - KeyValueInfo getKeyValue_Transaction(TransactionConnection con, String key) throws StorageQueryException; + KeyValueInfo getKeyValue_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String key) + throws StorageQueryException; interface TransactionLogic { T mainLogicAndCommit(TransactionConnection con) throws StorageQueryException, StorageTransactionLogicException; diff --git a/src/main/java/io/supertokens/pluginInterface/thirdparty/ThirdPartyStorage.java b/src/main/java/io/supertokens/pluginInterface/thirdparty/ThirdPartyStorage.java index 0b820046..22f51256 100644 --- a/src/main/java/io/supertokens/pluginInterface/thirdparty/ThirdPartyStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/thirdparty/ThirdPartyStorage.java @@ -18,6 +18,9 @@ import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException; @@ -25,24 +28,17 @@ public interface ThirdPartyStorage extends AuthRecipeStorage { - void signUp(UserInfo userInfo) - throws StorageQueryException, DuplicateUserIdException, DuplicateThirdPartyUserException; + UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String email, UserInfo.ThirdParty thirdParty, long timeJoined) + throws StorageQueryException, DuplicateUserIdException, DuplicateThirdPartyUserException, + TenantOrAppNotFoundException; - void deleteThirdPartyUser(String userId) throws StorageQueryException; + void deleteThirdPartyUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException; - UserInfo getThirdPartyUserInfoUsingId(String thirdPartyId, String thirdPartyUserId) throws StorageQueryException; + UserInfo getThirdPartyUserInfoUsingId(TenantIdentifier tenantIdentifier, String thirdPartyId, + String thirdPartyUserId) throws StorageQueryException; - UserInfo getThirdPartyUserInfoUsingId(String userId) throws StorageQueryException; + UserInfo getThirdPartyUserInfoUsingId(AppIdentifier appIdentifier, String userId) throws StorageQueryException; - @Deprecated - UserInfo[] getThirdPartyUsers(@Nonnull String userId, @Nonnull Long timeJoined, @Nonnull Integer limit, - @Nonnull String timeJoinedOrder) throws StorageQueryException; - - @Deprecated - UserInfo[] getThirdPartyUsers(@Nonnull Integer limit, @Nonnull String timeJoinedOrder) throws StorageQueryException; - - @Deprecated - long getThirdPartyUsersCount() throws StorageQueryException; - - UserInfo[] getThirdPartyUsersByEmail(@Nonnull String email) throws StorageQueryException; + UserInfo[] getThirdPartyUsersByEmail(TenantIdentifier tenantIdentifier, @Nonnull String email) + throws StorageQueryException; } \ No newline at end of file diff --git a/src/main/java/io/supertokens/pluginInterface/thirdparty/UserInfo.java b/src/main/java/io/supertokens/pluginInterface/thirdparty/UserInfo.java index b1d2d5cb..ef85520e 100644 --- a/src/main/java/io/supertokens/pluginInterface/thirdparty/UserInfo.java +++ b/src/main/java/io/supertokens/pluginInterface/thirdparty/UserInfo.java @@ -25,8 +25,8 @@ public class UserInfo extends AuthRecipeUserInfo { public final String email; - public UserInfo(String id, String email, ThirdParty thirdParty, long timeJoined) { - super(id, timeJoined); + public UserInfo(String id, String email, ThirdParty thirdParty, long timeJoined, String[] tenantIds) { + super(id, timeJoined, tenantIds); this.thirdParty = thirdParty; this.email = email; } diff --git a/src/main/java/io/supertokens/pluginInterface/thirdparty/sqlStorage/ThirdPartySQLStorage.java b/src/main/java/io/supertokens/pluginInterface/thirdparty/sqlStorage/ThirdPartySQLStorage.java index e0fdd4ba..08260679 100644 --- a/src/main/java/io/supertokens/pluginInterface/thirdparty/sqlStorage/ThirdPartySQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/thirdparty/sqlStorage/ThirdPartySQLStorage.java @@ -17,6 +17,7 @@ package io.supertokens.pluginInterface.thirdparty.sqlStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; import io.supertokens.pluginInterface.thirdparty.ThirdPartyStorage; @@ -24,9 +25,11 @@ public interface ThirdPartySQLStorage extends ThirdPartyStorage, SQLStorage { - UserInfo getUserInfoUsingId_Transaction(TransactionConnection con, String thirdPartyId, String thirdPartyUserId) + UserInfo getUserInfoUsingId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String thirdPartyId, String thirdPartyUserId) throws StorageQueryException; - void updateUserEmail_Transaction(TransactionConnection con, String thirdPartyId, String thirdPartyUserId, - String newEmail) throws StorageQueryException; + void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String thirdPartyId, + String thirdPartyUserId, + String newEmail) throws StorageQueryException; } diff --git a/src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java b/src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java index 5709efc4..e29f091e 100644 --- a/src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java +++ b/src/main/java/io/supertokens/pluginInterface/totp/TOTPDevice.java @@ -8,7 +8,8 @@ public class TOTPDevice { public final int skew; public final boolean verified; - public TOTPDevice(String userId, String deviceName, String secretKey, int period, int skew, boolean verified) { + public TOTPDevice(String userId, String deviceName, String secretKey, int period, + int skew, boolean verified) { this.userId = userId; this.deviceName = deviceName; this.secretKey = secretKey; @@ -29,7 +30,8 @@ public boolean equals(Object obj) { return false; } TOTPDevice other = (TOTPDevice) obj; - return this.userId.equals(other.userId) && this.deviceName.equals(other.deviceName) + return this.userId.equals(other.userId) && + this.deviceName.equals(other.deviceName) && this.secretKey.equals(other.secretKey) && this.period == other.period && this.skew == other.skew && this.verified == other.verified; } diff --git a/src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java b/src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java index 3618de21..ab8cbbae 100644 --- a/src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/totp/TOTPStorage.java @@ -1,29 +1,42 @@ package io.supertokens.pluginInterface.totp; -import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; public interface TOTPStorage extends NonAuthRecipeStorage { - /** Create a new device and a new user if the user does not exist: */ - void createDevice(TOTPDevice device) - throws StorageQueryException, DeviceAlreadyExistsException; + /** + * Create a new device and a new user if the user does not exist: + */ + void createDevice(AppIdentifier appIdentifier, TOTPDevice device) + throws StorageQueryException, DeviceAlreadyExistsException, TenantOrAppNotFoundException; - /** Verify a user's device with the given name: */ - void markDeviceAsVerified(String userId, String deviceName) - throws StorageQueryException, UnknownDeviceException; + /** + * Verify a user's device with the given name: + */ + void markDeviceAsVerified(AppIdentifier appIdentifier, String userId, String deviceName) + throws StorageQueryException, UnknownDeviceException; - /** Update device name of a device: */ - void updateDeviceName(String userId, String oldDeviceName, String newDeviceName) - throws StorageQueryException, DeviceAlreadyExistsException, - UnknownDeviceException; + /** + * Update device name of a device: + */ + void updateDeviceName(AppIdentifier appIdentifier, String userId, String oldDeviceName, String newDeviceName) + throws StorageQueryException, DeviceAlreadyExistsException, + UnknownDeviceException; - /** Get the devices for a user */ - TOTPDevice[] getDevices(String userId) - throws StorageQueryException; + /** + * Get the devices for a user + */ + TOTPDevice[] getDevices(AppIdentifier appIdentifier, String userId) + throws StorageQueryException; - /** Remove expired codes from totp used codes for all users: */ - int removeExpiredCodes(long expiredBefore) - throws StorageQueryException; + /** + * Remove expired codes from totp used codes for all users: + */ + int removeExpiredCodes(TenantIdentifier tenantIdentifier, long expiredBefore) + throws StorageQueryException; } diff --git a/src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java b/src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java index d2fb9a74..62ff6ea2 100644 --- a/src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java +++ b/src/main/java/io/supertokens/pluginInterface/totp/TOTPUsedCode.java @@ -7,7 +7,9 @@ public class TOTPUsedCode { public final long expiryTime; public final long createdTime; - public TOTPUsedCode(String userId, String code, Boolean isValidCode, long expiryTime, long createdTime) { + public TOTPUsedCode(String userId, String code, Boolean isValidCode, + long expiryTime, + long createdTime) { this.userId = userId; this.code = code; this.isValid = isValidCode; diff --git a/src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java index d9261de4..55d46060 100644 --- a/src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/totp/sqlStorage/TOTPSQLStorage.java @@ -1,6 +1,9 @@ package io.supertokens.pluginInterface.totp.sqlStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; import io.supertokens.pluginInterface.totp.TOTPDevice; @@ -10,13 +13,17 @@ import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; public interface TOTPSQLStorage extends TOTPStorage, SQLStorage { - public int deleteDevice_Transaction(TransactionConnection con, String userId, String deviceName) + public int deleteDevice_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, + String deviceName) throws StorageQueryException; - public TOTPDevice[] getDevices_Transaction(TransactionConnection con, String userId) + public TOTPDevice[] getDevices_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException; - public void removeUser_Transaction(TransactionConnection con, String userId) + public void removeUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException; + + public boolean removeUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException; /** @@ -24,11 +31,14 @@ public void removeUser_Transaction(TransactionConnection con, String userId) * order of created time): */ public TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(TransactionConnection con, - String userId) + TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException; - /** Insert a used TOTP code for an existing user: */ - void insertUsedCode_Transaction(TransactionConnection con, TOTPUsedCode code) - throws StorageQueryException, TotpNotEnabledException, UsedCodeAlreadyExistsException; + /** + * Insert a used TOTP code for an existing user: + */ + void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifier tenantIdentifier, TOTPUsedCode code) + throws StorageQueryException, TotpNotEnabledException, UsedCodeAlreadyExistsException, + TenantOrAppNotFoundException; } diff --git a/src/main/java/io/supertokens/pluginInterface/useridmapping/UserIdMappingStorage.java b/src/main/java/io/supertokens/pluginInterface/useridmapping/UserIdMappingStorage.java index 2f68c51c..7b1ebd4c 100644 --- a/src/main/java/io/supertokens/pluginInterface/useridmapping/UserIdMappingStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/useridmapping/UserIdMappingStorage.java @@ -18,6 +18,7 @@ import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.useridmapping.exception.UnknownSuperTokensUserIdException; import io.supertokens.pluginInterface.useridmapping.exception.UserIdMappingAlreadyExistsException; @@ -27,17 +28,24 @@ public interface UserIdMappingStorage extends Storage { - void createUserIdMapping(String superTokensUserId, String externalUserId, @Nullable String externalUserIdInfo) + // whilst these take the full tenantIdentifier as an input, they ignore the tenantId + // cause user ID mapping is per app and not per tenant. + + void createUserIdMapping(AppIdentifier appIdentifier, String superTokensUserId, String externalUserId, + @Nullable String externalUserIdInfo) throws StorageQueryException, UnknownSuperTokensUserIdException, UserIdMappingAlreadyExistsException; - boolean deleteUserIdMapping(String userId, boolean isSuperTokensUserId) throws StorageQueryException; + boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) + throws StorageQueryException; - UserIdMapping getUserIdMapping(String userId, boolean isSuperTokensUserId) throws StorageQueryException; + UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) + throws StorageQueryException; - UserIdMapping[] getUserIdMapping(String userId) throws StorageQueryException; + UserIdMapping[] getUserIdMapping(AppIdentifier appIdentifier, String userId) throws StorageQueryException; - boolean updateOrDeleteExternalUserIdInfo(String userId, boolean isSuperTokensUserId, - @Nullable String externalUserIdInfo) throws StorageQueryException; + boolean updateOrDeleteExternalUserIdInfo(AppIdentifier appIdentifier, String userId, + boolean isSuperTokensUserId, + @Nullable String externalUserIdInfo) throws StorageQueryException; // This function will be used to retrieve the userId mapping for a list of userIds. The key of the HashMap will be // superTokensUserId and the value will be the externalUserId. If a mapping does not exist for an input userId, diff --git a/src/main/java/io/supertokens/pluginInterface/usermetadata/UserMetadataStorage.java b/src/main/java/io/supertokens/pluginInterface/usermetadata/UserMetadataStorage.java index 560f77cf..d25b2cfc 100644 --- a/src/main/java/io/supertokens/pluginInterface/usermetadata/UserMetadataStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/usermetadata/UserMetadataStorage.java @@ -17,12 +17,12 @@ package io.supertokens.pluginInterface.usermetadata; import com.google.gson.JsonObject; -import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; public interface UserMetadataStorage extends NonAuthRecipeStorage { - JsonObject getUserMetadata(String userId) throws StorageQueryException; + JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException; - int deleteUserMetadata(String userId) throws StorageQueryException; + int deleteUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException; } diff --git a/src/main/java/io/supertokens/pluginInterface/usermetadata/sqlStorage/UserMetadataSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/usermetadata/sqlStorage/UserMetadataSQLStorage.java index 7ffe8f21..99d5aaf2 100644 --- a/src/main/java/io/supertokens/pluginInterface/usermetadata/sqlStorage/UserMetadataSQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/usermetadata/sqlStorage/UserMetadataSQLStorage.java @@ -17,15 +17,18 @@ package io.supertokens.pluginInterface.usermetadata.sqlStorage; import com.google.gson.JsonObject; - import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; import io.supertokens.pluginInterface.usermetadata.UserMetadataStorage; public interface UserMetadataSQLStorage extends UserMetadataStorage, SQLStorage { - JsonObject getUserMetadata_Transaction(TransactionConnection con, String userId) throws StorageQueryException; - - int setUserMetadata_Transaction(TransactionConnection con, String userId, JsonObject metadata) + JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId) throws StorageQueryException; + + int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + JsonObject metadata) + throws StorageQueryException, TenantOrAppNotFoundException; } diff --git a/src/main/java/io/supertokens/pluginInterface/userroles/UserRolesStorage.java b/src/main/java/io/supertokens/pluginInterface/userroles/UserRolesStorage.java index ddf91633..e080df6f 100644 --- a/src/main/java/io/supertokens/pluginInterface/userroles/UserRolesStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/userroles/UserRolesStorage.java @@ -16,9 +16,10 @@ package io.supertokens.pluginInterface.userroles; -import com.google.gson.JsonObject; -import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; import io.supertokens.pluginInterface.userroles.exception.DuplicateUserRoleMappingException; import io.supertokens.pluginInterface.userroles.exception.UnknownRoleException; @@ -26,30 +27,33 @@ public interface UserRolesStorage extends NonAuthRecipeStorage { // associate a userId with a role that exists - void addRoleToUser(String userId, String role) - throws StorageQueryException, UnknownRoleException, DuplicateUserRoleMappingException; + void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, String role) + throws StorageQueryException, UnknownRoleException, DuplicateUserRoleMappingException, + TenantOrAppNotFoundException; // get all roles associated with the input userId - String[] getRolesForUser(String userId) throws StorageQueryException; + String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException; // get all users associated with the input role - String[] getUsersForRole(String role) throws StorageQueryException; + String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role) throws StorageQueryException; // get permissions associated with the input role - String[] getPermissionsForRole(String role) throws StorageQueryException; + String[] getPermissionsForRole(AppIdentifier appIdentifier, String role) throws StorageQueryException; // get roles associated with the input permission - String[] getRolesThatHavePermission(String permission) throws StorageQueryException; + String[] getRolesThatHavePermission(AppIdentifier appIdentifier, String permission) throws StorageQueryException; // delete a role - boolean deleteRole(String role) throws StorageQueryException; + boolean deleteRole(AppIdentifier appIdentifier, String role) throws StorageQueryException; // get all created roles - String[] getRoles() throws StorageQueryException; + String[] getRoles(AppIdentifier appIdentifier) throws StorageQueryException; // check if input roles exists - boolean doesRoleExist(String role) throws StorageQueryException; + boolean doesRoleExist(AppIdentifier appIdentifier, String role) throws StorageQueryException; // delete all roles for the input userId - int deleteAllRolesForUser(String userId) throws StorageQueryException; + int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException; + + void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException; } diff --git a/src/main/java/io/supertokens/pluginInterface/userroles/sqlStorage/UserRolesSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/userroles/sqlStorage/UserRolesSQLStorage.java index 0a51a071..a887ee33 100644 --- a/src/main/java/io/supertokens/pluginInterface/userroles/sqlStorage/UserRolesSQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/userroles/sqlStorage/UserRolesSQLStorage.java @@ -17,6 +17,9 @@ package io.supertokens.pluginInterface.userroles.sqlStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; import io.supertokens.pluginInterface.userroles.UserRolesStorage; @@ -25,24 +28,29 @@ public interface UserRolesSQLStorage extends UserRolesStorage, SQLStorage { // delete role associated with the input userId from the input roles - boolean deleteRoleForUser_Transaction(TransactionConnection con, String userId, String role) + boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId, + String role) throws StorageQueryException; - // create a new role if it doesnt exist - boolean createNewRoleOrDoNothingIfExists_Transaction(TransactionConnection con, String role) - throws StorageQueryException; + boolean createNewRoleOrDoNothingIfExists_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String role) + throws StorageQueryException, TenantOrAppNotFoundException; // associate a permission with a role - void addPermissionToRoleOrDoNothingIfExists_Transaction(TransactionConnection con, String role, String permission) + void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String role, String permission) throws StorageQueryException, UnknownRoleException; // delete a permission associated with the input role - boolean deletePermissionForRole_Transaction(TransactionConnection con, String role, String permission) + boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role, + String permission) throws StorageQueryException; // delete all permissions associated with the input role - int deleteAllPermissionsForRole_Transaction(TransactionConnection con, String role) throws StorageQueryException; + int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) + throws StorageQueryException; // check if a role exists - boolean doesRoleExist_Transaction(TransactionConnection con, String role) throws StorageQueryException; + boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) + throws StorageQueryException; } From ba06ad8e53fb931416961f9e9abd3d77a47bd47f Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 5 Jun 2023 12:31:51 +0530 Subject: [PATCH 09/12] adding dev-v3.0.0 tag to this commit to ensure building --- jar/plugin-interface-3.0.0.jar | Bin 88223 -> 88223 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/plugin-interface-3.0.0.jar b/jar/plugin-interface-3.0.0.jar index 369781ee539b4ed1a90c6d8034d58f36c9e0371f..c1e0ea425565448c76114c6962ba3eb1c3949591 100644 GIT binary patch delta 3412 zcmZWr2~?EV6`sqm$$$vT9yZC0pa_GqI4lDwFzf;fxE3@jwE>|inA8#!5>yfw@L6A7 z8cB@Nn504{f;KLYsxeZHIf<<{?ZMPTQWIkcO-~|~)4sX);z{G21OGR7`R@AOKL@)V z4t6`Z73FE|T{IeJXU+FdTThjrv{v)~4zER`!}Vm_ zLj+ljveOS-L?O7^1lHv+%g*D+RI|mWVFuad3zOttdo~=>z5?j+g2NV=X!H2Xb01d+ zf=1H^qJ6ENXoKxtWRf5FTI-yqp}E*!gb}^Opfg3h=bUGUxg_h-kZ_}Jgaq{yzd~YT zjH`B;BCfF$Y2|uxT<2^(;NC)Xp&ohYRO|5~r8DN~sePmXqe%BP$i;!+CZ6eWMbejz z9}likq+@m>c$Hq327-q?9|(cgKJU75>R7oW2y`|VjM41-4aI2i@6=+-gD0^Vd(jo< z;{KyfTUQ5FVI9^FLXJ^EjbRIs=%cXLiD;w#c`|Vf?;y#(i0jnN`ds(SK9yg%RvaJD zi^~zN*5Az-B%}73RSrlrI?Iok`^-ks9;XxjS(IvJ94V}g316U(NE*ricwm#L<@YK7}hy`6*fSnJpOBCG{e>*+#y?BfHL|lJpekOa?J**2$u9 z2(U(^&m+&;xo;2%%s5RHs$;@6%OATJku`$PAWIg~R#~`VALhi`qr|zeo$A|PRE&bJ ziv$t2mb4S_UD!&Xvve1MxN<>G1B=Nic*#}L3@>d%URyK7wOwxFR3(*qsWJx#YMrub z9d)*KttU`^$fjg< zl|*$>Q&YN{DW(4I6$JFwZM57iRVAhXRpbS_DjxdE%_-^xPCB7SdoiacACuYAQ%Zpb zPv)W^^3;72+`43z?Ww9qs)eZ8bb2?%@;SSgfc2atP|$yj!05Ys2#E8%;T0E~#`#K- zX3qP8B(QDD)&V~hzB;fP{e(JgG9(jpkDR5lD=X2>`pyUQsrhanT_zvfy%%BuFA;p5 zGL5?4Mp^vf&-)21xvu;2!w6t>xSHf zZcHc6J)l-xeQ=w){q}zgZPX_m;4WJCehW~BDp_C;i)^~yqxR5)O1Z)j7Nh8lBRG=P zMQ6xBk=q1#$_@|d4nv`~K-DzUNmQojW!+?OmMLX0rT3~1$|b(JXR`z=X-u}|p z2Y!aCRv-8Yg3o-Q6~Q`Rcm}~8Us#V|Qx*isXZ@%udESrpss4bSlI{Mm8V$eqhgAqF z0$?42YXQ_A8TlBK9exaIQRNc|OA#~#GU0F_8|MeXQ>f|>f^r0#%qoTWU`WAl>w{VU zWiUL6lEopg62aLJXhV=23NIqK5eiLYGZmI0I60NQ3}GCwWgNszQ*rKxao^$}hiddV z{5aGg@Xt_sqaN^=i>%c{9Z`oU@t7X))g&zjj(EucYf!If@l4CMY49tQIEAz8hH#$5 z=i%^kRAH^_Be>R{i1A){s~B0+!GuOF)7kCo>F^v%8fHKh`P;@dI)lfR9tn*o?i&ZG zGvR5JoSF&GA}~eqxNy=JqS)0nng_Eb`iJFv8oB&pBm5HmuNc`dJBFv+AHz}8X0dD6 zENDRE@Y$UCuV=$rltAnc+qg59+juh;Hlu!J9Be``6bH{EsE%jwuXuJ{lK{=A`XYe? zE}6rN_NO^K9Cz5 zr?XAwT&PFM>AAcbG4tRFl*oDPuJ)7;PJIUNI&St@25)v^CU5rZne6JGHJ)o0ci_S|8cFS7jMD^4&7dC8L~~ O`^#ZDx`!@;rvCv>2aZGl delta 3412 zcmZWr2~?EV6`sqm$*>5@o?(&92#PQ$i^DR2vTp(kxWs~1r8Xcm1x+nMAweZ^0iX5N zC6UA!jcF?MkD!eU7-Ni7V@_hLO?xo)kkrH&LerB<<+ShJ`>;L9oCEXycfa4g@BMSA z$Kgi3hvV5 zPtSf_V+5V9A6Um)6NL@7ci~C?;HT9)O-FNfpcpfHvms~7c;7kSPL{-y4QcXQ+hiCe zL4*5O?m|#*iE-6GAp+Ohv9t;kJ7I9v4tlf#I)!@Xqf?#d3nMpo3UrzVJ;ruV_I7sRBeY0eGqaS0c#3dgo2O4UZ+5tOwW;tV|XV?_D9?R ztgj)@BeP%B7p`X~22Jcrgsb-3nL}jMF{|1ED;k^aPm%j=7S<7GU;$Y~YE>L5w5EhF z(1)cBTX={W+aC(kFLvH)W+OHOFAj%5Eh2qBdDhK)gMcyP3?&haG1sgBWG^CX6u*PAvoLIRwj%7q z+*o^*I2UzLe+P<7Q1EpzBMRC|I|%qKY9r89wwpj)1tX`yCFB&m^crbKmbKgC)(A4# z_1$jlbQMkZa#b!4RGYeL9WmRs*5HV9C`g^!(kaf8!HIR0nxvO@y{=!6x<1<^htI^0 zt#V|=ca`qvjuh$E`u7~L*GJNZ<7YrR&;VLlTcYu6TO1puIk%O4xUp zdiv{2&ZPQ#*Chfyhg-1{E&eqkj}0D8#~MA3jnF;t)-vC1yF`zX_BvfG`{vD=TGAUw z$m!0Tlc)(7oQD|GK_{IqQQUUZguS=6;nQHMXWyMyDN@>SE{5Jd+)EKM z@9x)Qgt1!l?W1BQpNJ?KtYSDK;baJ)(D%JS%v31Tra&Hvc6dXq z6!DARkSf!1M<0knkl+J}sA}*5+*v;019!UkU!&3UGa>J5Cj!ef=!eV9}Fq@v_4qrzYK;aQL-ciRw6hT0__O$Lg57j zH$$PBY^H&@s6+{;rYSFTm`d0xf#fb`y2$e&Oz#WT&@r^7GN!zo<2N}G-0ViFR39u7Z4)n?i2 z`Uur)Z^Q%_K0=%|1FUG&IztWc>lyGYN*ZTEHTlbNjm=c!N{@sl6!%Yn)LF0rC8uY> zGYG6vVqCIeob|;uZKuyrOYU0<@s& ziv(5c(z$BU{xDZfIov81K(@Kns~nN?8hkK^M$(_R0V>D47KaH+2a{%vlBDbX1|`P zTs^WTYRys%{4Gn3e|fgL8iun~&#QA3IhS)_BWA|%Rk6wq^tR)=5YmTUyc-r@kyq93)P69FH|$SU#OKRE2S!G_ChtR?uD=iYsp@u&REYP*oJwM%K+b# myt7OZ`CXYh^i}18NLkB;a#4!}85JV31OpYY96{(}X#Ov~jd?Wy From 44a65f08ad4e0166ef3f98f524a2992f90780207 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 9 Jun 2023 15:14:56 +0530 Subject: [PATCH 10/12] fix: appid fk test (#103) --- src/main/java/io/supertokens/pluginInterface/Storage.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/io/supertokens/pluginInterface/Storage.java b/src/main/java/io/supertokens/pluginInterface/Storage.java index 68c68f05..881d96c3 100644 --- a/src/main/java/io/supertokens/pluginInterface/Storage.java +++ b/src/main/java/io/supertokens/pluginInterface/Storage.java @@ -90,4 +90,8 @@ boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, String cla Set getValidFieldsInConfig(); void setLogLevels(Set logLevels); + + String[] getAllTablesInTheDatabase() throws StorageQueryException; + + String[] getAllTablesInTheDatabaseThatHasDataForAppId(String appId) throws StorageQueryException; } From 6e3861004386096a57da508d295afbf3bf9fc204 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 9 Jun 2023 15:58:55 +0530 Subject: [PATCH 11/12] fix: isTesting in constructor (#104) * fix: appid fk test * fix: pr comment --- src/main/java/io/supertokens/pluginInterface/Storage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/pluginInterface/Storage.java b/src/main/java/io/supertokens/pluginInterface/Storage.java index 881d96c3..54154645 100644 --- a/src/main/java/io/supertokens/pluginInterface/Storage.java +++ b/src/main/java/io/supertokens/pluginInterface/Storage.java @@ -30,7 +30,7 @@ public interface Storage { // if silent is true, do not log anything out on the console - void constructor(String processId, boolean silent); + void constructor(String processId, boolean silent, boolean isTesting); void loadConfig(JsonObject jsonConfig, Set logLevels, TenantIdentifier tenantIdentifier) throws InvalidConfigException; From c9bd5170fc007536d99e053d47734c8abcf4062a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 15 Jun 2023 13:22:47 +0530 Subject: [PATCH 12/12] adding dev-v3.0.0 tag to this commit to ensure building --- jar/plugin-interface-3.0.0.jar | Bin 88223 -> 88276 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jar/plugin-interface-3.0.0.jar b/jar/plugin-interface-3.0.0.jar index c1e0ea425565448c76114c6962ba3eb1c3949591..4e092b10d7a2c074e97e985f5bed155f1785006c 100644 GIT binary patch delta 7352 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 OD;|r6sK^aC&;J9HqY{Mx delta 7122 zcmZu#30#fY`@iRwRJYQ;Z@0QF+O8I*w9%q{C9)KQ##WigE-|)|Qpa>gSq2SZP;*UT zl5S`;l$13_V+`}Nl$q>9`k&>!uFL;^KD@{CefH-$&pF4HnnAm225BZlD=Dk}0pW#vV_%n`&e|+*6{wd)msQK{)wJCIb(?&ZinY zVTiE}v{X}vR?6z|jjaY{y|1(f(8V`{)&ngfbA8Q~xfX!OYXq>pTO<=eIVQkZk~7$W za>MonF-8nvw<=T9s~XLxE{qfeQ+4@*K1Nue)k6u>hn!@hLY-(z$ky4!Ecff`Dt+dy z7<+4viY$Pp=_fId+bzD*Jrg_F&xh%0BJ_;~&_Qo3fQ6*dFh^#>3uYP{Q8NX`{{l@C z*YB^d?wo+r=m_&oz4FIS%5mkt-JfrzGwF}Ro4IT;Ih);a+{gfJC;^W%=&Sm%jM0=bu`O`jni}aU((EQv*DPAv{&!88N!hfDJ<>vbH#m!&t-1ps& zb6ihtkQPenPX%hkm6)up+Tb2`-LpZO{FS%VIqT%V*L8pITOxS7;bD2(*rY9c_cXOV zEmyzrzvBr_SH3=bvgqFJ%vtNpZo1VpBpDS*s;fV;T>sy<-yncLQgtFVd|lm(?V_C zrMW5so1Yh*-!!h|Psx;>X`Kg4x2495^=qFEe^nN?=a*&bbw=A3yYxB!6e)O~x_|$n z?T+3l$X4n0z&E{%Pop$H-PX88Hb}#{_;Ts$ z{??9)mD=`=4Lu@Xki-~W=oVJp4My*RqUGxKk&a4%-x>t`MW51zg$?-8iiAL34({lOy@r>gh ze=RgPq8WK)O~tJ{t1907=lm|bJZt&s&i6xJOHL1LYRI~uUKgaKFr=VPxEK{#0WG-6 zl1+MT38P$cy2=*&oy{0@&NXuom2&qq zVQNBJ#c-K7+xXvlGoqx%=MftVH5p>qHo}C8JQ}f-BieeQ2790nkNPv`8vI+Boo@oZ zWGqB$c8~yygBV9pP&4CD4cW#Pm7@*;I>HV;TfJiqFgEA5JVlW?qpG2jJ25=xr4c@w8B=H;P%6Mm|LaMklEs+{ZNo!+( zXwpV%8rEb)QluY$=^}D^7EAZZSppSGIiKCnp!2z-*+Og4uZ-u$ay_=_EhrvLnOAK* z#X_mE6~mlDJ4!@_xy-C)(M-0;E1sl8&!G)1@dsYxQ*R%D1;qidyHKsn2bf5LAfGXY zZq=arB*|rl%=p}Gdk4``uW`t1@&O$GE-37=ht(T3CGop1Fe!`O%}naS-qma|XJ09W zN~=PsOQBW&Fu>=~9kwt&qDB``s3V5rszI<=LJv?{@vPRz0&{Gu3IBX3_qYu zSWwtVU@mE`wr7@K)=XuKG)W;_JUqs=#k|@WYP!C5EHi|63`{?xHRO|{Pcrqy8^$oX zek@H==;QD8NTsjgB;6Ka857C-`e>#;rh)6u|D8sxJO6RN^s_7G_Q%~hjdUm46%1_9 z`4AwKypZ}ZG50fNY;pc9w-L5BuasF0goj!La7qj7*Lf`#3?Mt})CIP)ZeEx{om$G`R7u+60MKHZ@D|>Vm}tbtReQ$x?NAQwsSob!WRqF9svx<1%*bK|F1Mb zhmw=4IQxN+v{tN_eXISrMf7YnklQhvjTx)T6>=t)Hf|<|S7)c6EdCRU)DW zp6)ZGPTlC^Zd@y#Z)c{>UvdGQd{xCZuti{$hBFRYk59`=#Dm`Iq2dIbnD|CjxbdZq zgxsJCO5en{?hpg0pj}`0TKpb)8XFu?Sa@RTW!MM-w zi~h7h(vyHLvKj=`QJXTLxvTqK8PrliRHgzZ&`n@+?Cl>#*k83lJlC*ovwwB1pEa46(K*hf=!MZY0L#cgHoILRz2uRTeaV;M9i+b`B^<$T^TN#CpCG z$@J*McOoY_s%eAKHx3`8nkWYonGXcr?1WJv9B|S_cWgMNJp`EH%mEt6#}){w#6(*R ztZ^j(+J@KEYv)dt%y#Vc3j8Q=levt)l?6Q>R7JYykRb`tfUmFg;f02lMq*8B-V|Tv zEkKj(INNjVFp&HKfbHH!sMnqYy9k)=Ba_0QI&u{OQ|erNC@=$)P@@RAJAYzsT+{ia zOQzVzD4f|a1s|)30KGIJS`(2!l8S&EeCsbj^JI3H+he=SKOhb$IF$3KpCDrbXh!@U zfFt#U1}6!GQ$t}OH1GsvV-9CXH(-o_G3 zUT{^M`e$uG_B$v-Viz!)YNgjQTqkAhNTPz)KM9i$YHJZfK z#?_O!&Tp7Nvj~gQKI)bbcZ}$!$q+kvx6~8l-b8`~A|o--{m{7l>=f$3r%bx`aHhEfM7dtwL6mdl8mXsIukt=|apV*z!>d9^Mo z^x+zKX4kaP$=HMW*mcD{9=MTmpW0}GE`@RBqnrKxCJL-56rcew4$$((`o|W^0DNJ# zqZ^F|kT*Up<0W>y95>Whg#a#W{zyU{rWfhUx2NfFtkSDk#(;8$b7$vn0>Dxkz+KKo z)YR`!UVtC5nY}n9#c=pri2%y=0V0~K3LmURr;KXZQZ^K6`*NXJ`C?$`Hp-1luPskv zF+tjQVkDd;=F!9#+bczJwH7f)mR_>+5iV?t)66*=kX&)Ue75j|kqebY4frkCC zpxWN$(-~Y{`_l)DG+-^NA3_g?|FH9a?u56Wvj> zjQB4hCN)!Hz-U1Kw-?bsJI@m$d0oj*?~9``w{r^zILgN{xiRpgFVNoJ9Ln9<4iF-u zMEn-hgoYROk*aKJ(35@OB24UbK^Zb)b{Lk3Y^B8GVZfF)$sIyGatRZ?kb5}LrAhqo zV8dZ6R+)z~!nrEbgQfKfvk`%Bo1|~4xcm+G>3kQW%i;KR#1A?)Ja#0Nhm&eqV?${Q z`8|A1u6W)UpoV%QfD`rDbPUPH4F#AD<8p#@-y{f})FnW@W4JczCxBO%41o1rrt<94 zTs^vl=0^b+S`6AFm;Bs3f5%=e!RLfx<-Kkb)=}`++n=8v=cnFbKQ|nb4Gw97f=6Y2Q0Hw3bbMwi-_i%;unPa@RK-9X z*C2NuJu!5_D)g`l#WHlgpep9R{OA^RARen&{E7nVug&DThb3UZ_6-5Zt6C_3NTZ|i zz<}TQABHB$`RS=U`yGulxL*Th*#iNs-S>%DkyHkJbio!TVPO9NDp3ZWpAEfL1bodv z)Sd)fsICD_Rr(tMIRAv`+iYNoGLpHyy*wEY$zTLh@b&A%uFCBR`o#mMA{M7&mO?fw z2q$}?E>FQaQ&gxdNyJ5 zK~bq(4ws~2t!K5V){&?)6SV3iN+lnEV&E_#Hmi ZB0mU_bInsvE?|Qi#sh7Yl?KGO{{ug!CGP+L