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