mirror of
https://github.com/HeyPuter/puter.git
synced 2025-12-30 17:50:00 -06:00
dev: begin adding developer app permissions
This commit is contained in:
61
src/backend/src/routers/auth/grant-dev-app.js
Normal file
61
src/backend/src/routers/auth/grant-dev-app.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const APIError = require("../../api/APIError");
|
||||
const eggspress = require("../../api/eggspress");
|
||||
const { UserActorType } = require("../../services/auth/Actor");
|
||||
const { Context } = require("../../util/context");
|
||||
|
||||
module.exports = eggspress('/auth/grant-dev-app', {
|
||||
subdomain: 'api',
|
||||
auth2: true,
|
||||
allowedMethods: ['POST'],
|
||||
}, async (req, res, next) => {
|
||||
const x = Context.get();
|
||||
const svc_permission = x.get('services').get('permission');
|
||||
|
||||
// Only users can grant user-app permissions
|
||||
const actor = Context.get('actor');
|
||||
if ( ! (actor.type instanceof UserActorType) ) {
|
||||
throw APIError.create('forbidden');
|
||||
}
|
||||
|
||||
if ( req.body.origin ) {
|
||||
const svc_auth = x.get('services').get('auth');
|
||||
req.body.app_uid = await svc_auth.app_uid_from_origin(req.body.origin);
|
||||
}
|
||||
|
||||
if ( ! req.body.app_uid ) {
|
||||
throw APIError.create('field_missing', null, { key: 'app_uid' });
|
||||
}
|
||||
|
||||
if ( ! req.body.permission ) {
|
||||
throw APIError.create('field_missing', null, {
|
||||
key: 'permission'
|
||||
});
|
||||
}
|
||||
|
||||
await svc_permission.grant_dev_app_permission(
|
||||
actor, req.body.app_uid, req.body.permission,
|
||||
req.body.extra || {}, req.body.meta || {}
|
||||
);
|
||||
|
||||
res.json({});
|
||||
});
|
||||
|
||||
|
||||
62
src/backend/src/routers/auth/revoke-dev-app.js
Normal file
62
src/backend/src/routers/auth/revoke-dev-app.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const eggspress = require("../../api/eggspress");
|
||||
const { UserActorType } = require("../../services/auth/Actor");
|
||||
const { Context } = require("../../util/context");
|
||||
const APIError = require('../../api/APIError');
|
||||
|
||||
module.exports = eggspress('/auth/revoke-dev-app', {
|
||||
subdomain: 'api',
|
||||
auth2: true,
|
||||
allowedMethods: ['POST'],
|
||||
}, async (req, res, next) => {
|
||||
const x = Context.get();
|
||||
const svc_permission = x.get('services').get('permission');
|
||||
|
||||
// Only users can grant user-app permissions
|
||||
const actor = Context.get('actor');
|
||||
if ( ! (actor.type instanceof UserActorType) ) {
|
||||
throw APIError.create('forbidden');
|
||||
}
|
||||
|
||||
if ( req.body.origin ) {
|
||||
const svc_auth = x.get('services').get('auth');
|
||||
req.body.app_uid = await svc_auth.app_uid_from_origin(req.body.origin);
|
||||
}
|
||||
|
||||
if ( ! req.body.app_uid ) {
|
||||
throw APIError.create('field_missing', null, { key: 'app_uid' });
|
||||
}
|
||||
|
||||
if ( req.body.permission === '*' ) {
|
||||
await svc_permission.revoke_dev_app_all(
|
||||
actor, req.body.app_uid, req.body.meta || {},
|
||||
);
|
||||
}
|
||||
|
||||
await svc_permission.revoke_dev_app_permission(
|
||||
actor, req.body.app_uid, req.body.permission,
|
||||
req.body.meta || {},
|
||||
);
|
||||
|
||||
res.json({});
|
||||
});
|
||||
|
||||
|
||||
|
||||
36
src/backend/src/services/HostnameService.js
Normal file
36
src/backend/src/services/HostnameService.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const BaseService = require("./BaseService");
|
||||
|
||||
const os = require('os');
|
||||
|
||||
class HostnameService extends BaseService {
|
||||
_construct () {
|
||||
this.entries = {};
|
||||
}
|
||||
|
||||
_init () {
|
||||
if ( this.global_config.domain ) {
|
||||
this.entries[this.global_config.domain] = {
|
||||
scope: 'web',
|
||||
};
|
||||
this.entries[`api.${this.global_config.domain}`] = {
|
||||
scope: 'api',
|
||||
};
|
||||
}
|
||||
|
||||
const addresses = this.get_broadcast_addresses();
|
||||
|
||||
if ( ! this.global_config.no_nip ) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
get_broadcast_addresses () {
|
||||
const ifaces = os.networkInterfaces();
|
||||
|
||||
for ( const iface_key in ifaces ) {
|
||||
console.log('iface_key', iface_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HostnameService };
|
||||
@@ -49,6 +49,8 @@ class PermissionAPIService extends BaseService {
|
||||
app.use(require('../routers/auth/get-user-app-token'))
|
||||
app.use(require('../routers/auth/grant-user-app'))
|
||||
app.use(require('../routers/auth/revoke-user-app'))
|
||||
app.use(require('../routers/auth/grant-dev-app'))
|
||||
app.use(require('../routers/auth/revoke-dev-app'))
|
||||
app.use(require('../routers/auth/grant-user-user'));
|
||||
app.use(require('../routers/auth/revoke-user-user'));
|
||||
app.use(require('../routers/auth/grant-user-group'));
|
||||
|
||||
@@ -450,6 +450,160 @@ class PermissionService extends BaseService {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Grants an app a permission for any user, as long as the user granting the
|
||||
* permission also has the permission.
|
||||
*
|
||||
* @param {Actor} actor - The actor granting the permission (must be a user).
|
||||
* @param {string} app_uid - The unique identifier or name of the app.
|
||||
* @param {string} username - The username of the user receiving the permission.
|
||||
* @param {string} permission - The permission string to grant.
|
||||
* @param {Object} [extra={}] - Additional metadata or conditions for the permission.
|
||||
* @param {Object} [meta] - Metadata for logging or auditing purposes.
|
||||
* @throws {Error} If the user to grant permission to is not found or if attempting to grant permissions to oneself.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async grant_dev_app_permission (actor, app_uid, permission, extra = {}, meta) {
|
||||
permission = await this._rewrite_permission(permission);
|
||||
|
||||
let app = await get_app({ uid: app_uid });
|
||||
if ( ! app ) app = await get_app({ name: app_uid });
|
||||
|
||||
if ( ! app ) {
|
||||
throw APIError.create('entity_not_found', null, {
|
||||
identifier: 'app:' + app_uid,
|
||||
});
|
||||
}
|
||||
|
||||
const app_id = app.id;
|
||||
|
||||
// UPSERT permission
|
||||
await this.db.write(
|
||||
'INSERT INTO `dev_to_app_permissions` (`user_id`, `app_id`, `permission`, `extra`) ' +
|
||||
'VALUES (?, ?, ?, ?) ' +
|
||||
this.db.case({
|
||||
mysql: 'ON DUPLICATE KEY UPDATE `extra` = ?',
|
||||
otherwise: 'ON CONFLICT(`user_id`, `app_id`, `permission`) DO UPDATE SET `extra` = ?',
|
||||
}),
|
||||
[
|
||||
actor.type.user.id,
|
||||
app_id,
|
||||
permission,
|
||||
JSON.stringify(extra),
|
||||
JSON.stringify(extra),
|
||||
]
|
||||
);
|
||||
|
||||
// INSERT audit table
|
||||
const audit_values = {
|
||||
user_id: actor.type.user.id,
|
||||
user_id_keep: actor.type.user.id,
|
||||
app_id: app_id,
|
||||
app_id_keep: app_id,
|
||||
permission,
|
||||
action: 'grant',
|
||||
reason: meta?.reason || 'granted via PermissionService',
|
||||
};
|
||||
|
||||
const sql_cols = Object.keys(audit_values).map((key) => `\`${key}\``).join(', ');
|
||||
const sql_vals = Object.keys(audit_values).map((key) => `?`).join(', ');
|
||||
|
||||
await this.db.write(
|
||||
'INSERT INTO `audit_dev_to_app_permissions` (' + sql_cols + ') ' +
|
||||
'VALUES (' + sql_vals + ')',
|
||||
Object.values(audit_values)
|
||||
);
|
||||
}
|
||||
async revoke_dev_app_permission (actor, app_uid, permission, meta) {
|
||||
permission = await this._rewrite_permission(permission);
|
||||
|
||||
// For now, actor MUST be a user
|
||||
if ( ! (actor.type instanceof UserActorType) ) {
|
||||
throw new Error('actor must be a user');
|
||||
}
|
||||
|
||||
let app = await get_app({ uid: app_uid });
|
||||
if ( ! app ) app = await get_app({ name: app_uid });
|
||||
if ( ! app ) {
|
||||
throw APIError.create('entity_not_found', null, {
|
||||
identifier: 'app' + app_uid,
|
||||
})
|
||||
}
|
||||
const app_id = app.id;
|
||||
|
||||
// DELETE permission
|
||||
await this.db.write(
|
||||
'DELETE FROM `dev_to_app_permissions` ' +
|
||||
'WHERE `user_id` = ? AND `app_id` = ? AND `permission` = ?',
|
||||
[
|
||||
actor.type.user.id,
|
||||
app_id,
|
||||
permission,
|
||||
]
|
||||
);
|
||||
|
||||
// INSERT audit table
|
||||
const audit_values = {
|
||||
user_id: actor.type.user.id,
|
||||
user_id_keep: actor.type.user.id,
|
||||
app_id: app_id,
|
||||
app_id_keep: app_id,
|
||||
permission,
|
||||
action: 'revoke',
|
||||
reason: meta?.reason || 'revoked via PermissionService',
|
||||
};
|
||||
|
||||
const sql_cols = Object.keys(audit_values).map((key) => `\`${key}\``).join(', ');
|
||||
const sql_vals = Object.keys(audit_values).map((key) => `?`).join(', ');
|
||||
|
||||
await this.db.write(
|
||||
'INSERT INTO `audit_dev_to_app_permissions` (' + sql_cols + ') ' +
|
||||
'VALUES (' + sql_vals + ')',
|
||||
Object.values(audit_values)
|
||||
);
|
||||
}
|
||||
async revoke_dev_app_all (actor, app_uid, meta) {
|
||||
// For now, actor MUST be a user
|
||||
if ( ! (actor.type instanceof UserActorType) ) {
|
||||
throw new Error('actor must be a user');
|
||||
}
|
||||
|
||||
let app = await get_app({ uid: app_uid });
|
||||
if ( ! app ) app = await get_app({ name: app_uid });
|
||||
const app_id = app.id;
|
||||
|
||||
// DELETE permissions
|
||||
await this.db.write(
|
||||
'DELETE FROM `dev_to_app_permissions` ' +
|
||||
'WHERE `user_id` = ? AND `app_id` = ?',
|
||||
[
|
||||
actor.type.user.id,
|
||||
app_id,
|
||||
]
|
||||
);
|
||||
|
||||
// INSERT audit table
|
||||
const audit_values = {
|
||||
user_id: actor.type.user.id,
|
||||
user_id_keep: actor.type.user.id,
|
||||
app_id: app_id,
|
||||
app_id_keep: app_id,
|
||||
permission: '*',
|
||||
action: 'revoke',
|
||||
reason: meta?.reason || 'revoked all via PermissionService',
|
||||
};
|
||||
|
||||
const sql_cols = Object.keys(audit_values).map((key) => `\`${key}\``).join(', ');
|
||||
const sql_vals = Object.keys(audit_values).map((key) => `?`).join(', ');
|
||||
|
||||
await this.db.write(
|
||||
'INSERT INTO `audit_dev_to_app_permissions` (' + sql_cols + ') ' +
|
||||
'VALUES (' + sql_vals + ')',
|
||||
Object.values(audit_values)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Grants a permission to a user for a specific app.
|
||||
*
|
||||
|
||||
@@ -157,7 +157,10 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
]],
|
||||
[32, [
|
||||
'0035_threads.sql',
|
||||
]]
|
||||
]],
|
||||
[33, [
|
||||
'0036_dev-to-app.sql',
|
||||
]],
|
||||
];
|
||||
|
||||
// Database upgrade logic
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
CREATE TABLE `dev_to_app_permissions` (
|
||||
`user_id` int(10) NOT NULL,
|
||||
`app_id` int(10) NOT NULL,
|
||||
`permission` varchar(255) NOT NULL,
|
||||
`extra` JSON DEFAULT NULL,
|
||||
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (`user_id`, `app_id`, `permission`)
|
||||
);
|
||||
|
||||
CREATE TABLE `audit_dev_to_app_permissions` (
|
||||
`id` INTEGER PRIMARY KEY,
|
||||
|
||||
`user_id` int(10) DEFAULT NULL,
|
||||
`user_id_keep` int(10) NOT NULL,
|
||||
|
||||
`app_id` int(10) DEFAULT NULL,
|
||||
`app_id_keep` int(10) NOT NULL,
|
||||
|
||||
`permission` varchar(255) NOT NULL,
|
||||
`extra` JSON DEFAULT NULL,
|
||||
|
||||
`action` VARCHAR(16) DEFAULT NULL, -- "granted" or "revoked"
|
||||
`reason` VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
@@ -401,6 +401,58 @@ const PERMISSION_SCANNERS = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user-app',
|
||||
documentation: `
|
||||
If the actor is an app, this scans for permissions granted to the app
|
||||
because any other user has the permission and granted it to the app
|
||||
for all users of the app.
|
||||
`,
|
||||
async scan (a) {
|
||||
const { reading, actor, permission_options } = a.values();
|
||||
if ( !(actor.type instanceof AppUnderUserActorType) ) {
|
||||
return;
|
||||
}
|
||||
const db = a.iget('db');
|
||||
|
||||
let sql_perm = permission_options.map(() =>
|
||||
`\`permission\` = ?`).join(' OR ');
|
||||
if ( permission_options.length > 1 ) sql_perm = '(' + sql_perm + ')';
|
||||
|
||||
// SELECT permission
|
||||
const rows = await db.read(
|
||||
'SELECT * FROM `dev_to_app_permissions` ' +
|
||||
'WHERE `app_id` = ? AND ' +
|
||||
sql_perm,
|
||||
[
|
||||
actor.type.user.id,
|
||||
actor.type.app.id,
|
||||
...permission_options,
|
||||
]
|
||||
);
|
||||
|
||||
if ( rows[0] ) {
|
||||
const row = rows[0];
|
||||
row.extra = db.case({
|
||||
mysql: () => row.extra,
|
||||
otherwise: () => JSON.parse(row.extra ?? '{}')
|
||||
})();
|
||||
const issuer_user = await get_user({ id: row.user_id });
|
||||
const issuer_actor = Actor.adapt(issuer_user);
|
||||
const issuer_reading = await a.icall('scan', issuer_actor, row.permission);
|
||||
const has_terminal = reading_has_terminal({ reading: issuer_reading });
|
||||
reading.push({
|
||||
$: 'path',
|
||||
via: 'dev-app',
|
||||
permission: row.permission,
|
||||
has_terminal,
|
||||
data: row.extra,
|
||||
issuer_username: actor.type.user.username,
|
||||
reading: issuer_reading,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -38,6 +38,12 @@ export default class Perms {
|
||||
})
|
||||
}
|
||||
|
||||
async grantAppAnyUser (app_uid, permission) {
|
||||
return await this.req_('/auth/grant-dev-app', {
|
||||
app_uid, permission,
|
||||
})
|
||||
}
|
||||
|
||||
async grantOrigin (origin, permission) {
|
||||
return await this.req_('/auth/grant-user-app', {
|
||||
origin, permission,
|
||||
@@ -63,6 +69,12 @@ export default class Perms {
|
||||
})
|
||||
}
|
||||
|
||||
async revokeAppAnyUser (app_uid, permission) {
|
||||
return await this.req_('/auth/revoke-dev-app', {
|
||||
app_uid, permission,
|
||||
})
|
||||
}
|
||||
|
||||
async revokeOrigin (origin, permission) {
|
||||
return await this.req_('/auth/revoke-user-app', {
|
||||
origin, permission,
|
||||
|
||||
Reference in New Issue
Block a user