Skip to content
Snippets Groups Projects
Verified Commit 2335fae7 authored by Torsten Grote's avatar Torsten Grote
Browse files

API endpoint for mailbox setup

parent 0ff835f0
No related branches found
No related tags found
1 merge request!30Store single-use auth token and implement setup API endpoint
Pipeline #7907 passed
......@@ -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()
}
......
......@@ -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)
}
}
}
}
......
......@@ -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)
}
......
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)
......@@ -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> {
......
......@@ -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) {
......
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
}
......@@ -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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment