From 2335fae7cf9d29388b0bef1c9069b9760b9ee857 Mon Sep 17 00:00:00 2001 From: Torsten Grote <t@grobox.de> Date: Tue, 19 Oct 2021 17:56:47 -0300 Subject: [PATCH] API endpoint for mailbox setup --- .../mailbox/core/server/AuthManager.kt | 12 ++- .../mailbox/core/server/Routing.kt | 7 +- .../mailbox/core/server/WebServerManager.kt | 4 +- .../mailbox/core/setup/SetupManager.kt | 61 ++++++++++-- .../mailbox/core/server/AuthManagerTest.kt | 13 +++ .../mailbox/core/server/IntegrationTest.kt | 2 +- .../mailbox/core/setup/SetupManagerTest.kt | 92 ++++++++++++++++--- mailbox-core/src/test/resources/logback.xml | 1 + 8 files changed, 163 insertions(+), 29 deletions(-) diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt index f72d5aac..3c830129 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt @@ -5,6 +5,7 @@ import org.briarproject.mailbox.core.contacts.Contact import org.briarproject.mailbox.core.db.Database import org.briarproject.mailbox.core.server.MailboxPrincipal.ContactPrincipal import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal +import org.briarproject.mailbox.core.server.MailboxPrincipal.SetupPrincipal import org.briarproject.mailbox.core.setup.SetupManager import org.briarproject.mailbox.core.system.RandomIdManager import javax.inject.Inject @@ -25,11 +26,11 @@ class AuthManager @Inject constructor( randomIdManager.assertIsRandomId(token) return db.transactionWithResult(true) { txn -> val contact = db.getContactWithToken(txn, token) - if (contact != null) { - ContactPrincipal(contact) - } else { - if (token == setupManager.getOwnerToken(txn)) OwnerPrincipal - else null + when { + contact != null -> ContactPrincipal(contact) + setupManager.getOwnerToken(txn) == token -> OwnerPrincipal + setupManager.getSetupToken(txn) == token -> SetupPrincipal + else -> null } } } @@ -79,6 +80,7 @@ class AuthManager @Inject constructor( } sealed class MailboxPrincipal : Principal { + object SetupPrincipal : MailboxPrincipal() object OwnerPrincipal : MailboxPrincipal() data class ContactPrincipal(val contact: Contact) : MailboxPrincipal() } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt index 80ef74ab..007ae9a7 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt @@ -21,11 +21,12 @@ import io.ktor.routing.routing import io.ktor.util.getOrFail import org.briarproject.mailbox.core.contacts.ContactsManager import org.briarproject.mailbox.core.files.FileManager +import org.briarproject.mailbox.core.setup.SetupManager import org.briarproject.mailbox.core.system.InvalidIdException internal const val V = "/" // TODO set to "/v1" for release -internal fun Application.configureBasicApi() = routing { +internal fun Application.configureBasicApi(setupManager: SetupManager) = routing { route(V) { get { call.respondText("Hello world!", ContentType.Text.Plain) @@ -35,7 +36,9 @@ internal fun Application.configureBasicApi() = routing { call.respond(HttpStatusCode.OK, "delete: Not yet implemented") } put("/setup") { - call.respond(HttpStatusCode.OK, "put: Not yet implemented") + call.handle { + setupManager.onSetupRequest(call) + } } } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt index 82140aa9..771167d3 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt @@ -11,6 +11,7 @@ import org.briarproject.mailbox.core.contacts.ContactsManager import org.briarproject.mailbox.core.files.FileManager import org.briarproject.mailbox.core.lifecycle.Service import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT +import org.briarproject.mailbox.core.setup.SetupManager import org.slf4j.Logger import org.slf4j.LoggerFactory.getLogger import javax.inject.Inject @@ -25,6 +26,7 @@ interface WebServerManager : Service { @Singleton internal class WebServerManagerImpl @Inject constructor( private val authManager: AuthManager, + private val setupManager: SetupManager, private val contactsManager: ContactsManager, private val fileManager: FileManager, ) : WebServerManager { @@ -48,7 +50,7 @@ internal class WebServerManagerImpl @Inject constructor( install(ContentNegotiation) { jackson() } - configureBasicApi() + configureBasicApi(setupManager) configureContactApi(contactsManager) configureFilesApi(fileManager) } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt index 14002b3a..611dccf6 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt @@ -1,7 +1,14 @@ package org.briarproject.mailbox.core.setup +import io.ktor.application.ApplicationCall +import io.ktor.auth.principal +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond import org.briarproject.mailbox.core.db.DbException import org.briarproject.mailbox.core.db.Transaction +import org.briarproject.mailbox.core.server.AuthException +import org.briarproject.mailbox.core.server.MailboxPrincipal +import org.briarproject.mailbox.core.server.MailboxPrincipal.SetupPrincipal import org.briarproject.mailbox.core.settings.Settings import org.briarproject.mailbox.core.settings.SettingsManager import org.briarproject.mailbox.core.system.RandomIdManager @@ -22,26 +29,68 @@ class SetupManager @Inject constructor( fun restartSetup() { val settings = Settings() settings[SETTINGS_SETUP_TOKEN] = randomIdManager.getNewRandomId() - settings[SETTINGS_OWNER_TOKEN] = "" // we can't remove or null or, so we need to empty it + settings[SETTINGS_OWNER_TOKEN] = "" // we can't remove or null, so we need to empty it settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE_OWNER) } + /** + * Handler for `PUT /setup` API endpoint. + * + * Wipes setup token and responds with new owner token and 201 status code. + */ + @Throws(AuthException::class) + suspend fun onSetupRequest(call: ApplicationCall) { + val principal: MailboxPrincipal? = call.principal() + if (principal !is SetupPrincipal) throw AuthException() + + // set new owner token and clear single-use setup token + val ownerToken = randomIdManager.getNewRandomId() + setToken(null, ownerToken) + val response = SetupResponse(ownerToken) + + call.respond(HttpStatusCode.Created, response) + } + /** * Visible for testing, consider private. */ @Throws(DbException::class) - internal fun setOwnerToken(token: String) { + internal fun setToken(setupToken: String?, ownerToken: String?) { val settings = Settings() - settings[SETTINGS_OWNER_TOKEN] = token + if (setupToken == null) { + settings[SETTINGS_SETUP_TOKEN] = "" + } else { + randomIdManager.assertIsRandomId(setupToken) + settings[SETTINGS_SETUP_TOKEN] = setupToken + } + if (ownerToken == null) { + settings[SETTINGS_OWNER_TOKEN] = "" + } else { + randomIdManager.assertIsRandomId(ownerToken) + settings[SETTINGS_OWNER_TOKEN] = ownerToken + } settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE_OWNER) } + fun getSetupToken(txn: Transaction): String? { + val settings = settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) + return settings[SETTINGS_SETUP_TOKEN].nullify() + } + @Throws(DbException::class) fun getOwnerToken(txn: Transaction): String? { val settings = settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) - val ownerToken = settings[SETTINGS_OWNER_TOKEN] - return if (ownerToken.isNullOrEmpty()) null - else ownerToken + return settings[SETTINGS_OWNER_TOKEN].nullify() + } + + /** + * @return the same string or null if it is empty + */ + private fun String?.nullify(): String? { + return if (isNullOrEmpty()) null + else this } } + +internal data class SetupResponse(val token: String) diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt index 6eaf19ad..397b11f8 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt @@ -7,6 +7,7 @@ import org.briarproject.mailbox.core.TestUtils.getNewRandomContact import org.briarproject.mailbox.core.TestUtils.getNewRandomId import org.briarproject.mailbox.core.db.Database import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal +import org.briarproject.mailbox.core.server.MailboxPrincipal.SetupPrincipal import org.briarproject.mailbox.core.setup.SetupManager import org.briarproject.mailbox.core.system.InvalidIdException import org.briarproject.mailbox.core.system.RandomIdManager @@ -61,11 +62,23 @@ class AuthManagerTest { everyTransactionWithResult(db, true) { txn -> every { db.getContactWithToken(txn, id) } returns null every { setupManager.getOwnerToken(txn) } returns otherId + every { setupManager.getSetupToken(txn) } returns otherId } assertNull(authManager.getPrincipal(id)) } + @Test + fun `getPrincipal() returns SetupPrincipal`() { + everyTransactionWithResult(db, true) { txn -> + every { db.getContactWithToken(txn, id) } returns null + every { setupManager.getOwnerToken(txn) } returns otherId + every { setupManager.getSetupToken(txn) } returns id + } + + assertEquals(SetupPrincipal, authManager.getPrincipal(id)) + } + @Test fun `assertCanDownloadFromFolder() throws for null MailboxPrincipal`() { assertThrows<AuthException> { diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt index 140ef2e3..238b39da 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt @@ -57,7 +57,7 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) { } protected fun addOwnerToken() { - testComponent.getSetupManager().setOwnerToken(ownerToken) + testComponent.getSetupManager().setToken(null, ownerToken) } protected fun addContact(c: Contact) { diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt index 88470f41..167cd18b 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt @@ -1,8 +1,14 @@ package org.briarproject.mailbox.core.setup +import io.ktor.client.request.put +import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.runBlocking import org.briarproject.mailbox.core.server.IntegrationTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull import kotlin.test.assertNull class SetupManagerTest : IntegrationTest() { @@ -10,27 +16,85 @@ class SetupManagerTest : IntegrationTest() { private val db by lazy { testComponent.getDatabase() } private val setupManager by lazy { testComponent.getSetupManager() } + @AfterEach + fun resetToken() { + // re-set both token to not interfere with other tests + setupManager.setToken(null, null) + } + @Test - fun `restarting setup wipes owner token`() { - // initially, there's no owner token - val initialOwnerToken = db.transactionWithResult(true) { txn -> - setupManager.getOwnerToken(txn) + fun `restarting setup wipes owner token and creates setup token`() { + // initially, there's no setup and no owner token + db.transaction(true) { txn -> + assertNull(setupManager.getSetupToken(txn)) + assertNull(setupManager.getOwnerToken(txn)) } - assertNull(initialOwnerToken) // setting an owner token stores it in DB - setupManager.setOwnerToken(ownerToken) - val firstOwnerToken = db.transactionWithResult(true) { txn -> - setupManager.getOwnerToken(txn) + setupManager.setToken(null, ownerToken) + db.transaction(true) { txn -> + assertNull(setupManager.getSetupToken(txn)) + assertEquals(ownerToken, setupManager.getOwnerToken(txn)) } - assertEquals(ownerToken, firstOwnerToken) - // restarting setup wipes owner token + // restarting setup wipes owner token, creates setup token setupManager.restartSetup() - val wipedOwnerToken = db.transactionWithResult(true) { txn -> - setupManager.getOwnerToken(txn) + db.transaction(true) { txn -> + val setupToken = setupManager.getSetupToken(txn) + assertNotNull(setupToken) + testComponent.getRandomIdManager().assertIsRandomId(setupToken) + assertNull(setupManager.getOwnerToken(txn)) + } + } + + @Test + fun `setup request gets rejected when using non-setup token`() = runBlocking { + // initially, there's no setup and no owner token + db.transaction(true) { txn -> + assertNull(setupManager.getSetupToken(txn)) + assertNull(setupManager.getOwnerToken(txn)) + } + // owner token gets rejected + assertEquals( + HttpStatusCode.Unauthorized, + httpClient.put<HttpResponse>("$baseUrl/setup") { + authenticateWithToken(ownerToken) + }.status + ) + + // now we set the owner token which still gets rejected + setupManager.setToken(null, ownerToken) + assertEquals( + HttpStatusCode.Unauthorized, + httpClient.put<HttpResponse>("$baseUrl/setup") { + authenticateWithToken(ownerToken) + }.status + ) + + // now we set the setup token, but use a different one for the request, so it gets rejected + setupManager.setToken(token, null) + assertEquals( + HttpStatusCode.Unauthorized, + httpClient.put<HttpResponse>("$baseUrl/setup") { + authenticateWithToken(ownerToken) + }.status + ) + } + + @Test + fun `setup request clears setup token and sets new owner token`() = runBlocking { + // set a setup-token + setupManager.setToken(token, null) + + // use it for setup PUT request + val response: SetupResponse = httpClient.put("$baseUrl/setup") { + authenticateWithToken(token) + } + // setup token got wiped and new owner token from response got stored + db.transaction(true) { txn -> + assertNull(setupManager.getSetupToken(txn)) + assertEquals(setupManager.getOwnerToken(txn), response.token) } - assertNull(wipedOwnerToken) } -} \ No newline at end of file +} diff --git a/mailbox-core/src/test/resources/logback.xml b/mailbox-core/src/test/resources/logback.xml index 945c218b..f53439b2 100644 --- a/mailbox-core/src/test/resources/logback.xml +++ b/mailbox-core/src/test/resources/logback.xml @@ -9,4 +9,5 @@ </root> <logger name="org.eclipse.jetty" level="INFO"/> <logger name="io.netty" level="INFO"/> + <logger name="io.mockk" level="INFO" /> </configuration> -- GitLab