mirror of https://github.com/portainer/portainer
feat(k8s): Introduce the ability to restrict access to default namespace (EE-745) (#5337)
parent
c26af1449c
commit
7d6b1edd48
|
@ -151,11 +151,17 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
|
||||
if payload.Kubernetes != nil {
|
||||
if payload.Kubernetes.Configuration.RestrictDefaultNamespace !=
|
||||
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace {
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
endpoint.Kubernetes = *payload.Kubernetes
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
|
||||
updateAuthorizations = true
|
||||
endpoint.UserAccessPolicies = payload.UserAccessPolicies
|
||||
|
|
|
@ -34,7 +34,7 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo
|
|||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
token, err := getRoundTripToken(request, transport.tokenManager)
|
||||
token, err := transport.getRoundTripToken(request, transport.tokenManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpo
|
|||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
token, err := getRoundTripToken(request, transport.tokenManager)
|
||||
token, err := transport.getRoundTripToken(request, transport.tokenManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ func (manager *tokenManager) getAdminServiceAccountToken() string {
|
|||
return manager.adminToken
|
||||
}
|
||||
|
||||
func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, error) {
|
||||
func (manager *tokenManager) getUserServiceAccountToken(userID int, endpointID portainer.EndpointID) (string, error) {
|
||||
manager.mutex.Lock()
|
||||
defer manager.mutex.Unlock()
|
||||
|
||||
|
@ -61,7 +61,13 @@ func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, err
|
|||
teamIds = append(teamIds, int(membership.TeamID))
|
||||
}
|
||||
|
||||
err = manager.kubecli.SetupUserServiceAccount(userID, teamIds)
|
||||
endpoint, err := manager.dataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
|
||||
err = manager.kubecli.SetupUserServiceAccount(userID, teamIds, restrictDefaultNamespace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ func (transport *baseTransport) executeKubernetesRequest(request *http.Request)
|
|||
// #region ROUND TRIP
|
||||
|
||||
func (transport *baseTransport) prepareRoundTrip(request *http.Request) (string, error) {
|
||||
token, err := getRoundTripToken(request, transport.tokenManager)
|
||||
token, err := transport.getRoundTripToken(request, transport.tokenManager)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ func (transport *baseTransport) RoundTrip(request *http.Request) (*http.Response
|
|||
return transport.proxyKubernetesRequest(request)
|
||||
}
|
||||
|
||||
func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) {
|
||||
func (transport *baseTransport) getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -112,7 +112,7 @@ func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (strin
|
|||
if tokenData.Role == portainer.AdministratorRole {
|
||||
token = tokenManager.getAdminServiceAccountToken()
|
||||
} else {
|
||||
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
||||
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID), transport.endpoint.ID)
|
||||
if err != nil {
|
||||
log.Printf("Failed retrieving service account token: %v", err)
|
||||
return "", err
|
||||
|
|
|
@ -9,10 +9,6 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type (
|
||||
namespaceAccessPolicies map[string]portainer.K8sNamespaceAccessPolicy
|
||||
)
|
||||
|
||||
// NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace
|
||||
func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
|
||||
kcl.lock.Lock()
|
||||
|
@ -48,18 +44,8 @@ func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNam
|
|||
return policies, nil
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error {
|
||||
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
|
||||
|
||||
var accessPolicies namespaceAccessPolicies
|
||||
err = json.Unmarshal([]byte(accessData), &accessPolicies)
|
||||
func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string, restrictDefaultNamespace bool) error {
|
||||
accessPolicies, err := kcl.GetNamespaceAccessPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -70,20 +56,16 @@ func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, service
|
|||
}
|
||||
|
||||
for _, namespace := range namespaces.Items {
|
||||
if namespace.Name == defaultNamespace {
|
||||
continue
|
||||
}
|
||||
|
||||
policies, ok := accessPolicies[namespace.Name]
|
||||
if !ok {
|
||||
err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name)
|
||||
if namespace.Name == defaultNamespace && !restrictDefaultNamespace {
|
||||
err = kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !hasUserAccessToNamespace(userID, teamIDs, policies) {
|
||||
policies, ok := accessPolicies[namespace.Name]
|
||||
if !ok || !hasUserAccessToNamespace(userID, teamIDs, policies) {
|
||||
err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -17,7 +17,7 @@ func (kcl *KubeClient) GetServiceAccountBearerToken(userID int) (string, error)
|
|||
// SetupUserServiceAccount will make sure that all the required resources are created inside the Kubernetes
|
||||
// cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user.
|
||||
//It will also create required default RoleBinding and ClusterRoleBinding rules.
|
||||
func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error {
|
||||
func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error {
|
||||
serviceAccountName := userServiceAccountName(userID, kcl.instanceID)
|
||||
|
||||
err := kcl.ensureRequiredResourcesExist()
|
||||
|
@ -25,20 +25,7 @@ func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error
|
|||
return err
|
||||
}
|
||||
|
||||
err = kcl.ensureServiceAccountForUserExists(serviceAccountName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return kcl.setupNamespaceAccesses(userID, teamIDs, serviceAccountName)
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) ensureRequiredResourcesExist() error {
|
||||
return kcl.createPortainerUserClusterRole()
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName string) error {
|
||||
err := kcl.createUserServiceAccount(portainerNamespace, serviceAccountName)
|
||||
err = kcl.createUserServiceAccount(portainerNamespace, serviceAccountName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -53,7 +40,11 @@ func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName stri
|
|||
return err
|
||||
}
|
||||
|
||||
return kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace)
|
||||
return kcl.setupNamespaceAccesses(userID, teamIDs, serviceAccountName, restrictDefaultNamespace)
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) ensureRequiredResourcesExist() error {
|
||||
return kcl.createPortainerUserClusterRole()
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error {
|
||||
|
|
|
@ -414,10 +414,11 @@ type (
|
|||
|
||||
// KubernetesConfiguration represents the configuration of a Kubernetes endpoint
|
||||
KubernetesConfiguration struct {
|
||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
|
||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
|
||||
RestrictDefaultNamespace bool `json:"RestrictDefaultNamespace"`
|
||||
}
|
||||
|
||||
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
||||
|
@ -1170,7 +1171,7 @@ type (
|
|||
|
||||
// KubeClient represents a service used to query a Kubernetes environment
|
||||
KubeClient interface {
|
||||
SetupUserServiceAccount(userID int, teamIDs []int) error
|
||||
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
|
||||
GetServiceAccountBearerToken(userID int) (string, error)
|
||||
StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
|
||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||
|
|
|
@ -1003,6 +1003,8 @@ definitions:
|
|||
type: boolean
|
||||
UseServerMetrics:
|
||||
type: boolean
|
||||
RestrictDefaultNamespace:
|
||||
type: boolean
|
||||
type: object
|
||||
portainer.KubernetesData:
|
||||
properties:
|
||||
|
|
|
@ -10,5 +10,6 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatab
|
|||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
refreshCallback: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -18,7 +18,11 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC
|
|||
};
|
||||
|
||||
this.canManageAccess = function (item) {
|
||||
return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item);
|
||||
if (!this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace) {
|
||||
return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item);
|
||||
} else {
|
||||
return !this.isSystemNamespace(item);
|
||||
}
|
||||
};
|
||||
|
||||
this.disableRemove = function (item) {
|
||||
|
|
|
@ -145,11 +145,7 @@
|
|||
<label class="control-label text-left">
|
||||
Restrict access to the default namespace
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-setup-default" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.RestrictDefaultNamespace" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -107,6 +107,7 @@ class KubernetesConfigureController {
|
|||
endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||
endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
||||
endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses;
|
||||
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace = this.formValues.RestrictDefaultNamespace;
|
||||
}
|
||||
|
||||
transformFormValues() {
|
||||
|
@ -259,6 +260,7 @@ class KubernetesConfigureController {
|
|||
UseLoadBalancer: false,
|
||||
UseServerMetrics: false,
|
||||
IngressClasses: [],
|
||||
RestrictDefaultNamespace: false,
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -281,6 +283,7 @@ class KubernetesConfigureController {
|
|||
|
||||
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||
this.formValues.RestrictDefaultNamespace = this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace;
|
||||
this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => {
|
||||
ic.IsNew = false;
|
||||
ic.NeedsDeletion = false;
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
order-by="Namespace.Name"
|
||||
remove-action="ctrl.removeAction"
|
||||
refresh-callback="ctrl.getResourcePools"
|
||||
endpoint="ctrl.endpoint"
|
||||
></kubernetes-resource-pools-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsView',
|
|||
templateUrl: './resourcePools.html',
|
||||
controller: 'KubernetesResourcePoolsController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
"dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js",
|
||||
"dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt copy:assets && grunt start:client",
|
||||
"start:toolkit": "grunt start:toolkit",
|
||||
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
|
||||
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
|
||||
"clean:all": "grunt clean:all",
|
||||
"format": "prettier --loglevel warn --write \"**/*.{js,css,html}\"",
|
||||
"lint": "yarn lint:client; yarn lint:server",
|
||||
|
|
Loading…
Reference in New Issue