Implement operator role for apps
There are two main use cases: * A consultant/contractor/external developer is given access to just an app. * A "service" personnel (say upstream app author) is to be given access to single app for debugging. Since, this is an "app admin", they are also given access to apps to be consistent with the idea that Cloudron admin has access to all apps. part of #791
This commit is contained in:
75
src/apps.js
75
src/apps.js
@@ -2,6 +2,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
canAccess,
|
||||
isOperator,
|
||||
removeInternalFields,
|
||||
removeRestrictedFields,
|
||||
|
||||
@@ -23,6 +24,7 @@ exports = module.exports = {
|
||||
uninstall,
|
||||
|
||||
setAccessRestriction,
|
||||
setOperators,
|
||||
setLabel,
|
||||
setIcon,
|
||||
setTags,
|
||||
@@ -124,6 +126,7 @@ exports = module.exports = {
|
||||
_validatePortBindings: validatePortBindings,
|
||||
_validateAccessRestriction: validateAccessRestriction,
|
||||
_translatePortBindings: translatePortBindings,
|
||||
_accessLevel: accessLevel,
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
@@ -164,7 +167,7 @@ const appstore = require('./appstore.js'),
|
||||
|
||||
const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.operatorsJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.enableMailbox', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
|
||||
'apps.dataDir', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(',');
|
||||
@@ -253,6 +256,7 @@ function translatePortBindings(portBindings, manifest) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// also validates operators
|
||||
function validateAccessRestriction(accessRestriction) {
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
|
||||
@@ -435,8 +439,8 @@ function getDataDir(app, dataDir) {
|
||||
function removeInternalFields(app) {
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
|
||||
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
|
||||
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain', 'accessLevel',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'operators',
|
||||
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
|
||||
'label', 'alternateDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts', 'enableMailbox');
|
||||
}
|
||||
@@ -445,7 +449,7 @@ function removeInternalFields(app) {
|
||||
function removeRestrictedFields(app) {
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction', 'alternateDomains', 'aliasDomains', 'sso',
|
||||
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup');
|
||||
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup', 'accessLevel');
|
||||
}
|
||||
|
||||
async function getIcon(app, options) {
|
||||
@@ -511,6 +515,10 @@ function postProcess(result) {
|
||||
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
|
||||
delete result.accessRestrictionJson;
|
||||
|
||||
result.operators = safe.JSON.parse(result.operatorsJson);
|
||||
if (result.operators && !result.operators.users) result.operators.users = [];
|
||||
delete result.operatorsJson;
|
||||
|
||||
result.sso = !!result.sso;
|
||||
result.enableBackup = !!result.enableBackup;
|
||||
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate;
|
||||
@@ -581,22 +589,42 @@ function attachProperties(app, domainObjectMap) {
|
||||
app.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
}
|
||||
|
||||
function isAdmin(user) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
return users.compareRoles(user.role, users.ROLE_ADMIN) >= 0;
|
||||
}
|
||||
|
||||
function isOperator(app, user) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
if (!app.operators) return isAdmin(user);
|
||||
|
||||
if (app.operators.users.some(function (e) { return e === user.id; })) return true;
|
||||
if (!app.operators.groups) return isAdmin(user);
|
||||
if (app.operators.groups.some(function (gid) { return Array.isArray(user.groupIds) && user.groupIds.indexOf(gid) !== -1; })) return true;
|
||||
|
||||
return isAdmin(user);
|
||||
}
|
||||
|
||||
function canAccess(app, user) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
if (app.accessRestriction === null) return true;
|
||||
|
||||
// check user access
|
||||
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return true;
|
||||
|
||||
if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) return true; // admins can always access any app
|
||||
|
||||
if (!app.accessRestriction.groups) return false;
|
||||
|
||||
if (!app.accessRestriction.groups) return isOperator(app, user);
|
||||
if (app.accessRestriction.groups.some(function (gid) { return Array.isArray(user.groupIds) && user.groupIds.indexOf(gid) !== -1; })) return true;
|
||||
|
||||
return false;
|
||||
return isOperator(app, user);
|
||||
}
|
||||
|
||||
function accessLevel(app, user) {
|
||||
if (isAdmin(user)) return 'admin';
|
||||
if (isOperator(app, user)) return 'operator';
|
||||
return canAccess(app, user) ? 'user' : null;
|
||||
}
|
||||
|
||||
async function add(id, appStoreId, manifest, location, domain, portBindings, data) {
|
||||
@@ -900,8 +928,16 @@ async function getByFqdn(fqdn) {
|
||||
async function listByUser(user) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
const result = await list();
|
||||
return result.filter((app) => canAccess(app, user));
|
||||
const allApps = await list();
|
||||
const result = [];
|
||||
for (const app of allApps) {
|
||||
const al = accessLevel(app, user);
|
||||
if (!al) continue;
|
||||
app.accessLevel = al;
|
||||
result.push(app);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function downloadManifest(appStoreId, manifest) {
|
||||
@@ -1172,6 +1208,19 @@ async function setAccessRestriction(app, accessRestriction, auditSource) {
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, accessRestriction });
|
||||
}
|
||||
|
||||
async function setOperators(app, operators, auditSource) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof operators, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const appId = app.id;
|
||||
let error = validateAccessRestriction(operators); // not a typo. same structure for operators and accessRestriction
|
||||
if (error) throw error;
|
||||
|
||||
await update(appId, { operators });
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, operators });
|
||||
}
|
||||
|
||||
async function setLabel(app, label, auditSource) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof label, 'string');
|
||||
|
||||
Reference in New Issue
Block a user