Skip to content

Commit fe0cd74

Browse files
Cloud address translation (#830)
## Usage and product changes We introduce a way to provide address translation when attempting to connect to cloud servers (cf. typedb/typedb-driver#624). This is useful when the route from the user to the servers differs from the route the servers are configured with (e.g. connection to public-facing servers from an internal network). Note: we currently require that the user provides translation for the addresses of _all_ nodes in the Cloud deployment. <img width="532" src="https://github.com/vaticle/typedb-studio/assets/18616863/74859fbd-de4f-4844-b1e6-f3507dc364b7">
1 parent 5e9b319 commit fe0cd74

File tree

5 files changed

+157
-37
lines changed

5 files changed

+157
-37
lines changed

module/connection/ServerDialog.kt

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.ui.awt.ComposeDialog
2626
import androidx.compose.ui.focus.FocusRequester
2727
import androidx.compose.ui.focus.focusRequester
2828
import androidx.compose.ui.unit.dp
29+
import com.vaticle.typedb.driver.api.TypeDBCredential
2930
import com.vaticle.typedb.studio.framework.common.theme.Theme
3031
import com.vaticle.typedb.studio.framework.material.ActionableList
3132
import com.vaticle.typedb.studio.framework.material.Dialog
@@ -46,12 +47,13 @@ import com.vaticle.typedb.studio.framework.material.Tooltip
4647
import com.vaticle.typedb.studio.service.Service
4748
import com.vaticle.typedb.studio.service.common.util.Label
4849
import com.vaticle.typedb.studio.service.common.util.Property
49-
import com.vaticle.typedb.studio.service.common.util.Property.Server.TYPEDB_CORE
5050
import com.vaticle.typedb.studio.service.common.util.Property.Server.TYPEDB_CLOUD
51+
import com.vaticle.typedb.studio.service.common.util.Property.Server.TYPEDB_CORE
5152
import com.vaticle.typedb.studio.service.common.util.Sentence
5253
import com.vaticle.typedb.studio.service.connection.DriverState.Status.CONNECTED
5354
import com.vaticle.typedb.studio.service.connection.DriverState.Status.CONNECTING
5455
import com.vaticle.typedb.studio.service.connection.DriverState.Status.DISCONNECTED
56+
import java.nio.file.Path
5557

5658
object ServerDialog {
5759

@@ -69,6 +71,10 @@ object ServerDialog {
6971
var cloudAddresses: MutableList<String> = mutableStateListOf<String>().also {
7072
appData.cloudAddresses?.let { saved -> it.addAll(saved) }
7173
}
74+
var cloudAddressTranslation: MutableList<Pair<String, String>> = mutableStateListOf<Pair<String, String>>().also {
75+
appData.cloudAddressTranslation?.let { saved -> it.addAll(saved) }
76+
}
77+
var useCloudAddressTranslation: Boolean by mutableStateOf(appData.useCloudAddressTranslation ?: false)
7278
var username: String by mutableStateOf(appData.username ?: "")
7379
var password: String by mutableStateOf("")
7480
var tlsEnabled: Boolean by mutableStateOf(appData.tlsEnabled ?: true)
@@ -77,37 +83,68 @@ object ServerDialog {
7783
override fun cancel() = Service.driver.connectServerDialog.close()
7884
override fun isValid(): Boolean = when (server) {
7985
TYPEDB_CORE -> coreAddress.isNotBlank() && addressFormatIsValid(coreAddress)
80-
TYPEDB_CLOUD -> !(cloudAddresses.isEmpty() || username.isBlank() || password.isBlank())
86+
TYPEDB_CLOUD -> username.isNotBlank() && password.isNotBlank() && if (useCloudAddressTranslation) {
87+
cloudAddressTranslation.isNotEmpty()
88+
} else {
89+
cloudAddresses.isNotEmpty()
90+
}
8191
}
8292

8393
override fun submit() {
8494
when (server) {
8595
TYPEDB_CORE -> Service.driver.tryConnectToTypeDBCoreAsync(coreAddress) {
8696
Service.driver.connectServerDialog.close()
8797
}
88-
TYPEDB_CLOUD -> Service.driver.tryConnectToTypeDBCloudAsync(
89-
cloudAddresses.toSet(), username, password, tlsEnabled, caCertificate
90-
) { Service.driver.connectServerDialog.close() }
98+
TYPEDB_CLOUD -> {
99+
val credentials = if (caCertificate.isBlank()) TypeDBCredential(username, password, tlsEnabled)
100+
else TypeDBCredential(username, password, Path.of(caCertificate))
101+
val onSuccess = { Service.driver.connectServerDialog.close() }
102+
if (useCloudAddressTranslation) {
103+
val firstAddress = cloudAddressTranslation.first().first
104+
Service.driver.tryConnectToTypeDBCloudAsync("$username@$firstAddress", cloudAddressTranslation.associate { it }, credentials, onSuccess)
105+
} else {
106+
val firstAddress = cloudAddresses.first()
107+
Service.driver.tryConnectToTypeDBCloudAsync("$username@$firstAddress", cloudAddresses.toSet(), credentials, onSuccess)
108+
}
109+
}
91110
}
92111
password = ""
93112
appData.server = server
94113
appData.coreAddress = coreAddress
95114
appData.cloudAddresses = cloudAddresses
115+
appData.cloudAddressTranslation = cloudAddressTranslation
116+
appData.useCloudAddressTranslation = useCloudAddressTranslation
96117
appData.username = username
97118
appData.tlsEnabled = tlsEnabled
98119
appData.caCertificate = caCertificate
99120
}
100121
}
101122

102123
private object AddAddressForm : Form.State() {
103-
var value: String by mutableStateOf("")
124+
var server: String by mutableStateOf("")
125+
override fun cancel() = Service.driver.manageAddressesDialog.close()
126+
override fun isValid() = server.isNotBlank() && addressFormatIsValid(server) && !state.cloudAddresses.contains(server)
127+
128+
override fun submit() {
129+
assert(isValid())
130+
state.cloudAddresses.add(server)
131+
server = ""
132+
}
133+
}
134+
135+
private object AddAddressTranslationForm : Form.State() {
136+
var server: String by mutableStateOf("")
137+
var translation: String by mutableStateOf("")
104138
override fun cancel() = Service.driver.manageAddressesDialog.close()
105-
override fun isValid() = value.isNotBlank() && addressFormatIsValid(value) && !state.cloudAddresses.contains(value)
139+
override fun isValid() = serverIsValid() && translationIsValid()
140+
fun serverIsValid() = server.isNotBlank() && addressFormatIsValid(server) && !state.cloudAddressTranslation.any { it.first == server }
141+
fun translationIsValid() = translation.isNotBlank() && addressFormatIsValid(translation) && !state.cloudAddressTranslation.any { it.second == translation }
106142

107143
override fun submit() {
108144
assert(isValid())
109-
state.cloudAddresses.add(value)
110-
value = ""
145+
state.cloudAddressTranslation.add(Pair(server, translation))
146+
server = ""
147+
translation = ""
111148
}
112149
}
113150

@@ -187,8 +224,9 @@ object ServerDialog {
187224
private fun ManageCloudAddressesButton(state: ConnectServerForm, shouldFocus: Boolean) {
188225
val focusReq = if (shouldFocus) remember { FocusRequester() } else null
189226
Field(label = Label.ADDRESSES) {
227+
val numAddresses = if (state.useCloudAddressTranslation) state.cloudAddressTranslation.size else state.cloudAddresses.size
190228
TextButton(
191-
text = Label.MANAGE_CLOUD_ADDRESSES + " (${state.cloudAddresses.size})",
229+
text = Label.MANAGE_CLOUD_ADDRESSES + " ($numAddresses)",
192230
focusReq = focusReq, leadingIcon = Form.IconArg(Icon.CONNECT_TO_TYPEDB),
193231
enabled = Service.driver.isDisconnected
194232
) {
@@ -205,11 +243,21 @@ object ServerDialog {
205243
Column(Modifier.fillMaxSize()) {
206244
Text(value = Sentence.MANAGE_ADDRESSES_MESSAGE, softWrap = true)
207245
Spacer(Modifier.height(Dialog.DIALOG_SPACING))
208-
CloudAddressList(Modifier.fillMaxWidth().weight(1f))
209-
Spacer(Modifier.height(Dialog.DIALOG_SPACING))
210-
AddCloudAddressForm()
246+
if (state.useCloudAddressTranslation) {
247+
CloudAddressTranslationList(Modifier.fillMaxWidth().weight(1f))
248+
Spacer(Modifier.height(Dialog.DIALOG_SPACING))
249+
AddCloudAddressTranslationForm()
250+
} else {
251+
CloudAddressList(Modifier.fillMaxWidth().weight(1f))
252+
Spacer(Modifier.height(Dialog.DIALOG_SPACING))
253+
AddCloudAddressForm()
254+
}
211255
Spacer(Modifier.height(Dialog.DIALOG_SPACING * 2))
212256
Row(verticalAlignment = Alignment.Bottom) {
257+
Text(value = Label.TRANSLATE_ADDRESSES)
258+
RowSpacer()
259+
Checkbox(value = state.useCloudAddressTranslation) { state.useCloudAddressTranslation = it }
260+
RowSpacer()
213261
Spacer(modifier = Modifier.weight(1f))
214262
RowSpacer()
215263
TextButton(text = Label.CLOSE) { dialogState.close() }
@@ -224,12 +272,12 @@ object ServerDialog {
224272
Submission(AddAddressForm, modifier = Modifier.height(Form.FIELD_HEIGHT), showButtons = false) {
225273
Row {
226274
TextInputValidated(
227-
value = AddAddressForm.value,
275+
value = AddAddressForm.server,
228276
placeholder = Label.DEFAULT_SERVER_ADDRESS,
229-
onValueChange = { AddAddressForm.value = it },
277+
onValueChange = { AddAddressForm.server = it },
230278
modifier = Modifier.weight(1f).focusRequester(focusReq),
231279
invalidWarning = Label.ADDRESS_PORT_WARNING,
232-
validator = { AddAddressForm.value.isBlank() || addressFormatIsValid(AddAddressForm.value) }
280+
validator = { AddAddressForm.server.isBlank() || addressFormatIsValid(AddAddressForm.server) }
233281
)
234282
RowSpacer()
235283
TextButton(text = Label.ADD, enabled = AddAddressForm.isValid()) { AddAddressForm.submit() }
@@ -238,9 +286,38 @@ object ServerDialog {
238286
LaunchedEffect(focusReq) { focusReq.requestFocus() }
239287
}
240288

289+
@Composable
290+
private fun AddCloudAddressTranslationForm() {
291+
val focusReq = remember { FocusRequester() }
292+
Submission(AddAddressTranslationForm, modifier = Modifier.height(Form.FIELD_HEIGHT), showButtons = false) {
293+
Row {
294+
TextInputValidated(
295+
value = AddAddressTranslationForm.server,
296+
placeholder = Label.DEFAULT_SERVER_ADDRESS,
297+
onValueChange = { AddAddressTranslationForm.server = it },
298+
modifier = Modifier.weight(1f).focusRequester(focusReq),
299+
invalidWarning = Label.ADDRESS_PORT_WARNING,
300+
validator = { AddAddressTranslationForm.server.isBlank() || addressFormatIsValid(AddAddressTranslationForm.server) }
301+
)
302+
RowSpacer()
303+
TextInputValidated(
304+
value = AddAddressTranslationForm.translation,
305+
placeholder = Label.DEFAULT_SERVER_ADDRESS,
306+
onValueChange = { AddAddressTranslationForm.translation = it },
307+
modifier = Modifier.weight(1f).focusRequester(focusReq),
308+
invalidWarning = Label.ADDRESS_PORT_WARNING,
309+
validator = { AddAddressTranslationForm.translation.isBlank() || addressFormatIsValid(AddAddressTranslationForm.translation) }
310+
)
311+
RowSpacer()
312+
TextButton(text = Label.ADD, enabled = AddAddressTranslationForm.isValid()) { AddAddressTranslationForm.submit() }
313+
}
314+
}
315+
LaunchedEffect(focusReq) { focusReq.requestFocus() }
316+
}
317+
241318
@Composable
242319
private fun CloudAddressList(modifier: Modifier) = ActionableList.SingleButtonLayout(
243-
items = state.cloudAddresses.toMutableList(),
320+
items = state.cloudAddresses,
244321
modifier = modifier.border(1.dp, Theme.studio.border),
245322
buttonSide = ActionableList.Side.RIGHT,
246323
buttonFn = { address ->
@@ -252,6 +329,21 @@ object ServerDialog {
252329
}
253330
)
254331

332+
@Composable
333+
private fun CloudAddressTranslationList(modifier: Modifier) = ActionableList.SingleButtonLayout(
334+
items = state.cloudAddressTranslation.map { "${it.first}${it.second}" },
335+
modifier = modifier.border(1.dp, Theme.studio.border),
336+
buttonSide = ActionableList.Side.RIGHT,
337+
buttonFn = { address ->
338+
val parts = address.split("", limit = 2)
339+
Form.IconButtonArg(
340+
icon = Icon.REMOVE,
341+
color = { Theme.studio.errorStroke },
342+
onClick = { state.cloudAddressTranslation.remove(parts[0] to parts[1]) }
343+
)
344+
}
345+
)
346+
255347
@Composable
256348
private fun UsernameFormField(state: ConnectServerForm) = Field(label = Label.USERNAME) {
257349
TextInput(

module/user/UpdateDefaultPasswordDialog.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,17 @@ object UpdateDefaultPasswordDialog {
3232
var newPassword: String by mutableStateOf("")
3333
var repeatPassword: String by mutableStateOf("")
3434

35-
override fun cancel() = Service.driver.updateDefaultPasswordDialog.cancel()
36-
override fun submit() = Service.driver.updateDefaultPasswordDialog.submit(oldPassword, newPassword)
35+
override fun cancel() {
36+
Service.driver.updateDefaultPasswordDialog.cancel()
37+
oldPassword = ""
38+
newPassword = ""
39+
}
40+
override fun submit() {
41+
assert(isValid())
42+
Service.driver.updateDefaultPasswordDialog.submit(oldPassword, newPassword)
43+
oldPassword = ""
44+
newPassword = ""
45+
}
3746
override fun isValid() = oldPassword.isNotEmpty() && newPassword.isNotEmpty()
3847
&& oldPassword != newPassword && repeatPassword == newPassword
3948
}

service/common/DataService.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ class DataService {
6161
private val CONNECTION_SERVER = "connection.server"
6262
private val CONNECTION_CORE_ADDRESS = "connection.core_address"
6363
private val CONNECTION_CLOUD_ADDRESSES = "connection.cloud_addresses"
64+
private val CONNECTION_CLOUD_ADDRESS_TRANSLATION = "connection.cloud_address_translation"
65+
private val CONNECTION_USE_CLOUD_ADDRESS_TRANSLATION = "connection.use_cloud_address_translation"
6466
private val CONNECTION_USERNAME = "connection.username"
6567
private val CONNECTION_TLS_ENABLED = "connection.tls_enabled"
6668
private val CONNECTION_CA_CERTIFICATE = "connection.ca_certificate"
@@ -71,10 +73,18 @@ class DataService {
7173
var coreAddress: String?
7274
get() = properties?.getProperty(CONNECTION_CORE_ADDRESS)
7375
set(value) = value?.let { setProperty(CONNECTION_CORE_ADDRESS, it) } ?: Unit
74-
var cloudAddresses: MutableList<String>?
76+
var cloudAddresses: List<String>?
7577
get() = properties?.getProperty(CONNECTION_CLOUD_ADDRESSES)
76-
?.split(",")?.filter { it.isNotBlank() }?.toMutableList()
78+
?.split(",")?.filter { it.isNotBlank() }
7779
set(value) = value?.let { setProperty(CONNECTION_CLOUD_ADDRESSES, it.joinToString(",")) } ?: Unit
80+
var cloudAddressTranslation: List<Pair<String, String>>?
81+
get() = properties?.getProperty(CONNECTION_CLOUD_ADDRESS_TRANSLATION)
82+
?.split(",")?.filter { it.contains("=") }?.map { it.split("=", limit = 2) }?.map{ it[0] to it[1] }
83+
set(value) = value
84+
?.let { setProperty(CONNECTION_CLOUD_ADDRESS_TRANSLATION, it.map { pair -> "${pair.first}=${pair.second}" } .joinToString(",")) } ?: Unit
85+
var useCloudAddressTranslation: Boolean?
86+
get() = properties?.getProperty(CONNECTION_USE_CLOUD_ADDRESS_TRANSLATION)?.toBooleanStrictOrNull()
87+
set(value) = value?.let { setProperty(CONNECTION_USE_CLOUD_ADDRESS_TRANSLATION, it.toString()) } ?: Unit
7888
var username: String?
7989
get() = properties?.getProperty(CONNECTION_USERNAME)
8090
set(value) = value?.let { setProperty(CONNECTION_USERNAME, it) } ?: Unit

service/common/util/Label.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ object Label {
229229
const val TRANSACTION_STATUS = "Transaction Status"
230230
const val TRANSACTION_TIMEOUT_MINS = "Transaction Timeout (mins)"
231231
const val TRANSACTION_TYPE = "Transaction Type"
232+
const val TRANSLATE_ADDRESSES = "Translate addresses"
232233
const val TYPE = "Type"
233234
const val TYPEDB_STUDIO = "TypeDB Studio"
234235
const val TYPEDB_STUDIO_APPLICATION_ERROR = "TypeDB Studio Application Error"

service/connection/DriverState.kt

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class DriverState(
5353

5454
companion object {
5555
private const val DATABASE_LIST_REFRESH_RATE_MS = 100
56-
private val PASSWORD_EXPIRY_WARN_DURATION = Duration.ofDays(7);
56+
private val PASSWORD_EXPIRY_WARN_DURATION = Duration.ofDays(7)
5757
private val LOGGER = KotlinLogging.logger {}
5858
}
5959

@@ -114,19 +114,25 @@ class DriverState(
114114
) = tryConnectAsync(newConnectionName = address, onSuccess = onSuccess) { TypeDB.coreDriver(address) }
115115

116116
fun tryConnectToTypeDBCloudAsync(
117-
addresses: Set<String>, username: String, password: String,
118-
tlsEnabled: Boolean, caPath: String, onSuccess: (() -> Unit)? = null
117+
connectionName: String, addresses: Set<String>, credentials: TypeDBCredential, onSuccess: (() -> Unit)? = null
119118
) {
120-
val credentials = if (caPath.isBlank()) TypeDBCredential(username, password, tlsEnabled)
121-
else TypeDBCredential(username, password, Path.of(caPath))
122119
val postLoginFn = {
123120
onSuccess?.invoke()
124121
if (needsToChangeDefaultPassword()) forcePasswordUpdate()
125122
else mayWarnPasswordExpiry()
126123
}
127-
tryConnectAsync(newConnectionName = "$username@${addresses.first()}", postLoginFn) {
128-
TypeDB.cloudDriver(addresses, credentials)
124+
tryConnectAsync(newConnectionName = connectionName, postLoginFn) { TypeDB.cloudDriver(addresses, credentials) }
125+
}
126+
127+
fun tryConnectToTypeDBCloudAsync(
128+
connectionName: String, addressTranslation: Map<String, String>, credentials: TypeDBCredential, onSuccess: (() -> Unit)? = null
129+
) {
130+
val postLoginFn = {
131+
onSuccess?.invoke()
132+
if (needsToChangeDefaultPassword()) forcePasswordUpdate()
133+
else mayWarnPasswordExpiry()
129134
}
135+
tryConnectAsync(newConnectionName = connectionName, postLoginFn) { TypeDB.cloudDriver(addressTranslation, credentials) }
130136
}
131137

132138
private fun forcePasswordUpdate() = updateDefaultPasswordDialog.open(
@@ -138,15 +144,17 @@ class DriverState(
138144
tryUpdateUserPassword(old, new) {
139145
updateDefaultPasswordDialog.close()
140146
close()
141-
tryConnectToTypeDBCloudAsync(
142-
addresses = dataSrv.connection.cloudAddresses!!.toSet(),
143-
username = dataSrv.connection.username!!,
144-
password = new,
145-
tlsEnabled = dataSrv.connection.tlsEnabled!!,
146-
caPath = dataSrv.connection.caCertificate!!
147-
) {
148-
notificationSrv.info(LOGGER, Message.Connection.RECONNECTED_WITH_NEW_PASSWORD_SUCCESSFULLY)
149-
}
147+
148+
val username = dataSrv.connection.username!!
149+
val password = new
150+
val credentials = if (dataSrv.connection.caCertificate!!.isBlank()) TypeDBCredential(username, password, dataSrv.connection.tlsEnabled!!)
151+
else TypeDBCredential(username, password, Path.of(dataSrv.connection.caCertificate!!))
152+
val onSuccess = { notificationSrv.info(LOGGER, Message.Connection.RECONNECTED_WITH_NEW_PASSWORD_SUCCESSFULLY) }
153+
154+
if (dataSrv.connection.useCloudAddressTranslation == true)
155+
tryConnectToTypeDBCloudAsync(connectionName!!, dataSrv.connection.cloudAddressTranslation!!.associate { it }, credentials, onSuccess)
156+
else
157+
tryConnectToTypeDBCloudAsync(connectionName!!, dataSrv.connection.cloudAddresses!!.toSet(), credentials, onSuccess)
150158
}
151159
}
152160

0 commit comments

Comments
 (0)