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:
Girish Ramakrishnan
2021-09-21 10:11:27 -07:00
parent f44fa2cf47
commit bb2ad0e986
7 changed files with 193 additions and 53 deletions

View File

@@ -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');