Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command-line flag to forbid resource access #51

Merged
merged 1 commit into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions app/src/components/resources/ResourceEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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=<NAME> to be listed for a resource.
const Events: React.FunctionComponent<IEventsProps> = ({ cluster, namespace, name }: IEventsProps) => {
const [events, setEvents] = useState<IRow[]>(emptyState(4, ''));
const [events, setEvents] = useState<IRow[]>(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.
Expand Down Expand Up @@ -55,13 +55,13 @@ const Events: React.FunctionComponent<IEventsProps> = ({ 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]);

Expand Down
6 changes: 3 additions & 3 deletions app/src/components/resources/ResourcePods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const ResourcePods: React.FunctionComponent<IResourcePodsProps> = ({
namespace,
selector,
}: IResourcePodsProps) => {
const [pods, setPods] = useState<IRow[]>(emptyState(resources.pods.columns.length, ''));
const [pods, setPods] = useState<IRow[]>(emptyState(resources.pods.columns.length, '', false));

// fetchPods fetches the pods for the given cluster, namespace and label selector.
const fetchPods = useCallback(async () => {
Expand All @@ -41,10 +41,10 @@ const ResourcePods: React.FunctionComponent<IResourcePodsProps> = ({
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]);

Expand Down
30 changes: 22 additions & 8 deletions app/src/components/resources/ResourcesListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -25,11 +31,13 @@ const ResourcesListItem: React.FunctionComponent<IResourcesListItemProps> = ({
selector,
selectResource,
}: IResourcesListItemProps) => {
const [rows, setRows] = useState<IRow[]>(emptyState(resource.columns.length, ''));
// const [rows, setRows] = useState<IRow[]>(emptyState(resource.columns.length, ''));
const [data, setData] = useState<IDataState>({ 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);
Expand All @@ -48,12 +56,12 @@ const ResourcesListItem: React.FunctionComponent<IResourcesListItemProps> = ({
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]);

Expand All @@ -70,13 +78,19 @@ const ResourcesListItem: React.FunctionComponent<IResourcesListItemProps> = ({
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)
}
>
<TableHeader />
<TableBody onRowClick={selectResource ? (e, row, props, data): void => selectResource(row) : undefined} />
<TableBody
onRowClick={
selectResource && data.rows.length > 0 && data.rows[0].cells?.length === resource.columns.length
? (e, row, props, data): void => selectResource(row)
: undefined
}
/>
</Table>
);
};
Expand Down
32 changes: 23 additions & 9 deletions app/src/utils/resources.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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: [
Expand All @@ -1325,13 +1333,19 @@ export const emptyState = (cols: number, error: string): IRow[] => {
title: (
<Bullseye>
<EmptyState variant={EmptyStateVariant.small}>
<EmptyStateIcon icon={SearchIcon} />
<Title headingLevel="h2" size="lg">
No results found
</Title>
<EmptyStateBody>
{error ? error : 'No results match the filter criteria. Select another cluster or namespace.'}
</EmptyStateBody>
{isLoading ? (
<EmptyStateIcon variant="container" component={Spinner} />
) : (
<React.Fragment>
<EmptyStateIcon icon={SearchIcon} />
<Title headingLevel="h2" size="lg">
{error ? 'An error occured' : 'No results found'}
</Title>
<EmptyStateBody>
{error ? error : 'No results match the filter criteria. Select another cluster or namespace.'}
</EmptyStateBody>
</React.Fragment>
)}
</EmptyState>
</Bullseye>
),
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
28 changes: 26 additions & 2 deletions pkg/api/plugins/clusters/clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"sort"
"strings"
"time"

applicationProto "github.com/kobsio/kobs/pkg/api/plugins/application/proto"
Expand All @@ -15,17 +16,19 @@ import (

"github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

var (
log = logrus.WithFields(logrus.Fields{"package": "clusters"})
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") != "" {
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down