mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 07:49:41 +02:00
feat(rbac): detect if rbac is enabled [EE-4308] (#8139)
This commit is contained in:
parent
8dcc5e4adb
commit
c1cc8bad77
10 changed files with 327 additions and 6 deletions
|
@ -58,6 +58,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz
|
||||||
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllers)).Methods(http.MethodPut)
|
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllers)).Methods(http.MethodPut)
|
||||||
endpointRouter.Handle("/ingresses/delete", httperror.LoggerHandler(h.deleteKubernetesIngresses)).Methods(http.MethodPost)
|
endpointRouter.Handle("/ingresses/delete", httperror.LoggerHandler(h.deleteKubernetesIngresses)).Methods(http.MethodPost)
|
||||||
endpointRouter.Handle("/services/delete", httperror.LoggerHandler(h.deleteKubernetesServices)).Methods(http.MethodPost)
|
endpointRouter.Handle("/services/delete", httperror.LoggerHandler(h.deleteKubernetesServices)).Methods(http.MethodPost)
|
||||||
|
endpointRouter.Path("/rbac_enabled").Handler(httperror.LoggerHandler(h.isRBACEnabled)).Methods(http.MethodGet)
|
||||||
endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.createKubernetesNamespace)).Methods(http.MethodPost)
|
endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.createKubernetesNamespace)).Methods(http.MethodPost)
|
||||||
endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.updateKubernetesNamespace)).Methods(http.MethodPut)
|
endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.updateKubernetesNamespace)).Methods(http.MethodPut)
|
||||||
endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.getKubernetesNamespaces)).Methods(http.MethodGet)
|
endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.getKubernetesNamespaces)).Methods(http.MethodGet)
|
||||||
|
|
51
api/http/handler/kubernetes/rbac.go
Normal file
51
api/http/handler/kubernetes/rbac.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id IsRBACEnabled
|
||||||
|
// @summary Check if RBAC is enabled
|
||||||
|
// @description Check if RBAC is enabled in the current Kubernetes cluster.
|
||||||
|
// @description **Access policy**: administrator
|
||||||
|
// @tags rbac_enabled
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @produce text/plain
|
||||||
|
// @param id path int true "Environment(Endpoint) identifier"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/rbac_enabled [get]
|
||||||
|
func (handler *Handler) isRBACEnabled(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
// with the endpoint id and user auth, create a kube client instance
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return httperror.BadRequest(
|
||||||
|
"Invalid environment identifier route variable",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient(
|
||||||
|
strconv.Itoa(endpointID), r.Header.Get("Authorization"),
|
||||||
|
)
|
||||||
|
if !ok {
|
||||||
|
return httperror.InternalServerError(
|
||||||
|
"Failed to lookup KubeClient",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// with the kube client instance, check if RBAC is enabled
|
||||||
|
isRBACEnabled, err := cli.IsRBACEnabled()
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Failed to check RBAC status", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, isRBACEnabled)
|
||||||
|
}
|
14
api/internal/randomstring/random_string.go
Normal file
14
api/internal/randomstring/random_string.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package randomstring
|
||||||
|
|
||||||
|
import "math/rand"
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
||||||
|
// RandomString returns a random lowercase alphanumeric string of length n
|
||||||
|
func RandomString(n int) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
166
api/kubernetes/cli/rbac.go
Normal file
166
api/kubernetes/cli/rbac.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/internal/randomstring"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
authv1 "k8s.io/api/authorization/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
authv1types "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||||
|
corev1types "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
rbacv1types "k8s.io/client-go/kubernetes/typed/rbac/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsRBACEnabled checks if RBAC is enabled in the cluster by creating a service account, then checking it's access to a resourcequota before and after setting a cluster role and cluster role binding
|
||||||
|
func (kcl *KubeClient) IsRBACEnabled() (bool, error) {
|
||||||
|
namespace := "default"
|
||||||
|
verb := "list"
|
||||||
|
resource := "resourcequotas"
|
||||||
|
|
||||||
|
saClient := kcl.cli.CoreV1().ServiceAccounts(namespace)
|
||||||
|
uniqueString := randomstring.RandomString(4) // append a unique string to resource names, incase they already exist
|
||||||
|
saName := "portainer-rbac-test-sa-" + uniqueString
|
||||||
|
err := createServiceAccount(saClient, saName, namespace)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error creating service account")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer deleteServiceAccount(saClient, saName)
|
||||||
|
|
||||||
|
accessReviewClient := kcl.cli.AuthorizationV1().LocalSubjectAccessReviews(namespace)
|
||||||
|
allowed, err := checkServiceAccountAccess(accessReviewClient, saName, verb, resource, namespace)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error checking service account access")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the service account with no authorizations is allowed, RBAC must be disabled
|
||||||
|
if allowed {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise give the service account an rbac authorisation and check again
|
||||||
|
roleClient := kcl.cli.RbacV1().Roles(namespace)
|
||||||
|
roleName := "portainer-rbac-test-role-" + uniqueString
|
||||||
|
err = createRole(roleClient, roleName, verb, resource, namespace)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error creating role")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer deleteRole(roleClient, roleName)
|
||||||
|
|
||||||
|
roleBindingClient := kcl.cli.RbacV1().RoleBindings(namespace)
|
||||||
|
roleBindingName := "portainer-rbac-test-role-binding-" + uniqueString
|
||||||
|
err = createRoleBinding(roleBindingClient, roleBindingName, roleName, saName, namespace)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error creating role binding")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer deleteRoleBinding(roleBindingClient, roleBindingName)
|
||||||
|
|
||||||
|
allowed, err = checkServiceAccountAccess(accessReviewClient, saName, verb, resource, namespace)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error checking service account access with authorizations added")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the service account allowed to list resource quotas after given rbac role, then RBAC is enabled
|
||||||
|
return allowed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createServiceAccount(saClient corev1types.ServiceAccountInterface, name string, namespace string) error {
|
||||||
|
serviceAccount := &corev1.ServiceAccount{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := saClient.Create(context.Background(), serviceAccount, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteServiceAccount(saClient corev1types.ServiceAccountInterface, name string) {
|
||||||
|
err := saClient.Delete(context.Background(), name, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error deleting service account: " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRole(roleClient rbacv1types.RoleInterface, name string, verb string, resource string, namespace string) error {
|
||||||
|
role := &rbacv1.Role{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Rules: []rbacv1.PolicyRule{
|
||||||
|
{
|
||||||
|
APIGroups: []string{""},
|
||||||
|
Verbs: []string{verb},
|
||||||
|
Resources: []string{resource},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := roleClient.Create(context.Background(), role, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteRole(roleClient rbacv1types.RoleInterface, name string) {
|
||||||
|
err := roleClient.Delete(context.Background(), name, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error deleting role: " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRoleBinding(roleBindingClient rbacv1types.RoleBindingInterface, clusterRoleBindingName string, roleName string, serviceAccountName string, namespace string) error {
|
||||||
|
clusterRoleBinding := &rbacv1.RoleBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: clusterRoleBindingName,
|
||||||
|
},
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Name: serviceAccountName,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RoleRef: rbacv1.RoleRef{
|
||||||
|
Kind: "Role",
|
||||||
|
Name: roleName,
|
||||||
|
APIGroup: "rbac.authorization.k8s.io",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := roleBindingClient.Create(context.Background(), clusterRoleBinding, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteRoleBinding(roleBindingClient rbacv1types.RoleBindingInterface, name string) {
|
||||||
|
err := roleBindingClient.Delete(context.Background(), name, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error deleting role binding: " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkServiceAccountAccess(accessReviewClient authv1types.LocalSubjectAccessReviewInterface, serviceAccountName string, verb string, resource string, namespace string) (bool, error) {
|
||||||
|
subjectAccessReview := &authv1.LocalSubjectAccessReview{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Spec: authv1.SubjectAccessReviewSpec{
|
||||||
|
ResourceAttributes: &authv1.ResourceAttributes{
|
||||||
|
Namespace: namespace,
|
||||||
|
Verb: verb,
|
||||||
|
Resource: resource,
|
||||||
|
},
|
||||||
|
User: "system:serviceaccount:default:" + serviceAccountName, // a workaround to be able to use the service account as a user
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := accessReviewClient.Create(context.Background(), subjectAccessReview, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return result.Status.Allowed, nil
|
||||||
|
}
|
|
@ -1357,6 +1357,7 @@ type (
|
||||||
// KubeClient represents a service used to query a Kubernetes environment(endpoint)
|
// KubeClient represents a service used to query a Kubernetes environment(endpoint)
|
||||||
KubeClient interface {
|
KubeClient interface {
|
||||||
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
|
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
|
||||||
|
IsRBACEnabled() (bool, error)
|
||||||
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
|
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
|
||||||
GetServiceAccountBearerToken(userID int) (string, error)
|
GetServiceAccountBearerToken(userID int) (string, error)
|
||||||
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
|
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
|
||||||
|
|
|
@ -118,6 +118,29 @@
|
||||||
<!-- #region SECURITY -->
|
<!-- #region SECURITY -->
|
||||||
<div class="col-sm-12 form-section-title"> Security </div>
|
<div class="col-sm-12 form-section-title"> Security </div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ng-if="!ctrl.isRBACEnabled"
|
||||||
|
class="mt-1 mb-6 p-4 w-full border border-solid bg-warning-2 border-warning-5 text-warning-8 th-dark:bg-yellow-11 th-dark:text-white th-highcontrast:bg-yellow-11 th-highcontrast:text-white small flex gap-1 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="mt-0.5">
|
||||||
|
<pr-icon icon="'alert-triangle'" feather="true" class-name="'text-warning-7 th-dark:text-white th-highcontrast:text-white'"></pr-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p> Your cluster does not have Kubernetes role-based access control (RBAC) enabled. </p>
|
||||||
|
<p> This means you can't use Portainer RBAC functionality to regulate access to environment resources based on user roles. </p>
|
||||||
|
<p class="mb-0">
|
||||||
|
To enable RBAC, start the <a
|
||||||
|
class="th-dark:text-blue-7 th-highcontrast:text-blue-4"
|
||||||
|
href="https://kubernetes.io/docs/concepts/overview/components/#kube-apiserver"
|
||||||
|
target="_blank"
|
||||||
|
>API server</a
|
||||||
|
> with the <code class="box-decoration-clone bg-gray-4 th-dark:bg-black th-highcontrast:bg-black">--authorization-mode</code> flag set to a
|
||||||
|
comma-separated list that includes <code class="bg-gray-4 th-dark:bg-black th-highcontrast:bg-black">RBAC</code>, for example:
|
||||||
|
<code class="box-decoration-clone bg-gray-4 th-dark:bg-black th-highcontrast:bg-black">kube-apiserver --authorization-mode=Example1,RBAC,Example2</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="col-sm-12 text-muted small">
|
<span class="col-sm-12 text-muted small">
|
||||||
By default, all the users have access to the default namespace. Enable this option to set accesses on the default namespace.
|
By default, all the users have access to the default namespace. Enable this option to set accesses on the default namespace.
|
||||||
|
@ -126,10 +149,17 @@
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="control-label text-left col-sm-5 col-lg-4 px-0"> Restrict access to the default namespace </label>
|
<por-switch-field
|
||||||
<label class="switch col-sm-8">
|
checked="ctrl.formValues.RestrictDefaultNamespace"
|
||||||
<input type="checkbox" ng-model="ctrl.formValues.RestrictDefaultNamespace" /><span class="slider round" data-cy="kubeSetup-restrictDefaultNsToggle"></span>
|
name="'restrictDefaultNs'"
|
||||||
</label>
|
label="'Restrict access to the default namespace'"
|
||||||
|
on-change="(ctrl.onToggleRestrictNs)"
|
||||||
|
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||||
|
switch-class="'col-sm-8 text-muted'"
|
||||||
|
data-cy="kubeSetup-restrictDefaultNsToggle"
|
||||||
|
disabled="!ctrl.isRBACEnabled"
|
||||||
|
>
|
||||||
|
</por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-12 mt-5">
|
<div class="col-sm-12 mt-5">
|
||||||
|
@ -141,6 +171,7 @@
|
||||||
switch-class="'col-sm-8 text-muted'"
|
switch-class="'col-sm-8 text-muted'"
|
||||||
data-cy="kubeSetup-restrictStandardUserIngressWToggle"
|
data-cy="kubeSetup-restrictStandardUserIngressWToggle"
|
||||||
feature-id="ctrl.limitedFeatureIngressDeploy"
|
feature-id="ctrl.limitedFeatureIngressDeploy"
|
||||||
|
disabled="!ctrl.isRBACEnabled"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
|
|
||||||
import { getIngressControllerClassMap, updateIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
import { getIngressControllerClassMap, updateIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
||||||
|
import { getIsRBACEnabled } from '@/react/kubernetes/cluster/service';
|
||||||
|
|
||||||
class KubernetesConfigureController {
|
class KubernetesConfigureController {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
|
@ -54,6 +55,7 @@ class KubernetesConfigureController {
|
||||||
this.onToggleIngressAvailabilityPerNamespace = this.onToggleIngressAvailabilityPerNamespace.bind(this);
|
this.onToggleIngressAvailabilityPerNamespace = this.onToggleIngressAvailabilityPerNamespace.bind(this);
|
||||||
this.onToggleAllowNoneIngressClass = this.onToggleAllowNoneIngressClass.bind(this);
|
this.onToggleAllowNoneIngressClass = this.onToggleAllowNoneIngressClass.bind(this);
|
||||||
this.onChangeStorageClassAccessMode = this.onChangeStorageClassAccessMode.bind(this);
|
this.onChangeStorageClassAccessMode = this.onChangeStorageClassAccessMode.bind(this);
|
||||||
|
this.onToggleRestrictNs = this.onToggleRestrictNs.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
@ -261,6 +263,12 @@ class KubernetesConfigureController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onToggleRestrictNs() {
|
||||||
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues.RestrictDefaultNamespace = !this.formValues.RestrictDefaultNamespace;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* #region ON INIT */
|
/* #region ON INIT */
|
||||||
async onInit() {
|
async onInit() {
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -292,11 +300,18 @@ class KubernetesConfigureController {
|
||||||
IngressAvailabilityPerNamespace: false,
|
IngressAvailabilityPerNamespace: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// default to true if error is thrown
|
||||||
|
this.isRBACEnabled = true;
|
||||||
|
|
||||||
this.isIngressControllersLoading = true;
|
this.isIngressControllersLoading = true;
|
||||||
try {
|
try {
|
||||||
this.availableAccessModes = new KubernetesStorageClassAccessPolicies();
|
this.availableAccessModes = new KubernetesStorageClassAccessPolicies();
|
||||||
|
|
||||||
[this.StorageClasses, this.endpoint] = await Promise.all([this.KubernetesStorageService.get(this.state.endpointId), this.EndpointService.endpoint(this.state.endpointId)]);
|
[this.StorageClasses, this.endpoint, this.isRBACEnabled] = await Promise.all([
|
||||||
|
this.KubernetesStorageService.get(this.state.endpointId),
|
||||||
|
this.EndpointService.endpoint(this.state.endpointId),
|
||||||
|
getIsRBACEnabled(this.state.endpointId),
|
||||||
|
]);
|
||||||
|
|
||||||
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.state.endpointId });
|
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.state.endpointId });
|
||||||
this.originalIngressControllers = structuredClone(this.ingressControllers) || [];
|
this.originalIngressControllers = structuredClone(this.ingressControllers) || [];
|
||||||
|
|
|
@ -44,6 +44,28 @@
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<div
|
||||||
|
ng-if="!ctrl.isRBACEnabled"
|
||||||
|
class="mb-6 mx-[15px] p-4 border border-solid bg-warning-2 border-warning-5 text-warning-8 th-dark:bg-yellow-11 th-dark:text-white th-highcontrast:bg-yellow-11 th-highcontrast:text-white small flex gap-1 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="mt-0.5">
|
||||||
|
<pr-icon icon="'alert-triangle'" feather="true" class-name="'text-warning-7 th-dark:text-white th-highcontrast:text-white'"></pr-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p> Your cluster does not have Kubernetes role-based access control (RBAC) enabled. </p>
|
||||||
|
<p> This means you can't use Portainer RBAC functionality to regulate access to environment resources based on user roles. </p>
|
||||||
|
<p class="mb-0">
|
||||||
|
To enable RBAC, start the <a
|
||||||
|
class="th-dark:text-blue-7 th-highcontrast:text-blue-4"
|
||||||
|
href="https://kubernetes.io/docs/concepts/overview/components/#kube-apiserver"
|
||||||
|
target="_blank"
|
||||||
|
>API server</a
|
||||||
|
> with the <code class="box-decoration-clone bg-gray-4 th-dark:bg-black th-highcontrast:bg-black">--authorization-mode</code> flag set to a
|
||||||
|
comma-separated list that includes <code class="bg-gray-4 th-dark:bg-black th-highcontrast:bg-black">RBAC</code>, for example:
|
||||||
|
<code class="box-decoration-clone bg-gray-4 th-dark:bg-black th-highcontrast:bg-black">kube-apiserver --authorization-mode=Example1,RBAC,Example2</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<span class="col-sm-12 small text-warning">
|
<span class="col-sm-12 small text-warning">
|
||||||
<p class="vertical-center">
|
<p class="vertical-center">
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||||
|
|
|
@ -3,13 +3,15 @@ import _ from 'lodash-es';
|
||||||
import { KubernetesPortainerConfigMapConfigName, KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapAccessKey } from 'Kubernetes/models/config-map/models';
|
import { KubernetesPortainerConfigMapConfigName, KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapAccessKey } from 'Kubernetes/models/config-map/models';
|
||||||
import { UserAccessViewModel, TeamAccessViewModel } from 'Portainer/models/access';
|
import { UserAccessViewModel, TeamAccessViewModel } from 'Portainer/models/access';
|
||||||
import KubernetesConfigMapHelper from 'Kubernetes/helpers/configMapHelper';
|
import KubernetesConfigMapHelper from 'Kubernetes/helpers/configMapHelper';
|
||||||
|
import { getIsRBACEnabled } from '@/react/kubernetes/cluster/service';
|
||||||
|
|
||||||
class KubernetesResourcePoolAccessController {
|
class KubernetesResourcePoolAccessController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, $scope, Notifications, KubernetesResourcePoolService, KubernetesConfigMapService, GroupService, AccessService) {
|
constructor($async, $state, $scope, Notifications, KubernetesResourcePoolService, KubernetesConfigMapService, GroupService, AccessService, EndpointProvider) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.$scope = $scope;
|
this.$scope = $scope;
|
||||||
|
this.EndpointProvider = EndpointProvider;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||||
this.KubernetesConfigMapService = KubernetesConfigMapService;
|
this.KubernetesConfigMapService = KubernetesConfigMapService;
|
||||||
|
@ -45,12 +47,16 @@ class KubernetesResourcePoolAccessController {
|
||||||
multiselectOutput: [],
|
multiselectOutput: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// default to true if error is thrown
|
||||||
|
this.isRBACEnabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const name = this.$transition$.params().id;
|
const name = this.$transition$.params().id;
|
||||||
let [pool, configMap] = await Promise.all([
|
let [pool, configMap] = await Promise.all([
|
||||||
this.KubernetesResourcePoolService.get(name),
|
this.KubernetesResourcePoolService.get(name),
|
||||||
this.KubernetesConfigMapService.getAccess(KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapConfigName),
|
this.KubernetesConfigMapService.getAccess(KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapConfigName),
|
||||||
]);
|
]);
|
||||||
|
this.isRBACEnabled = await getIsRBACEnabled(this.EndpointProvider.endpointID());
|
||||||
const group = await this.GroupService.group(endpoint.GroupId);
|
const group = await this.GroupService.group(endpoint.GroupId);
|
||||||
const roles = [];
|
const roles = [];
|
||||||
const endpointAccesses = await this.AccessService.accesses(endpoint, group, roles);
|
const endpointAccesses = await this.AccessService.accesses(endpoint, group, roles);
|
||||||
|
|
14
app/react/kubernetes/cluster/service.ts
Normal file
14
app/react/kubernetes/cluster/service.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import PortainerError from '@/portainer/error';
|
||||||
|
import axios from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
export async function getIsRBACEnabled(environmentId: EnvironmentId) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`kubernetes/${environmentId}/rbac_enabled`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new PortainerError('Unable to check if RBAC is enabled.', e as Error);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue