Compare commits

...

4 Commits

Author SHA1 Message Date
testa113
a7b6ccf91f bring across widget tabs from EE 2023-03-30 16:51:47 +13:00
testa113
659402c3ec Merge branch 'develop' into feat/EE-5028/security_teaser 2023-03-30 16:32:56 +13:00
testa113
8b96f2cecd feat(redirect): navigate to the referring view 2023-03-30 16:24:18 +13:00
Prabhat Khera
c002d460c8 add security menu and BE teaser 2023-03-03 14:18:43 +13:00
16 changed files with 193 additions and 48 deletions

View File

@@ -293,15 +293,12 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
const deploy = {
name: 'kubernetes.deploy',
url: '/deploy?templateId',
url: '/deploy?templateId&referrer',
views: {
'content@': {
component: 'kubernetesDeployView',
},
},
params: {
templateId: '',
},
};
const resourcePools = {
@@ -397,8 +394,8 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
};
const endpointKubernetesSecurityConstraint = {
name: 'kubernetes.cluster.securityConstraint',
url: '/securityConstraint',
name: 'kubernetes.security',
url: '/security',
views: {
'content@': {
templateUrl: '../kubernetes/views/security-constraint/constraint.html',

View File

@@ -89,7 +89,7 @@
ng-if="$ctrl.isPrimary"
type="button"
class="btn btn-sm btn-primary vertical-center !ml-0 h-fit"
ui-sref="kubernetes.deploy"
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.applications' })"
data-cy="k8sApp-deployFromManifestButton"
>
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest

View File

@@ -37,7 +37,12 @@
<button type="button" class="btn btn-sm btn-secondary vertical-center !ml-0" ui-sref="kubernetes.configurations.new" data-cy="k8sConfig-addConfigWithFormButton">
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
</button>
<button type="button" class="btn btn-sm btn-primary vertical-center !ml-0" ui-sref="kubernetes.deploy" data-cy="k8sConfig-deployFromManifestButton">
<button
type="button"
class="btn btn-sm btn-primary vertical-center !ml-0"
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.configurations' })"
data-cy="k8sConfig-deployFromManifestButton"
>
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
</button>
</div>

View File

@@ -36,7 +36,12 @@
<button type="button" class="btn btn-sm btn-secondary !ml-0" ui-sref="kubernetes.resourcePools.new" data-cy="k8sNamespace-addNamespaceWithFormButton">
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
</button>
<button type="button" class="btn btn-sm btn-primary !ml-0" ui-sref="kubernetes.deploy" data-cy="k8sNamespace-deployFromManifestButton">
<button
type="button"
class="btn btn-sm btn-primary !ml-0"
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.resourcePools' })"
data-cy="k8sNamespace-deployFromManifestButton"
>
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
</button>
</div>

View File

@@ -30,7 +30,12 @@
<pr-icon icon="'trash-2'"></pr-icon>
Remove
</button>
<button type="button" class="btn btn-sm btn-primary vertical-center !ml-0" ui-sref="kubernetes.deploy" data-cy="k8sVolume-deployFromManifestButton">
<button
type="button"
class="btn btn-sm btn-primary vertical-center !ml-0"
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.volumes' })"
data-cy="k8sVolume-deployFromManifestButton"
>
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>
Create from manifest
</button>

View File

@@ -268,6 +268,12 @@ class KubernetesDeployController {
this.Notifications.success('Success', 'Manifest successfully deployed');
this.state.isEditorDirty = false;
if (this.$state.params.referrer) {
this.$state.go(this.$state.params.referrer);
return;
}
this.$state.go('kubernetes.applications');
} catch (err) {
this.Notifications.error('Unable to deploy manifest', err, 'Unable to deploy resources');

View File

@@ -1,10 +1,10 @@
<page-header
ng-if="state.viewReady"
title="'Kubernetes security constraints'"
title="'Security'"
breadcrumbs="[
{ label:'Environments', link:'portainer.endpoints' },
{ label:endpoint.Name, link:'portainer.endpoints.endpoint', linkParams:{id: endpoint.Id} },
'Security constraints'
'Security'
]"
reload="true"
></page-header>
@@ -12,29 +12,73 @@
<kubernetes-view-loading view-ready="state.viewReady"></kubernetes-view-loading>
<div ng-if="state.viewReady">
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="shield" title-text="Pod security constraints"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesSecurityConstraintForm">
<!-- main toggle -->
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
checked="formValues.enabled"
name="'disableSysctlSettingForRegularUsers'"
label="'Enable pod security constraints'"
feature-id="limitedFeaturePodSecurityPolicy"
label-class="'col-sm-3 col-lg-2 px-0'"
switch-class="'col-sm-8'"
<div class="be-indicator-container limited-be">
<div class="overlay">
<div class="limited-be-link vertical-center"
><be-feature-indicator feature="limitedFeatureAccessControl"></be-feature-indicator
><portainer-tooltip message="'This feature is currently limited to Business Edition users only. '"></portainer-tooltip
></div>
<div class="limited-be-content">
<rd-widget>
<rd-widget-header icon="user" title-text="Access Control"></rd-widget-header>
<rd-widget-body>
<div class="inline-flex">
<div class="mr-2 inline"><pr-icon icon="'info'" mode="'primary'"></pr-icon></div>
<div class="inline">
<div>
<a class="hyperlink" href="https://kubernetes.io/docs/reference/access-authn-authz/authorization/" target="_blank">Kubernetes authorization</a> generally uses
<a class="hyperlink" href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/" target="_blank">role based access control</a> (RBAC) to determine if a
user or process has the permissions to access or perform certain actions within the cluster.
</div>
<br />
<div
>The <b>Cluster Roles</b> and <b>Roles</b> screens (under this Security sub-menu) list the resources where permissions are defined in your cluster, and the
Bindings that grant those permissions to subjects (Users, Groups and Service Accounts). The <b>Service Accounts</b> screens lists existing identities (typically
for a Pod or set of Pods).</div
>
</por-switch-field>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>
<div class="be-indicator-container limited-be">
<div class="overlay">
<div class="limited-be-link vertical-center"
><be-feature-indicator feature="limitedFeaturePodSecurityPolicy"></be-feature-indicator
><portainer-tooltip message="'This feature is currently limited to Business Edition users only. '"></portainer-tooltip
></div>
<div class="limited-be-content">
<rd-widget>
<rd-widget-header icon="shield" title-text="Pod security constraints"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesSecurityConstraintForm">
<!-- main toggle -->
<div class="form-group">
<div class="col-sm-12">
<p class="text-muted small vertical-center">
<pr-icon icon="'info'" class-name="'icon icon-sm icon-primary'"></pr-icon>
You may configure privilege and access control settings for Pods in your cluster.
</p>
</div>
<div class="col-sm-12">
<por-switch-field
checked="formValues.enabled"
name="'disableSysctlSettingForRegularUsers'"
label="'Enable pod security constraints'"
feature-id="limitedFeaturePodSecurityPolicy"
label-class="'col-sm-3 col-lg-2 px-0'"
switch-class="'col-sm-8'"
>
</por-switch-field>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>
</div>

View File

@@ -7,6 +7,7 @@ angular.module('portainer.kubernetes').controller('KubernetesSecurityConstraintC
'EndpointService',
function ($scope, EndpointProvider, EndpointService) {
$scope.limitedFeaturePodSecurityPolicy = FeatureId.POD_SECURITY_POLICY_CONSTRAINT;
$scope.limitedFeatureAccessControl = FeatureId.K8S_ACCESS_CONTROL;
$scope.state = {
viewReady: false,
actionInProgress: false,

View File

@@ -0,0 +1,66 @@
import { RawParams } from '@uirouter/react';
import clsx from 'clsx';
import { ReactNode } from 'react';
import { Icon } from '@@/Icon';
import { Link } from '@@/Link';
export interface Tab {
name: string;
icon: ReactNode;
widget: ReactNode;
selectedTabParam: string;
}
interface Props {
currentTabIndex: number;
tabs: Tab[];
}
export function WidgetTabs({ currentTabIndex, tabs }: Props) {
// ensure that the selectedTab param is always valid
const invalidQueryParamValue = tabs.every(
(tab) => encodeURIComponent(tab.selectedTabParam) !== tab.selectedTabParam
);
if (invalidQueryParamValue) {
throw new Error('Invalid query param value for tab');
}
return (
<div className="row">
<div className="col-sm-12 !mb-0">
<div className="pl-2">
{tabs.map(({ name, icon }, index) => (
<Link
to="."
params={{ 'selected-tab': tabs[index].selectedTabParam }}
key={index}
className={clsx(
'inline-flex items-center gap-2 border-0 border-b-2 border-solid bg-transparent px-4 py-2',
currentTabIndex === index
? 'border-blue-8 text-blue-8 th-highcontrast:border-blue-6 th-highcontrast:text-blue-6 th-dark:border-blue-6 th-dark:text-blue-6'
: 'border-transparent'
)}
>
<Icon icon={icon} />
{name}
</Link>
))}
</div>
</div>
</div>
);
}
// findSelectedTabIndex returns the index of the selected-tab, or 0 if none is selected
export function findSelectedTabIndex(
{ params }: { params: RawParams },
tabs: Tab[]
) {
const selectedTabParam = params['selected-tab'] || tabs[0].selectedTabParam;
const currentTabIndex = tabs.findIndex(
(tab) => tab.selectedTabParam === selectedTabParam
);
return currentTabIndex || 0;
}

View File

@@ -4,11 +4,13 @@ import { WidgetFooter } from './WidgetFooter';
import { WidgetTitle } from './WidgetTitle';
import { WidgetTaskbar } from './WidgetTaskbar';
import { Loading } from './Loading';
import { WidgetTabs } from './WidgetTabs';
interface WithSubcomponents {
Body: typeof WidgetBody;
Footer: typeof WidgetFooter;
Title: typeof WidgetTitle;
Tabs: typeof WidgetTabs;
Taskbar: typeof WidgetTaskbar;
Loading: typeof Loading;
}
@@ -18,6 +20,7 @@ const Widget = MainComponent as typeof MainComponent & WithSubcomponents;
Widget.Body = WidgetBody;
Widget.Footer = WidgetFooter;
Widget.Title = WidgetTitle;
Widget.Tabs = WidgetTabs;
Widget.Taskbar = WidgetTaskbar;
Widget.Loading = Loading;
@@ -26,6 +29,7 @@ export {
WidgetBody,
WidgetFooter,
WidgetTitle,
WidgetTabs,
WidgetTaskbar,
Loading,
};

View File

@@ -179,7 +179,11 @@ function TableActions({ selectedItems }: TableActionsProps) {
Remove
</Button>
<Link to="kubernetes.deploy" className="space-left">
<Link
to="kubernetes.deploy"
params={{ referrer: 'kubernetes.services' }}
className="space-left hover:no-underline"
>
<Button className="btn-wrapper" color="primary" icon="plus">
Create from manifest
</Button>

View File

@@ -94,7 +94,11 @@ export function IngressDatatable() {
</Link>
</Authorized>
<Authorized authorizations="K8sIngressesW">
<Link to="kubernetes.deploy" className="space-left no-decoration">
<Link
to="kubernetes.deploy"
className="space-left no-decoration"
params={{ referrer: 'kubernetes.ingresses' }}
>
<Button icon={Plus} className="btn-wrapper">
Create from manifest
</Button>

View File

@@ -40,4 +40,5 @@ export enum FeatureId {
K8S_ROLLING_RESTART = 'k8s-rolling-restart',
K8SINSTALL = 'k8s-install',
K8S_ANNOTATIONS = 'k8s-annotations',
K8S_ACCESS_CONTROL = 'k8s-security',
}

View File

@@ -45,6 +45,7 @@ export async function init(edition: Edition) {
[FeatureId.K8S_ADM_ONLY_USR_INGRESS_DEPLY]: Edition.BE,
[FeatureId.K8S_ROLLING_RESTART]: Edition.BE,
[FeatureId.K8S_ANNOTATIONS]: Edition.BE,
[FeatureId.K8S_ACCESS_CONTROL]: Edition.BE,
};
state.currentEdition = currentEdition;

View File

@@ -16,3 +16,4 @@ export const STACK_PULL_IMAGE = 'stack-pull-image';
export const STACK_WEBHOOK = 'stack-webhook';
export const CONTAINER_WEBHOOK = 'container-webhook';
export const K8S_ANNOTATIONS = 'k8s-annotations';
export const K8S_ACCESS_CONTROL = 'k8s-security';

View File

@@ -1,4 +1,4 @@
import { Box, Edit, Layers, Lock, Server, Shuffle } from 'lucide-react';
import { Box, Edit, Layers, Lock, Server, Shuffle, Shield } from 'lucide-react';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Authorized } from '@/react/hooks/useUser';
@@ -100,6 +100,20 @@ export function KubernetesSidebar({ environmentId }: Props) {
data-cy="k8sSidebar-volumes"
/>
<Authorized
authorizations="K8sClusterSetupRW"
adminOnlyCE
environmentId={environmentId}
>
<SidebarItem
to="kubernetes.security"
params={{ endpointId: environmentId }}
label="Security"
data-cy="k8sSidebar-security"
icon={Shield}
/>
</Authorized>
<SidebarItem
label="Cluster"
to="kubernetes.cluster"
@@ -120,19 +134,6 @@ export function KubernetesSidebar({ environmentId }: Props) {
/>
</Authorized>
<Authorized
authorizations="K8sClusterSetupRW"
adminOnlyCE
environmentId={environmentId}
>
<SidebarItem
to="kubernetes.cluster.securityConstraint"
params={{ endpointId: environmentId }}
label="Security constraints"
data-cy="k8sSidebar-securityConstraints"
/>
</Authorized>
<SidebarItem
to="kubernetes.registries"
params={{ endpointId: environmentId }}