413 lines
18 KiB
JavaScript
413 lines
18 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2016 Red Hat, Inc.
|
|
*
|
|
* Cockpit is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Cockpit is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
(function() {
|
|
var angular = require('angular');
|
|
require('./kube-client');
|
|
|
|
angular.module('registry.policy', [
|
|
'kubeClient',
|
|
])
|
|
|
|
.factory("projectPolicy", [
|
|
'$q',
|
|
'$rootScope',
|
|
'kubeLoader',
|
|
'kubeMethods',
|
|
'kubeSelect',
|
|
'KubeWatch',
|
|
'KubeRequest',
|
|
'KUBE_SCHEMA',
|
|
function($q, $rootScope, loader, methods, select, watch, KubeRequest, KUBE_SCHEMA) {
|
|
var apiGroup;
|
|
var RBAC_GROUP = "rbac.authorization.k8s.io";
|
|
var RBAC_API = "/apis/rbac.authorization.k8s.io/v1beta1";
|
|
var POLICY_BINDING_API = KUBE_SCHEMA["RoleBinding"]["api"];
|
|
var watchPromise;
|
|
|
|
function setupRoleBinding(group) {
|
|
KUBE_SCHEMA["RoleBinding"]["api"] = group ? RBAC_API : POLICY_BINDING_API;
|
|
KUBE_SCHEMA["rolebindings"]["api"] = group ? RBAC_API : POLICY_BINDING_API;
|
|
apiGroup = group;
|
|
expireSAR(null);
|
|
expireWhoCan(null);
|
|
return group ? "rolebindings" : "policybindings";
|
|
}
|
|
|
|
function ensureWatchType() {
|
|
if (!watchPromise) {
|
|
watchPromise = new KubeRequest("GET", "/oapi/v1")
|
|
.then(function(response) {
|
|
var data = response.data || {};
|
|
var i;
|
|
var l = data.resources || [];
|
|
for (i = 0; i < l.length; i++) {
|
|
if (l[i].kind == "PolicyBinding")
|
|
return setupRoleBinding();
|
|
}
|
|
return setupRoleBinding(RBAC_GROUP);
|
|
}, function(err) {
|
|
console.warn("Error getting API", err);
|
|
return setupRoleBinding();
|
|
});
|
|
}
|
|
return watchPromise;
|
|
}
|
|
|
|
/*
|
|
* Data loading hacks:
|
|
*
|
|
* We would like to watch rolebindings, but not all versions support
|
|
* that. So we have to watch policybindings and then infer the
|
|
* rolebindings from there.
|
|
*
|
|
* In addition we would like to be able to load User and Group objects,
|
|
* even if only for a certain project. However, non-cluster admins
|
|
* fail to do this, so we simulate these objects from the role bindings.
|
|
*/
|
|
loader.listen(function(present, removed) {
|
|
var link;
|
|
var expire = { };
|
|
|
|
/* If reseting clear status */
|
|
if (!present && !removed) {
|
|
expireSAR(null);
|
|
expireWhoCan(null);
|
|
watchPromise = null;
|
|
return;
|
|
}
|
|
|
|
for (link in removed) {
|
|
if (removed[link].kind == "PolicyBinding") {
|
|
update_rolebindings(removed[link].roleBindings, true);
|
|
expire[removed[link].metadata.namespace] = true;
|
|
}
|
|
}
|
|
for (link in present) {
|
|
if (present[link].kind == "PolicyBinding") {
|
|
update_rolebindings(present[link].roleBindings, false);
|
|
expire[present[link].metadata.namespace] = true;
|
|
} else if (present[link].kind == "RoleBinding") {
|
|
ensure_subjects(present[link].subjects || []);
|
|
expire[present[link].metadata.namespace] = true;
|
|
}
|
|
}
|
|
|
|
var namespace;
|
|
for (namespace in expire) {
|
|
expireWhoCan(namespace);
|
|
expireSAR(namespace);
|
|
}
|
|
});
|
|
|
|
function update_rolebindings(bindings, removed) {
|
|
angular.forEach(bindings || [], function(wrapper) {
|
|
loader.handle(wrapper.roleBinding, removed, "RoleBinding");
|
|
});
|
|
}
|
|
|
|
function ensure_subjects(subjects) {
|
|
angular.forEach(subjects, function(subject) {
|
|
var link = loader.resolve(subject.kind, subject.name, subject.namespace);
|
|
if (link in loader.objects)
|
|
return;
|
|
|
|
/* Don't show system groups */
|
|
if (subject.kind == "Group" && subject.name.indexOf("system:") === 0)
|
|
return;
|
|
|
|
/* An interim object, until perhaps the real thing can be loaded */
|
|
var interim = { kind: subject.kind, apiVersion: "v1", metadata: { name: subject.name } };
|
|
if (subject.namespace)
|
|
interim.metadata.namespace = subject.namespace;
|
|
loader.handle(interim);
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Cached localresourceaccessreviews responses, and expired data
|
|
* Each one has a project key, containing an object with verb:resource keys.
|
|
*/
|
|
var cached = { };
|
|
var expired = { };
|
|
|
|
function fillWhoCan(namespace, verb, resource, result) {
|
|
var key = verb + ":" + resource;
|
|
|
|
if (!(namespace in cached))
|
|
cached[namespace] = { };
|
|
cached[namespace][key] = result;
|
|
|
|
if (result) {
|
|
if (namespace in expired) {
|
|
delete expired[namespace][key];
|
|
}
|
|
}
|
|
|
|
$rootScope.$applyAsync();
|
|
}
|
|
|
|
function expireWhoCan(namespace) {
|
|
if (namespace) {
|
|
expired[namespace] = angular.extend({ }, cached[namespace]);
|
|
delete cached[namespace];
|
|
} else {
|
|
expired = cached;
|
|
cached = { };
|
|
}
|
|
|
|
$rootScope.$applyAsync();
|
|
}
|
|
|
|
function lookupWhoCan(namespace, verb, resource) {
|
|
var key = verb + ":" + resource;
|
|
|
|
var ask = true;
|
|
var result = null;
|
|
|
|
var data = cached[namespace];
|
|
if (data) {
|
|
if (key in data) {
|
|
result = data[key];
|
|
ask = false;
|
|
}
|
|
}
|
|
|
|
if (!result) {
|
|
data = expired[namespace];
|
|
if (data) {
|
|
if (key in data)
|
|
result = data[key];
|
|
}
|
|
}
|
|
|
|
if (!ask)
|
|
return result;
|
|
|
|
/* Perform a request */
|
|
var request = {
|
|
kind: "LocalResourceAccessReview",
|
|
apiVersion: "v1",
|
|
namespace: "",
|
|
verb: verb,
|
|
resource: resource,
|
|
resourceName: "",
|
|
content: null
|
|
};
|
|
|
|
/* Fill in null info while looking up */
|
|
fillWhoCan(namespace, verb, resource, null);
|
|
|
|
var path = loader.resolve("localresourceaccessreviews", null, namespace);
|
|
methods.post(path, request)
|
|
.then(function(response) {
|
|
fillWhoCan(namespace, verb, resource, response);
|
|
}, function(response) {
|
|
console.warn("failed to lookup access:", namespace, verb, resource + ":",
|
|
response.message || JSON.stringify(response));
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
var sarCache = { };
|
|
|
|
function subjectAccessReview(namespace, user, verb, resource) {
|
|
var key = namespace + ':' + (user ? user.metadata.name : "") + ':' + verb + ':' + resource;
|
|
var defer = $q.defer();
|
|
|
|
if (key in sarCache) {
|
|
defer.resolve(sarCache[key]);
|
|
} else {
|
|
var request = {
|
|
kind: "SubjectAccessReview",
|
|
apiVersion: "v1",
|
|
namespace: namespace,
|
|
verb: verb,
|
|
resource: resource
|
|
};
|
|
|
|
methods.post(loader.resolve("subjectaccessreviews"), request)
|
|
.then(function(response) {
|
|
sarCache[key] = response.allowed;
|
|
defer.resolve(response.allowed);
|
|
}, function(response) {
|
|
console.warn("failed to review subject access:", response.message || JSON.stringify(response));
|
|
defer.reject(response.message || JSON.stringify(response));
|
|
});
|
|
}
|
|
|
|
return defer.promise;
|
|
}
|
|
|
|
function expireSAR(namespace) {
|
|
if (namespace) {
|
|
for (var key in sarCache) {
|
|
if (key.lastIndexOf(namespace + ':', 0) === 0)
|
|
delete sarCache[key];
|
|
}
|
|
} else {
|
|
sarCache = { };
|
|
}
|
|
|
|
$rootScope.$applyAsync();
|
|
}
|
|
|
|
/*
|
|
* HACK: There's no way to PATCH subjects in or out
|
|
* of a role, so we have to use this race prone mechanism.
|
|
*/
|
|
function modifyRole(namespace, role, callback) {
|
|
var path = loader.resolve("RoleBinding", role, namespace);
|
|
return loader.load(path)
|
|
.then(function(resource) {
|
|
callback(resource);
|
|
return methods.put(path, resource);
|
|
});
|
|
}
|
|
|
|
function createRole(namespace, role, subjects) {
|
|
var name = toName(role);
|
|
var binding = {
|
|
kind: "RoleBinding",
|
|
metadata: {
|
|
name: name,
|
|
namespace: namespace,
|
|
creationTimestamp: null,
|
|
},
|
|
userNames: [],
|
|
groupNames: [],
|
|
subjects: [],
|
|
roleRef: {
|
|
name: role,
|
|
kind: "ClusterRole",
|
|
}
|
|
};
|
|
addToArray(roleArray(binding, "subjects"), subjects);
|
|
addToArray(roleArrayKind(binding, subjects.kind), subjects.name);
|
|
return methods.create(binding, namespace);
|
|
}
|
|
|
|
function removeFromRole(project, role, subject) {
|
|
subject.apiGroup = apiGroup;
|
|
var namespace = toName(project);
|
|
return modifyRole(namespace, role, function(data) {
|
|
removeFromArray(roleArray(data, "subjects"), subject);
|
|
removeFromArray(roleArrayKind(data, subject.kind), subject.name);
|
|
}).then(function() {
|
|
expireWhoCan(namespace);
|
|
}, function(resp) {
|
|
/* If the role doesn't exist consider removed to work */
|
|
if (resp.code !== 404)
|
|
return $q.reject(resp);
|
|
});
|
|
}
|
|
|
|
function removeMemberFromProject(project, subjectRoleBindings, subject) {
|
|
var registryRoles = ["registry-admin", "registry-editor", "registry-viewer"];
|
|
var chain = $q.when();
|
|
subject.apiGroup = apiGroup;
|
|
|
|
angular.forEach(subjectRoleBindings, function(role) {
|
|
// Since we only added registry roles
|
|
// remove ONLY registry roles
|
|
if (indexOf(registryRoles, role.roleRef.name) !== -1) {
|
|
chain = chain.then(function() {
|
|
return removeFromRole(project, role.roleRef.name, subject);
|
|
});
|
|
}
|
|
});
|
|
return chain;
|
|
}
|
|
function indexOf(array, value) {
|
|
var i, len;
|
|
for (i = 0, len = array.length; i < len; i++) {
|
|
if (angular.equals(array[i], value))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function addToArray(array, value) {
|
|
var index = indexOf(array, value);
|
|
if (index < 0)
|
|
array.push(value);
|
|
}
|
|
|
|
function removeFromArray(array, value) {
|
|
var index = indexOf(array, value);
|
|
if (index >= 0)
|
|
array.splice(index, 1);
|
|
}
|
|
|
|
function roleArray(data, field) {
|
|
var array = data[field] || [];
|
|
data[field] = array;
|
|
return array;
|
|
}
|
|
|
|
function roleArrayKind(data, kind) {
|
|
if (kind == "Group" || kind == "SystemGroup")
|
|
return roleArray(data, "groupNames");
|
|
else
|
|
return roleArray(data, "userNames");
|
|
}
|
|
|
|
function toName(object) {
|
|
if (typeof object == "object")
|
|
return object.metadata.name;
|
|
else
|
|
return object;
|
|
}
|
|
|
|
return {
|
|
watch: function watch(until) {
|
|
ensureWatchType().then(function (what) {
|
|
loader.watch(what, until)
|
|
.then(function() {
|
|
expireWhoCan(null);
|
|
});
|
|
});
|
|
},
|
|
whoCan: function whoCan(project, verb, resource) {
|
|
return lookupWhoCan(toName(project), verb, resource);
|
|
},
|
|
addToRole: function addToRole(project, role, subject) {
|
|
subject.apiGroup = apiGroup;
|
|
var namespace = toName(project);
|
|
return modifyRole(namespace, role, function(data) {
|
|
addToArray(roleArray(data, "subjects"), subject);
|
|
addToArray(roleArrayKind(data, subject.kind), subject.name);
|
|
}).then(function() {
|
|
expireWhoCan(namespace);
|
|
}, function(resp) {
|
|
/* If the role doesn't exist create it */
|
|
if (resp.code === 404)
|
|
return createRole(namespace, role, subject);
|
|
return $q.reject(resp);
|
|
});
|
|
},
|
|
removeFromRole: removeFromRole,
|
|
removeMemberFromProject: removeMemberFromProject,
|
|
subjectAccessReview: subjectAccessReview
|
|
};
|
|
}
|
|
]);
|
|
}());
|