From 5d0674448d6fa39dcd588402847a2b4c4ac91c39 Mon Sep 17 00:00:00 2001 From: ricoberger Date: Mon, 26 Apr 2021 19:55:38 +0200 Subject: [PATCH] Add command-line flag to forbid resource access This commit adds a new command-line flag "--clusters.forbidden-resources", which can be used to forbid access to a list of resources. This allow a user to still use RBAC with wildcards for the permissions of kobs, but do not allow a user to access the specified resources. We also adjusted the corresponding React component, so that the error is now shown to the user. This also fixes a problem, where the app crashed, whenn the empty state was clicked in the resources list. --- CHANGELOG.md | 1 + .../components/resources/ResourceEvents.tsx | 8 ++--- app/src/components/resources/ResourcePods.tsx | 6 ++-- .../resources/ResourcesListItem.tsx | 30 ++++++++++++----- app/src/utils/resources.tsx | 32 +++++++++++++------ docs/configuration/getting-started.md | 1 + pkg/api/plugins/clusters/clusters.go | 28 ++++++++++++++-- 7 files changed, 80 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2d5f0a9f..aff0ae51a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan - [#45](https://github.com/kobsio/kobs/pull/45): Add value mappings for `sparkline` charts in the Prometheus plugin. - [#49](https://github.com/kobsio/kobs/pull/49): Add new chart type `table` for Prometheus plugin, which allows a user to render the results of multiple Prometheus queries in ab table. +- [#50](https://github.com/kobsio/kobs/pull/50): Add new command-line flag to forbid access for resources. ### Fixed diff --git a/app/src/components/resources/ResourceEvents.tsx b/app/src/components/resources/ResourceEvents.tsx index f6b5737ae..d74828746 100644 --- a/app/src/components/resources/ResourceEvents.tsx +++ b/app/src/components/resources/ResourceEvents.tsx @@ -20,7 +20,7 @@ interface IEventsProps { // Events is the component to display the events for a resource. The resource is identified by the cluster, namespace // and name. The event must contain the involvedObject.name= to be listed for a resource. const Events: React.FunctionComponent = ({ cluster, namespace, name }: IEventsProps) => { - const [events, setEvents] = useState(emptyState(4, '')); + const [events, setEvents] = useState(emptyState(4, '', false)); // fetchEvents is used to fetch all events to the provided resource. When the API returnes a list of resources, this // list is transformed into a the IRow interface, so we can display the events within the Table component. @@ -55,13 +55,13 @@ const Events: React.FunctionComponent = ({ cluster, namespace, nam setEvents(tmpEvents); } else { - setEvents(emptyState(4, '')); + setEvents(emptyState(4, '', false)); } } else { - setEvents(emptyState(4, '')); + setEvents(emptyState(4, '', false)); } } catch (err) { - setEvents(emptyState(4, err.message)); + setEvents(emptyState(4, err.message, false)); } }, [cluster, namespace, name]); diff --git a/app/src/components/resources/ResourcePods.tsx b/app/src/components/resources/ResourcePods.tsx index f49b402e9..97fcff6d3 100644 --- a/app/src/components/resources/ResourcePods.tsx +++ b/app/src/components/resources/ResourcePods.tsx @@ -22,7 +22,7 @@ const ResourcePods: React.FunctionComponent = ({ namespace, selector, }: IResourcePodsProps) => { - const [pods, setPods] = useState(emptyState(resources.pods.columns.length, '')); + const [pods, setPods] = useState(emptyState(resources.pods.columns.length, '', false)); // fetchPods fetches the pods for the given cluster, namespace and label selector. const fetchPods = useCallback(async () => { @@ -41,10 +41,10 @@ const ResourcePods: React.FunctionComponent = ({ if (resourceList.length === 1) { setPods(resources.pods.rows(resourceList)); } else { - setPods(emptyState(resources.pods.columns.length, '')); + setPods(emptyState(resources.pods.columns.length, '', false)); } } catch (err) { - setPods(emptyState(resources.pods.columns.length, err.message)); + setPods(emptyState(resources.pods.columns.length, err.message, false)); } }, [cluster, namespace, selector]); diff --git a/app/src/components/resources/ResourcesListItem.tsx b/app/src/components/resources/ResourcesListItem.tsx index 2564145dd..8673358dc 100644 --- a/app/src/components/resources/ResourcesListItem.tsx +++ b/app/src/components/resources/ResourcesListItem.tsx @@ -8,6 +8,12 @@ import { apiURL } from 'utils/constants'; // clustersService is the Clusters gRPC service, which is used to get a list of resources. const clustersService = new ClustersPromiseClient(apiURL, null, null); +interface IDataState { + error: string; + isLoading: boolean; + rows: IRow[]; +} + interface IResourcesListItemProps { clusters: string[]; namespaces: string[]; @@ -25,11 +31,13 @@ const ResourcesListItem: React.FunctionComponent = ({ selector, selectResource, }: IResourcesListItemProps) => { - const [rows, setRows] = useState(emptyState(resource.columns.length, '')); + // const [rows, setRows] = useState(emptyState(resource.columns.length, '')); + const [data, setData] = useState({ error: '', isLoading: false, rows: [] }); // fetchResources fetchs a list of resources for the given clusters, namespaces and an optional label selector. const fetchResources = useCallback(async () => { try { + setData({ error: '', isLoading: true, rows: [] }); const getResourcesRequest = new GetResourcesRequest(); getResourcesRequest.setClustersList(clusters); getResourcesRequest.setPath(resource.isCRD ? `apis/${resource.path}` : resource.path); @@ -48,12 +56,12 @@ const ResourcesListItem: React.FunctionComponent = ({ const tmpRows = resource.rows(getResourcesResponse.getResourcesList()); if (tmpRows.length > 0) { - setRows(tmpRows); + setData({ error: '', isLoading: false, rows: tmpRows }); } else { - setRows(emptyState(resource.columns.length, '')); + setData({ error: '', isLoading: false, rows: [] }); } } catch (err) { - setRows(emptyState(resource.columns.length, err.message)); + setData({ error: err.message, isLoading: false, rows: [] }); } }, [clusters, namespaces, resource, selector]); @@ -70,13 +78,19 @@ const ResourcesListItem: React.FunctionComponent = ({ isStickyHeader={true} cells={resource.columns} rows={ - rows.length > 0 && rows[0].cells?.length === resource.columns.length - ? rows - : emptyState(resource.columns.length, '') + data.rows.length > 0 && data.rows[0].cells?.length === resource.columns.length + ? data.rows + : emptyState(resource.columns.length, data.error, data.isLoading) } > - selectResource(row) : undefined} /> + 0 && data.rows[0].cells?.length === resource.columns.length + ? (e, row, props, data): void => selectResource(row) + : undefined + } + /> ); }; diff --git a/app/src/utils/resources.tsx b/app/src/utils/resources.tsx index 4f14fbe74..a9aa076fd 100644 --- a/app/src/utils/resources.tsx +++ b/app/src/utils/resources.tsx @@ -1,4 +1,12 @@ -import { Bullseye, EmptyState, EmptyStateBody, EmptyStateIcon, EmptyStateVariant, Title } from '@patternfly/react-core'; +import { + Bullseye, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + EmptyStateVariant, + Spinner, + Title, +} from '@patternfly/react-core'; import { CoreV1EventList, V1ClusterRoleBindingList, @@ -1316,7 +1324,7 @@ export const customResourceDefinition = (crds: CRD.AsObject[]): IResources => { // emptyState is used to display an empty state in the table for a resource, when the gRPC API call returned an error or // no results. -export const emptyState = (cols: number, error: string): IRow[] => { +export const emptyState = (cols: number, error: string, isLoading: boolean): IRow[] => { return [ { cells: [ @@ -1325,13 +1333,19 @@ export const emptyState = (cols: number, error: string): IRow[] => { title: ( - - - No results found - - - {error ? error : 'No results match the filter criteria. Select another cluster or namespace.'} - + {isLoading ? ( + + ) : ( + + + + {error ? 'An error occured' : 'No results found'} + + + {error ? error : 'No results match the filter criteria. Select another cluster or namespace.'} + + + )} ), diff --git a/docs/configuration/getting-started.md b/docs/configuration/getting-started.md index 4547b071c..42b69bba0 100644 --- a/docs/configuration/getting-started.md +++ b/docs/configuration/getting-started.md @@ -14,6 +14,7 @@ The following command-line arguments and environment variables are available. | `--clusters.cache-duration.namespaces` | `KOBS_CLUSTERS_CACHE_DURATION_NAMESPACES` | The duration, for how long requests to get the list of namespaces should be cached. | `5m` | | `--clusters.cache-duration.teams` | `KOBS_CLUSTERS_CACHE_DURATION_TEAMS` | The duration, for how long the teams data should be cached. | `60m` | | `--clusters.cache-duration.topology` | `KOBS_CLUSTERS_CACHE_DURATION_TOPOLOGY` | The duration, for how long the topology data should be cached. | `60m` | +| `--clusters.forbidden-resources` | `KOBS_CLUSTERS_FORBIDDEN_RESOURCES` | A list of resources, which can not be accessed via kobs. | | | `--config` | `KOBS_CONFIG` | Name of the configuration file. | `config.yaml` | | `--log.format` | `KOBS_LOG_FORMAT` | Set the output format of the logs. Must be `plain` or `json`. | `plain` | | `--log.level` | `KOBS_LOG_LEVEL` | Set the log level. Must be `trace`, `debug`, `info`, `warn`, `error`, `fatal` or `panic`. | `info` | diff --git a/pkg/api/plugins/clusters/clusters.go b/pkg/api/plugins/clusters/clusters.go index e8d3ba3f0..84ed316cb 100644 --- a/pkg/api/plugins/clusters/clusters.go +++ b/pkg/api/plugins/clusters/clusters.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "sort" + "strings" "time" applicationProto "github.com/kobsio/kobs/pkg/api/plugins/application/proto" @@ -15,6 +16,8 @@ import ( "github.com/sirupsen/logrus" flag "github.com/spf13/pflag" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var ( @@ -22,10 +25,10 @@ var ( cacheDurationNamespaces string cacheDurationTopology string cacheDurationTeams string + forbiddenResources []string ) -// init is used to define all command-line flags for the clusters package. Currently this is only the cache duration, -// which is used to cache the namespaces for a cluster. +// init is used to define all command-line flags for the clusters package. func init() { defaultCacheDurationNamespaces := "5m" if os.Getenv("KOBS_CLUSTERS_CACHE_DURATION_NAMESPACES") != "" { @@ -42,9 +45,26 @@ func init() { defaultCacheDurationTeams = os.Getenv("KOBS_CLUSTERS_CACHE_DURATION_TEAMS") } + var defaultForbiddenResources []string + if os.Getenv("KOBS_CLUSTERS_FORBIDDEN_RESOURCES") != "" { + defaultForbiddenResources = strings.Split(os.Getenv("KOBS_CLUSTERS_FORBIDDEN_RESOURCES"), ",") + } + flag.StringVar(&cacheDurationNamespaces, "clusters.cache-duration.namespaces", defaultCacheDurationNamespaces, "The duration, for how long requests to get the list of namespaces should be cached.") flag.StringVar(&cacheDurationTopology, "clusters.cache-duration.topology", defaultCacheDurationTopology, "The duration, for how long the topology data should be cached.") flag.StringVar(&cacheDurationTeams, "clusters.cache-duration.teams", defaultCacheDurationTeams, "The duration, for how long the teams data should be cached.") + flag.StringArrayVar(&forbiddenResources, "clusters.forbidden-resources", defaultForbiddenResources, "A list of resources, which can not be accessed via kobs.") +} + +// isForbidden checks if the requested resource was specified in the forbidden resources list. +func isForbidden(resource string) bool { + for _, r := range forbiddenResources { + if resource == r { + return true + } + } + + return false } // Config is the configuration required to load all clusters. @@ -183,6 +203,10 @@ func (c *Clusters) GetResources(ctx context.Context, getResourcesRequest *cluste return nil, fmt.Errorf("invalid cluster name") } + if isForbidden(getResourcesRequest.Resource) { + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("access for resource %s is forbidding", getResourcesRequest.Resource)) + } + if getResourcesRequest.Namespaces == nil { list, err := cluster.GetResources(ctx, "", getResourcesRequest.Path, getResourcesRequest.Resource, getResourcesRequest.ParamName, getResourcesRequest.Param) if err != nil {