diff --git a/misk/src/main/kotlin/misk/web/DashboardTab.kt b/misk/src/main/kotlin/misk/web/DashboardTab.kt new file mode 100644 index 00000000000..58d365e73b4 --- /dev/null +++ b/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 = setOf(), + services: Set = 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 +) : Provider { + @Inject + lateinit var registeredEntries: List + + 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 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 +) \ No newline at end of file diff --git a/misk/src/main/kotlin/misk/web/WebTab.kt b/misk/src/main/kotlin/misk/web/WebTab.kt index 70c96b0dcca..8bb5c94c10a 100644 --- a/misk/src/main/kotlin/misk/web/WebTab.kt +++ b/misk/src/main/kotlin/misk/web/WebTab.kt @@ -10,22 +10,19 @@ abstract class WebTab( val capabilities: Set = setOf(), val services: Set = 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 = setOf(), - services: Set = setOf() -) : WebTab(slug, url_path_prefix, capabilities, services) diff --git a/misk/src/main/kotlin/misk/web/metadata/AdminDashboardModule.kt b/misk/src/main/kotlin/misk/web/metadata/AdminDashboardModule.kt index 0d16f50f007..82bc7c2a231 100644 --- a/misk/src/main/kotlin/misk/web/metadata/AdminDashboardModule.kt +++ b/misk/src/main/kotlin/misk/web/metadata/AdminDashboardModule.kt @@ -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 @@ -30,52 +31,55 @@ class AdminDashboardModule(val environment: Environment) : KAbstractModule() { install(WebActionModule.create()) install(WebActionModule.create()) 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()) - multibind().toInstance(DashboardTab( - name = "Config", + multibind().toProvider( + DashboardTabProvider( 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()) - multibind().toInstance(DashboardTab( - name = "Web Actions", + multibind().toProvider( + DashboardTabProvider( 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/" )) } } @@ -83,10 +87,10 @@ class AdminDashboardModule(val environment: Environment) : KAbstractModule() { // 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() - // Set dummy values for access, these shouldn't matter, - // as test environments should prefer to use the FakeCallerAuthenticator. - .toInstance(AccessAnnotationEntry(capabilities = listOf("admin_access"))) + // Set dummy values for access, these shouldn't matter, + // as test environments should prefer to use the FakeCallerAuthenticator. + .toInstance(AccessAnnotationEntry(capabilities = listOf("admin_access"))) + install(AdminDashboardModule(environment)) } } diff --git a/misk/src/test/kotlin/misk/web/actions/TestAdminDashboardActionModule.kt b/misk/src/test/kotlin/misk/web/actions/AdminDashboardActionTestingModule.kt similarity index 85% rename from misk/src/test/kotlin/misk/web/actions/TestAdminDashboardActionModule.kt rename to misk/src/test/kotlin/misk/web/actions/AdminDashboardActionTestingModule.kt index d55d7d77b93..ea95de42e56 100644 --- a/misk/src/test/kotlin/misk/web/actions/TestAdminDashboardActionModule.kt +++ b/misk/src/test/kotlin/misk/web/actions/AdminDashboardActionTestingModule.kt @@ -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)) @@ -17,4 +17,4 @@ class TestAdminDashboardActionModule : KAbstractModule() { } } -class TestAdminDashboardConfig : Config +class TestAdminDashboardConfig : Config \ No newline at end of file diff --git a/misk/src/test/kotlin/misk/web/actions/AdminDashboardTabActionTest.kt b/misk/src/test/kotlin/misk/web/actions/AdminDashboardTabActionTest.kt new file mode 100644 index 00000000000..0eb99cdfd43 --- /dev/null +++ b/misk/src/test/kotlin/misk/web/actions/AdminDashboardTabActionTest.kt @@ -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() + return responseAdaptor.fromJson(response.body!!.source())!! + } + + private fun createOkHttpClient(): OkHttpClient { + val config = HttpClientEndpointConfig(jetty.httpServerUrl.toString()) + return httpClientFactory.create(config) + } +} diff --git a/misk/src/test/kotlin/misk/web/actions/ConfigMetadataActionTest.kt b/misk/src/test/kotlin/misk/web/actions/ConfigMetadataActionTest.kt index d6557d3f436..7cd1ded148d 100644 --- a/misk/src/test/kotlin/misk/web/actions/ConfigMetadataActionTest.kt +++ b/misk/src/test/kotlin/misk/web/actions/ConfigMetadataActionTest.kt @@ -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"), diff --git a/misk/src/test/kotlin/misk/web/actions/DashboardTabTest.kt b/misk/src/test/kotlin/misk/web/actions/DashboardTabTest.kt index c00ff24f31e..49d7188f042 100644 --- a/misk/src/test/kotlin/misk/web/actions/DashboardTabTest.kt +++ b/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 - - private val logger = getLogger() - - class TestModule : KAbstractModule() { - override fun configure() { - multibind().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") @@ -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 { - 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 { - 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") } } diff --git a/misk/src/test/kotlin/misk/web/actions/WebActionMetadataActionTest.kt b/misk/src/test/kotlin/misk/web/actions/WebActionMetadataActionTest.kt index 445cb08f37d..906c9604e9b 100644 --- a/misk/src/test/kotlin/misk/web/actions/WebActionMetadataActionTest.kt +++ b/misk/src/test/kotlin/misk/web/actions/WebActionMetadataActionTest.kt @@ -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 diff --git a/misk/web/tabs/config/package.json b/misk/web/tabs/config/package.json index 3b657097a7b..271a25d74f7 100644 --- a/misk/web/tabs/config/package.json +++ b/misk/web/tabs/config/package.json @@ -26,7 +26,8 @@ "devDependencies": { "@misk/dev": "0.1.13", "@misk/tslint": "0.1.13", - "@misk/test": "0.1.13" + "@misk/test": "0.1.13", + "@misk/prettier": "0.1.13" }, "jest": { "testEnvironment": "jsdom", @@ -46,16 +47,6 @@ "jsx" ] }, - "prettier": { - "arrowParens": "avoid", - "bracketSpacing": true, - "jsxBracketSameLine": false, - "printWidth": 80, - "semi": false, - "singleQuote": false, - "tabWidth": 2, - "trailingComma": "none", - "useTabs": false - }, + "prettier": "@misk/prettier", "generated": "// Generated by @misk/cli." } diff --git a/misk/web/tabs/loader/package.json b/misk/web/tabs/loader/package.json index 134d69523f9..060c01b1b85 100644 --- a/misk/web/tabs/loader/package.json +++ b/misk/web/tabs/loader/package.json @@ -26,7 +26,8 @@ "devDependencies": { "@misk/dev": "0.1.13", "@misk/tslint": "0.1.13", - "@misk/test": "0.1.13" + "@misk/test": "0.1.13", + "@misk/prettier": "0.1.13" }, "jest": { "testEnvironment": "jsdom", @@ -46,16 +47,6 @@ "jsx" ] }, - "prettier": { - "arrowParens": "avoid", - "bracketSpacing": true, - "jsxBracketSameLine": false, - "printWidth": 80, - "semi": false, - "singleQuote": false, - "tabWidth": 2, - "trailingComma": "none", - "useTabs": false - }, + "prettier": "@misk/prettier", "generated": "// Generated by @misk/cli." } diff --git a/misk/web/tabs/loader/src/containers/LoaderContainer.tsx b/misk/web/tabs/loader/src/containers/LoaderContainer.tsx index 55422120781..974c46b53f0 100644 --- a/misk/web/tabs/loader/src/containers/LoaderContainer.tsx +++ b/misk/web/tabs/loader/src/containers/LoaderContainer.tsx @@ -51,7 +51,7 @@ class LoaderContainer extends React.Component { if (!serviceMetadata) { unavailableEndpointUrls += serviceUrl + " " } - if (adminDashboardTabs && serviceMetadata) { + if (adminDashboardTabs != null && serviceMetadata) { return (