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 455bdff772511e233b603a562bd2ae7674aa0d6f..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,19 +5,16 @@ 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.settings.SettingsManager +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 import javax.inject.Singleton -// We might want to move this somewhere else later -internal const val SETTINGS_NAMESPACE_OWNER = "owner" -internal const val SETTINGS_OWNER_TOKEN = "ownerToken" - @Singleton class AuthManager @Inject constructor( private val db: Database, - private val settingsManager: SettingsManager, + private val setupManager: SetupManager, private val randomIdManager: RandomIdManager, ) { @@ -29,12 +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 { - val settings = settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) - if (token == settings[SETTINGS_OWNER_TOKEN]) OwnerPrincipal - else null + when { + contact != null -> ContactPrincipal(contact) + setupManager.getOwnerToken(txn) == token -> OwnerPrincipal + setupManager.getSetupToken(txn) == token -> SetupPrincipal + else -> null } } } @@ -84,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/settings/Settings.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/Settings.kt index 8e988a443a943dfe829b7becab2dd2e66bd40d5e..01ace732dbcdb82e04f1abaf9034819350121509 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/Settings.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/Settings.kt @@ -4,6 +4,17 @@ import java.util.Hashtable class Settings : Hashtable<String, String>() { + override fun put(key: String, value: String?): String? { + return if (value == null) super.put(key, "") + else super.put(key, value) + } + + override fun get(key: String): String? { + val value = super.get(key) + return if (value.isNullOrEmpty()) null + else value + } + fun getBoolean(key: String, defaultValue: Boolean): Boolean { val s = get(key) ?: return defaultValue if ("true" == s) return true 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 new file mode 100644 index 0000000000000000000000000000000000000000..37e4f518148de33390e4de313ce118cf40f8a31b --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt @@ -0,0 +1,80 @@ +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 +import javax.inject.Inject + +private const val SETTINGS_NAMESPACE_OWNER = "owner" +private const val SETTINGS_SETUP_TOKEN = "setupToken" +private const val SETTINGS_OWNER_TOKEN = "ownerToken" + +class SetupManager @Inject constructor( + private val randomIdManager: RandomIdManager, + private val settingsManager: SettingsManager, +) { + + /** + * Stores a new single-use setup token and wipes the owner auth token, if one existed. + */ + fun restartSetup() { + val settings = Settings() + settings[SETTINGS_SETUP_TOKEN] = randomIdManager.getNewRandomId() + 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 setToken(setupToken: String?, ownerToken: String?) { + val settings = Settings() + if (setupToken != null) randomIdManager.assertIsRandomId(setupToken) + settings[SETTINGS_SETUP_TOKEN] = setupToken + if (ownerToken != null) 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] + } + + @Throws(DbException::class) + fun getOwnerToken(txn: Transaction): String? { + val settings = settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) + return settings[SETTINGS_OWNER_TOKEN] + } + +} + +internal data class SetupResponse(val token: String) diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt index c374303c762f7845193081bf282b218a0dca1830..564531b71e307b705f0864f5c9559cc752fc057a 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt @@ -5,6 +5,7 @@ import org.briarproject.mailbox.core.db.Database import org.briarproject.mailbox.core.files.FileManager import org.briarproject.mailbox.core.lifecycle.LifecycleManager import org.briarproject.mailbox.core.settings.SettingsManager +import org.briarproject.mailbox.core.setup.SetupManager import org.briarproject.mailbox.core.system.RandomIdManager import javax.inject.Singleton @@ -18,6 +19,7 @@ interface TestComponent { fun injectCoreEagerSingletons(): CoreEagerSingletons fun getLifecycleManager(): LifecycleManager fun getSettingsManager(): SettingsManager + fun getSetupManager(): SetupManager fun getFileManager(): FileManager fun getDatabase(): Database fun getRandomIdManager(): RandomIdManager 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 1a3d6f5b0992b952674f1771900e817c070f5935..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,8 +7,8 @@ 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.settings.Settings -import org.briarproject.mailbox.core.settings.SettingsManager +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 import org.briarproject.mailbox.core.system.toHex @@ -21,10 +21,10 @@ import kotlin.test.assertNull class AuthManagerTest { private val db: Database = mockk() - private val settingsManager: SettingsManager = mockk() + private val setupManager: SetupManager = mockk() private val randomIdManager = RandomIdManager() - private val authManager = AuthManager(db, settingsManager, randomIdManager) + private val authManager = AuthManager(db, setupManager, randomIdManager) private val id = getNewRandomId() private val otherId = getNewRandomId() @@ -49,13 +49,9 @@ class AuthManagerTest { @Test fun `getPrincipal() returns authenticated owner`() { - val settings = Settings().apply { - put(SETTINGS_OWNER_TOKEN, id) - } - everyTransactionWithResult(db, true) { txn -> every { db.getContactWithToken(txn, id) } returns null - every { settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) } returns settings + every { setupManager.getOwnerToken(txn) } returns id } assertEquals(OwnerPrincipal, authManager.getPrincipal(id)) @@ -63,16 +59,24 @@ class AuthManagerTest { @Test fun `getPrincipal() returns null when unauthenticated`() { - val settings = Settings().apply { - put(SETTINGS_OWNER_TOKEN, otherId) + 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 { settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) } returns settings + every { setupManager.getOwnerToken(txn) } returns otherId + every { setupManager.getSetupToken(txn) } returns id } - assertNull(authManager.getPrincipal(id)) + assertEquals(SetupPrincipal, authManager.getPrincipal(id)) } @Test 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 d963bd1161b04bc19120967702551c9beb75076b..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 @@ -14,7 +14,6 @@ import org.briarproject.mailbox.core.TestUtils.getNewRandomContact import org.briarproject.mailbox.core.TestUtils.getNewRandomId import org.briarproject.mailbox.core.contacts.Contact import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT -import org.briarproject.mailbox.core.settings.Settings import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.TestInstance @@ -58,10 +57,7 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) { } protected fun addOwnerToken() { - val settingsManager = testComponent.getSettingsManager() - val settings = Settings() - settings[SETTINGS_OWNER_TOKEN] = ownerToken - settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE_OWNER) + 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 new file mode 100644 index 0000000000000000000000000000000000000000..9ee9c88e122bfa60e57210e25dcb1f64074e3888 --- /dev/null +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt @@ -0,0 +1,109 @@ +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() { + + 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 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)) + } + + // setting an owner token stores it in DB + setupManager.setToken(null, ownerToken) + db.transaction(true) { txn -> + assertNull(setupManager.getSetupToken(txn)) + assertEquals(ownerToken, setupManager.getOwnerToken(txn)) + } + + // restarting setup wipes owner token, creates setup token + setupManager.restartSetup() + 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) + } + } + + @Test + fun `authentication doesn't work with empty string`() = runBlocking { + // use it for setup PUT request + val response: HttpResponse = httpClient.put("$baseUrl/setup") { + authenticateWithToken("") + } + assertEquals(HttpStatusCode.Unauthorized, response.status) + } + +} 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>