Skip to content

Commit c0578ba

Browse files
disable fetching configuration unless you have rbac for it
1 parent fa5fdfe commit c0578ba

File tree

7 files changed

+109
-10
lines changed

7 files changed

+109
-10
lines changed

assets/src/components/Configuration.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're
22
import { useHistory, useParams } from 'react-router-dom'
33
import { useMutation, useQuery } from 'react-apollo'
44

5-
import { Button } from 'forge-core'
5+
import { Button, GqlError } from 'forge-core'
66

77
import { Box, Text } from 'grommet'
88

@@ -366,9 +366,9 @@ export default function Configuration() {
366366
const { repo } = useParams()
367367
const { setBreadcrumbs } = useContext(BreadcrumbsContext)
368368
const { setOnChange } = useContext(InstallationContext)
369-
const { data } = useQuery(APPLICATION_Q, {
369+
const { data, error } = useQuery(APPLICATION_Q, {
370370
variables: { name: repo },
371-
fetchPolicy: 'cache-and-network',
371+
fetchPolicy: 'network-only',
372372
})
373373
const onCompleted = useCallback(() => {
374374
history.push('/')
@@ -396,8 +396,19 @@ export default function Configuration() {
396396
)
397397
}
398398

399+
if (error) {
400+
return (
401+
<Box fill>
402+
<GqlError
403+
error={error}
404+
header="Cannot access configuration for this app"
405+
/>
406+
</Box>
407+
)
408+
}
409+
399410
return (
400-
<EditConfiguration
411+
<EditConfiguration
401412
application={data.application}
402413
overlays={data.configurationOverlays.map(({ metadata, ...rest }) => {
403414
const labels = metadata.labels.reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {})

lib/console.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ defmodule Console do
55

66
def demo_project?(), do: conf(:is_demo_project, false)
77

8+
def deep_get(map, keys, def \\ nil)
9+
def deep_get(map, [key], def), do: Map.get(map, key, def)
10+
def deep_get(map, [key | keys], def) do
11+
case Map.fetch(map, key) do
12+
{:ok, %{} = val} -> deep_get(val, keys, def)
13+
_ -> def
14+
end
15+
end
16+
817
def rand_str(size \\ 32) do
918
:crypto.strong_rand_bytes(size)
1019
|> Base.url_encode64()

lib/console/graphql/kubernetes/application.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule Console.GraphQl.Kubernetes.Application do
22
use Console.GraphQl.Schema.Base
3+
alias Console.Middleware.{Rbac}
34
alias Console.GraphQl.Resolvers.{Plural, Kubecost, License}
45

56
object :application do
@@ -25,7 +26,10 @@ defmodule Console.GraphQl.Kubernetes.Application do
2526
end)
2627
end
2728

28-
field :configuration, :configuration, resolve: &Plural.resolve_configuration/3
29+
field :configuration, :configuration do
30+
middleware Rbac, perm: :configure, field: [:metadata, :name]
31+
resolve &Plural.resolve_configuration/3
32+
end
2933
end
3034

3135
object :cost_analysis do

lib/console/graphql/middleware/rbac.ex

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ defmodule Console.Middleware.Rbac do
77
@dummy "!!invalid!!"
88

99
def call(%{context: %{current_user: %User{roles: %{admin: true}}}} = res, _), do: res
10-
def call(%{arguments: args, context: %{current_user: %User{} = user}} = res, opts) do
10+
def call(%{context: %{current_user: %User{} = user}} = res, opts) do
1111
perm = Keyword.get(opts, :perm)
12-
arg = Keyword.get(opts, :arg)
13-
repo = arg_fetch(args, arg)
12+
repo = fetch(res, Map.new(opts))
1413

1514
case Rbac.validate(user, repo, perm) do
1615
true -> res
1716
_ -> put_result(res, {:error, "forbidden"})
1817
end
1918
end
2019

21-
defp arg_fetch(%{} = args, arg) when is_atom(arg), do: Map.get(args, arg, @dummy)
22-
defp arg_fetch(%{} = args, [_ | _] = path), do: get_in(args, path) || @dummy
20+
defp fetch(%{source: %{} = source}, %{field: arg}) when is_atom(arg), do: Map.get(source, arg, @dummy)
21+
defp fetch(%{source: %{} = source}, %{field: [_ | _] = path}), do: Console.deep_get(source, path, @dummy)
22+
defp fetch(%{arguments: %{} = args}, %{arg: arg}) when is_atom(arg), do: Map.get(args, arg, @dummy)
23+
defp fetch(%{arguments: %{} = args}, %{arg: [_ | _] = path}), do: get_in(args, path) || @dummy
2324
end

test/console/graphql/queries/plural_queries_test.exs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,74 @@ defmodule Console.GraphQl.PluralQueriesTest do
294294
assert app["name"] == "app"
295295
assert app["spec"]["descriptor"]["type"] == "app"
296296
end
297+
298+
test "admins can sideload configuration by name" do
299+
user = insert(:user, roles: %{admin: true})
300+
expect(Kazan, :run, fn _ -> {:ok, application("app")} end)
301+
expect(Console.Deployer, :file, 2, fn _ -> {:ok, "found"} end)
302+
303+
{:ok, %{data: %{"application" => app}}} = run_query("""
304+
query App($name: String!) {
305+
application(name: $name) {
306+
name
307+
spec { descriptor { type } }
308+
configuration {
309+
terraform
310+
helm
311+
}
312+
}
313+
}
314+
""", %{"name" => "app"}, %{current_user: user})
315+
316+
assert app["name"] == "app"
317+
assert app["spec"]["descriptor"]["type"] == "app"
318+
assert app["configuration"]["helm"] == "found"
319+
assert app["configuration"]["terraform"] == "found"
320+
end
321+
322+
test "users w/ rbac can sideload configuration by name" do
323+
user = insert(:user)
324+
setup_rbac(user, ["app"], configure: true)
325+
expect(Kazan, :run, fn _ -> {:ok, application("app")} end)
326+
expect(Console.Deployer, :file, 2, fn _ -> {:ok, "found"} end)
327+
328+
{:ok, %{data: %{"application" => app}}} = run_query("""
329+
query App($name: String!) {
330+
application(name: $name) {
331+
name
332+
spec { descriptor { type } }
333+
configuration {
334+
terraform
335+
helm
336+
}
337+
}
338+
}
339+
""", %{"name" => "app"}, %{current_user: user})
340+
341+
assert app["name"] == "app"
342+
assert app["spec"]["descriptor"]["type"] == "app"
343+
assert app["configuration"]["helm"] == "found"
344+
assert app["configuration"]["terraform"] == "found"
345+
end
346+
347+
test "users w/o rbac cannot sideload configuration by name" do
348+
user = insert(:user)
349+
setup_rbac(user, ["other-app"], configure: true)
350+
expect(Kazan, :run, fn _ -> {:ok, application("app")} end)
351+
352+
{:ok, %{errors: [_ | _]}} = run_query("""
353+
query App($name: String!) {
354+
application(name: $name) {
355+
name
356+
spec { descriptor { type } }
357+
configuration {
358+
terraform
359+
helm
360+
}
361+
}
362+
}
363+
""", %{"name" => "app"}, %{current_user: user})
364+
end
297365
end
298366

299367
defp as_connection(nodes) do

test/support/factory.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,9 @@ defmodule Console.Factory do
133133
heartbeat: Timex.now()
134134
}
135135
end
136+
137+
def setup_rbac(user, repos \\ ["*"], perms) do
138+
role = insert(:role, repositories: repos, permissions: Map.new(perms))
139+
insert(:role_binding, role: role, user: user)
140+
end
136141
end

test/test_helper.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Mimic.copy(HTTPoison)
1414
Mimic.copy(Kazan.Watcher)
1515
Mimic.copy(Console.Kubernetes.PodExec)
1616
Mimic.copy(Kazan.Server)
17+
Mimic.copy(File)
1718

1819
ExUnit.start()
1920
Ecto.Adapters.SQL.Sandbox.mode(Console.Repo, :manual)

0 commit comments

Comments
 (0)