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 f72d5aac658c7e6b13ba22d1bd52ce2d6e34a548..3c8301296828eb018621f9eb26fc84e646757b83 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 80ef74aba57f4588afe0a0ac48e7307c3f3f4606..007ae9a7881cbd8119c3f07cdfc91f8e076e94da 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 82140aa955e54402e868e9e476c2cb82406ec1fa..771167d3aa077a542167416c5dafd3cf7a329313 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 14002b3a48905b5d5ab2fe992e69f6905cb95d81..611dccf6772b0093776015d4cf365f3852262cf9 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 6eaf19addf139cab6d53819bfee0fc73ba84265c..397b11f860268b36d1a928080918db245d66fdc8 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 140ef2e3aed6b908a02b841ba48587f6911bcf9a..238b39da86cb3c695f34cbd81a88b9428f083891 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 88470f41171f36d77efb80976f96e04e2b938941..167cd18bb7da2957e4796cdf6b1de2532eb191a0 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 945c218bb1ab715b006497a8900d0954aeb3a5a4..f53439b24f525bb17fe9029f307b4bee40e8e35c 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>