Skip to content

Commit

Permalink
[CLOSES cashapp#1129] DashboardTabProvider
Browse files Browse the repository at this point in the history
* New pattern of multibinding tabs to a DashboardTabProvider along with an AccessAnnotation
* Allows for Misk tabs to be set with the authentication from the multibound `AdminDashboardAccess` access annotation
* Also allows for service tabs to be easily set with service specific Access Annotations

* Fixes small bug in Loader tab where dashboard doesn't load when no tabs are bound
  • Loading branch information
adrw committed Aug 12, 2019
1 parent 4ee2b86 commit d2ea3f7
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 119 deletions.
54 changes: 54 additions & 0 deletions misk/src/main/kotlin/misk/web/DashboardTab.kt
@@ -0,0 +1,54 @@
package misk.web

import misk.security.authz.AccessAnnotationEntry
import javax.inject.Inject
import javax.inject.Provider
import kotlin.reflect.KClass

class DashboardTab(
slug: String,
url_path_prefix: String,
val name: String,
val category: String = "Admin",
capabilities: Set<String> = setOf(),
services: Set<String> = setOf()
) : WebTab(slug, url_path_prefix, capabilities, services)

/**
* Sets the tab's authentication based on the injected AdminDashboardAccess access annotation entry
*/
class DashboardTabProvider(
val slug: String,
val url_path_prefix: String,
val name: String,
val category: String = "Admin",
val accessAnnotation: KClass<out Annotation>
) : Provider<DashboardTab> {
@Inject
lateinit var registeredEntries: List<AccessAnnotationEntry>

override fun get(): DashboardTab {
val accessAnnotationEntry = registeredEntries.find { it.annotation == accessAnnotation }!!
return DashboardTab(
slug = slug,
url_path_prefix = url_path_prefix,
name = name,
category = category,
capabilities = accessAnnotationEntry.capabilities.toSet(),
services = accessAnnotationEntry.services.toSet()
)
}
}

inline fun <reified A : Annotation> DashboardTabProvider(
slug: String,
url_path_prefix: String,
name: String,
category: String = "Admin"
) = DashboardTabProvider(
slug = slug,
url_path_prefix = url_path_prefix,
name = name,
category = category,
accessAnnotation = A::class
)
31 changes: 14 additions & 17 deletions misk/src/main/kotlin/misk/web/WebTab.kt
Expand Up @@ -10,22 +10,19 @@ abstract class WebTab(
val capabilities: Set<String> = setOf(),
val services: Set<String> = setOf()
) : ValidWebEntry(slug = slug, url_path_prefix = url_path_prefix) {
fun isAuthenticated(caller: MiskCaller?): Boolean {
return when {
capabilities.isEmpty() && services.isEmpty() -> true // no capabilities/service requirement => unauthenticated requests allowed (including when caller is null)
caller == null -> false // capability/service requirement present but caller null => assume authentication broken
capabilities.any { caller.capabilities.contains(it) } -> true // matching capability
services.any { caller.service == it } -> true // matching service
else -> false
}
fun isAuthenticated(caller: MiskCaller?): Boolean = when {
// no capabilities/service requirement => unauthenticated and null caller requests allowed
capabilities.isEmpty() && services.isEmpty() -> true

// capability/service requirement present but caller null => assume authentication broken
caller == null -> false

// matching capability
capabilities.any { caller.capabilities.contains(it) } -> true

// matching service
services.any { caller.service == it } -> true

else -> false
}
}

class DashboardTab(
slug: String,
url_path_prefix: String,
val name: String,
val category: String = "Container Admin",
capabilities: Set<String> = setOf(),
services: Set<String> = setOf()
) : WebTab(slug, url_path_prefix, capabilities, services)
64 changes: 34 additions & 30 deletions misk/src/main/kotlin/misk/web/metadata/AdminDashboardModule.kt
Expand Up @@ -5,6 +5,7 @@ import misk.environment.Environment
import misk.inject.KAbstractModule
import misk.security.authz.AccessAnnotationEntry
import misk.web.DashboardTab
import misk.web.DashboardTabProvider
import misk.web.NetworkInterceptor
import misk.web.WebActionModule
import misk.web.actions.AdminDashboardTab
Expand All @@ -30,63 +31,66 @@ class AdminDashboardModule(val environment: Environment) : KAbstractModule() {
install(WebActionModule.create<AdminDashboardTabAction>())
install(WebActionModule.create<ServiceMetadataAction>())
install(WebTabResourceModule(
environment = environment,
slug = "loader",
web_proxy_url = "http://localhost:3100/"
environment = environment,
slug = "loader",
web_proxy_url = "http://localhost:3100/"
))
install(WebTabResourceModule(
environment = environment,
slug = "loader",
web_proxy_url = "http://localhost:3100/",
url_path_prefix = "/_admin/",
resourcePath = "classpath:/web/_tab/loader/"
environment = environment,
slug = "loader",
web_proxy_url = "http://localhost:3100/",
url_path_prefix = "/_admin/",
resourcePath = "classpath:/web/_tab/loader/"
))

// @misk packages
install(WebTabResourceModule(
environment = environment,
slug = "@misk",
web_proxy_url = "http://localhost:9100/",
url_path_prefix = "/@misk/",
resourcePath = "classpath:/web/_tab/loader/@misk/"
environment = environment,
slug = "@misk",
web_proxy_url = "http://localhost:9100/",
url_path_prefix = "/@misk/",
resourcePath = "classpath:/web/_tab/loader/@misk/"
))

// Config
install(WebActionModule.create<ConfigMetadataAction>())
multibind<DashboardTab, AdminDashboardTab>().toInstance(DashboardTab(
name = "Config",
multibind<DashboardTab, AdminDashboardTab>().toProvider(
DashboardTabProvider<AdminDashboardAccess>(
slug = "config",
url_path_prefix = "/_admin/config/",
name = "Config",
category = "Container Admin"
))
))
install(WebTabResourceModule(
environment = environment,
slug = "config",
web_proxy_url = "http://localhost:3200/"
environment = environment,
slug = "config",
web_proxy_url = "http://localhost:3200/"
))

// Web Actions
install(WebActionModule.create<WebActionMetadataAction>())
multibind<DashboardTab, AdminDashboardTab>().toInstance(DashboardTab(
name = "Web Actions",
multibind<DashboardTab, AdminDashboardTab>().toProvider(
DashboardTabProvider<AdminDashboardAccess>(
slug = "web-actions",
url_path_prefix = "/_admin/web-actions/"
))
url_path_prefix = "/_admin/web-actions/",
name = "Web Actions",
category = "Container Admin"
))
install(WebTabResourceModule(
environment = environment,
slug = "web-actions",
web_proxy_url = "http://localhost:3201/"
environment = environment,
slug = "web-actions",
web_proxy_url = "http://localhost:3201/"
))
}
}

// Module that allows testing/development environments to bind up the admin dashboard
class AdminDashboardTestingModule(val environment: Environment) : KAbstractModule() {
override fun configure() {
install(AdminDashboardModule(environment))
multibind<AccessAnnotationEntry>()
// Set dummy values for access, these shouldn't matter,
// as test environments should prefer to use the FakeCallerAuthenticator.
.toInstance(AccessAnnotationEntry<AdminDashboardAccess>(capabilities = listOf("admin_access")))
// Set dummy values for access, these shouldn't matter,
// as test environments should prefer to use the FakeCallerAuthenticator.
.toInstance(AccessAnnotationEntry<AdminDashboardAccess>(capabilities = listOf("admin_access")))
install(AdminDashboardModule(environment))
}
}
Expand Up @@ -7,7 +7,7 @@ import misk.inject.KAbstractModule
import misk.web.metadata.AdminDashboardTestingModule

// Common test module used to be able to test admin dashboard WebActions
class TestAdminDashboardActionModule : KAbstractModule() {
class AdminDashboardActionTestingModule : KAbstractModule() {
override fun configure() {
install(TestWebActionModule())
install(AdminDashboardTestingModule(Environment.TESTING))
Expand All @@ -17,4 +17,4 @@ class TestAdminDashboardActionModule : KAbstractModule() {
}
}

class TestAdminDashboardConfig : Config
class TestAdminDashboardConfig : Config
@@ -0,0 +1,86 @@
package misk.web.actions

import com.squareup.moshi.Moshi
import misk.client.HttpClientEndpointConfig
import misk.client.HttpClientFactory
import misk.moshi.adapter
import misk.security.authz.FakeCallerAuthenticator
import misk.testing.MiskTest
import misk.testing.MiskTestModule
import misk.web.jetty.JettyService
import okhttp3.OkHttpClient
import okhttp3.Request
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import javax.inject.Inject
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

@MiskTest(startService = true)
class AdminDashboardTabActionTest {
@MiskTestModule
val module = AdminDashboardActionTestingModule()

@Inject private lateinit var jetty: JettyService
@Inject private lateinit var httpClientFactory: HttpClientFactory

val path = "/api/admindashboardtabs"

@Test fun customCapabilityAccess_unauthenticated() {
val response = executeRequest(path = path)
assertEquals(0, response.adminDashboardTabs.size)
}

@Test fun customCapabilityAccess_unauthorized() {
val response = executeRequest(
path = path,
user = "sandy",
capabilities = "guest"
)
assertEquals(0, response.adminDashboardTabs.size)
}

@Test fun customCapabilityAccess_authorized() {
val response = executeRequest(
path = path,
user = "sandy",
capabilities = "admin_access"
)
assertEquals(2, response.adminDashboardTabs.size)
assertNotNull(response.adminDashboardTabs.find { it.name == "Config" })
assertNotNull(response.adminDashboardTabs.find { it.name == "Web Actions" })
}

private fun executeRequest(
path: String = "/",
service: String? = null,
user: String? = null,
capabilities: String? = null
): AdminDashboardTabAction.Response {
val client = createOkHttpClient()

val baseUrl = jetty.httpServerUrl
val requestBuilder = Request.Builder()
.url(baseUrl.resolve(path)!!)
service?.let {
requestBuilder.header(FakeCallerAuthenticator.SERVICE_HEADER, service)
}
user?.let {
requestBuilder.header(FakeCallerAuthenticator.USER_HEADER, user)
}
capabilities?.let {
requestBuilder.header(FakeCallerAuthenticator.CAPABILITIES_HEADER, capabilities)
}
val call = client.newCall(requestBuilder.build())
val response = call.execute()

val moshi = Moshi.Builder().build()
val responseAdaptor = moshi.adapter<AdminDashboardTabAction.Response>()
return responseAdaptor.fromJson(response.body!!.source())!!
}

private fun createOkHttpClient(): OkHttpClient {
val config = HttpClientEndpointConfig(jetty.httpServerUrl.toString())
return httpClientFactory.create(config)
}
}
Expand Up @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test
@MiskTest(startService = true)
class ConfigMetadataActionTest {
@MiskTestModule
val module = TestAdminDashboardActionModule()
val module = AdminDashboardActionTestingModule()

val testConfig = TestConfig(
IncludedConfig("foo"),
Expand Down
37 changes: 6 additions & 31 deletions misk/src/test/kotlin/misk/web/actions/DashboardTabTest.kt
@@ -1,38 +1,10 @@
package misk.web.actions

import misk.inject.KAbstractModule
import misk.logging.getLogger
import misk.testing.MiskTest
import misk.testing.MiskTestModule
import misk.web.DashboardTab
import org.junit.jupiter.api.Test
import javax.inject.Inject
import kotlin.test.assertFailsWith

@MiskTest
internal class DashboardTabTest {
@MiskTestModule
val module = TestModule()

@Inject lateinit var dashboardTabs: List<DashboardTab>

private val logger = getLogger<DashboardTabTest>()

class TestModule : KAbstractModule() {
override fun configure() {
multibind<DashboardTab>().toInstance(DashboardTab(
slug = "dashboard",
url_path_prefix = "/_admin/dashboard/",
name = "Dashboard"
))
}
}

@Test
internal fun testBindings() {
logger.info(dashboardTabs.toString())
}

@Test
internal fun tabGoodSlug() {
DashboardTab("good-1-slug-test", url_path_prefix = "/a/path/", name = "Name")
Expand All @@ -54,20 +26,23 @@ internal class DashboardTabTest {

@Test
internal fun tabGoodCategory() {
DashboardTab(slug = "slug", url_path_prefix = "/a/path/", name = "Name", category = "@tea-pot_418")
DashboardTab(slug = "slug", url_path_prefix = "/a/path/", name = "Name",
category = "@tea-pot_418")
}

@Test
internal fun tabCategoryWithSpace() {
assertFailsWith<IllegalArgumentException> {
DashboardTab(slug = "bad slug", url_path_prefix = "/a/path/", name = "Name", category = "bad icon")
DashboardTab(slug = "bad slug", url_path_prefix = "/a/path/", name = "Name",
category = "bad icon")
}
}

@Test
internal fun tabCategoryWithUpperCase() {
assertFailsWith<IllegalArgumentException> {
DashboardTab(slug = "BadSlug", url_path_prefix = "/a/path/", name = "Name", category = "Bad-Icon")
DashboardTab(slug = "BadSlug", url_path_prefix = "/a/path/", name = "Name",
category = "Bad-Icon")
}
}

Expand Down
Expand Up @@ -11,7 +11,7 @@ import javax.inject.Inject
@MiskTest(startService = true)
class WebActionMetadataActionTest {
@MiskTestModule
val module = TestAdminDashboardActionModule()
val module = AdminDashboardActionTestingModule()

@Inject lateinit var webActionMetadataAction: WebActionMetadataAction

Expand Down

0 comments on commit d2ea3f7

Please sign in to comment.