Skip to content

Commit

Permalink
dev: add endpoint for GUI to query app file access
Browse files Browse the repository at this point in the history
  • Loading branch information
KernelDeimos committed Oct 15, 2024
1 parent fd7bd33 commit 1ea6d27
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 0 deletions.
78 changes: 78 additions & 0 deletions src/backend/src/routers/auth/check-app-acl.endpoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const APIError = require("../../api/APIError");
const FSNodeParam = require("../../api/filesystem/FSNodeParam");
const StringParam = require("../../api/filesystem/StringParam");
const { get_app } = require("../../helpers");
const configurable_auth = require("../../middleware/configurable_auth");
const { Eq, Or } = require("../../om/query/query");
const { UserActorType, Actor, AppUnderUserActorType } = require("../../services/auth/Actor");
const { Context } = require("../../util/context");

module.exports = {
route: '/check-app-acl',
methods: ['POST'],

// TODO: "alias" should be part of parameters somehow
alias: {
uid: 'subject',
path: 'subject',
},
parameters: {
subject: new FSNodeParam('subject'),
mode: new StringParam('mode', { optional: true }),

// TODO: There should be an "AppParam", but it feels wrong to include
// so many concerns into `src/api/filesystem` like that. This needs to
// be de-coupled somehow first.
app: new StringParam('app'),
},
mw: [configurable_auth()],
handler: async (req, res) => {
const context = Context.get();
const actor = req.actor;

if ( ! (actor.type instanceof UserActorType) ) {
throw APIError.create('forbidden');
}

const subject = req.values.subject;

const svc_acl = context.get('services').get('acl');
if ( ! await svc_acl.check(actor, subject, 'see') ) {
throw APIError.create('subject_does_not_exist');
}

const es_app = context.get('services').get('es:app');
const app = await es_app.read({
predicate: new Or({
children: [
new Eq({ key: 'uid', value: req.values.app }),
new Eq({ key: 'name', value: req.values.app }),
]
}),
});
if ( ! app ) {
throw APIError.create('app_does_not_exist', null, {
identifier: req.values.app,
});
}

const app_actor = new Actor({
type: new AppUnderUserActorType({
user: actor.type.user,
// TODO: get legacy app object from entity instead of fetching again
app: await get_app({ uid: await app.get('uid') }),
})
});

console.log('app?', app);

res.json({
allowed: await svc_acl.check(
app_actor, subject,
// If mode is not specified, check the HIGHEST mode, because this
// will grant the LEAST cases
req.values.mode ?? svc_acl.get_highest_mode()
)
});
}
};
6 changes: 6 additions & 0 deletions src/backend/src/services/PermissionAPIService.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ class PermissionAPIService extends BaseService {
app.use(require('../routers/auth/grant-user-group'));
app.use(require('../routers/auth/revoke-user-group'));
app.use(require('../routers/auth/list-permissions'))

Endpoint(
require('../routers/auth/check-app-acl.endpoint.js'),
).but({
route: '/auth/check-app-acl',
}).attach(app);

// track: scoping iife
const r_group = (() => {
Expand Down
6 changes: 6 additions & 0 deletions src/backend/src/services/auth/ACLService.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ class ACLService extends BaseService {
return APIError.create('forbidden');
}

// If any logic depends on knowledge of the highest ACL mode, it should use
// this method in case a higher mode is added (ex: might add 'config' mode)
get_highest_mode () {
return 'write';
}

// TODO: DRY: Also in FilesystemService
_higher_modes (mode) {
// If you want to X, you can do so with any of [...Y]
Expand Down
10 changes: 10 additions & 0 deletions src/backend/src/util/expressutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const Endpoint = function Endpoint (spec) {
attach (route) {
const eggspress_options = {
allowedMethods: spec.methods ?? ['GET'],
...(spec.subdomain ? { subdomain: spec.subdomain } : {}),
...(spec.parameters ? { parameters: spec.parameters } : {}),
...(spec.alias ? { alias: spec.alias } : {}),
...(spec.mw ? { mw: spec.mw } : {}),
};
const eggspress_router = eggspress(
Expand All @@ -31,6 +34,13 @@ const Endpoint = function Endpoint (spec) {
spec.handler,
);
route.use(eggspress_router);
},
but (newSpec) {
// TODO: add merge with '$' behaviors (like config has)
return Endpoint({
...spec,
...newSpec,
});
}
};
}
Expand Down
5 changes: 5 additions & 0 deletions src/contextlink/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
* 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/>.
*/

// TODO: move dependants of this Context to use the one in putility instead

// DO NOT EDIT THIS FILE: update the one in putility instead and migrate.

export class Context {
constructor (values) {
for ( const k in values ) this[k] = values[k];
Expand Down

0 comments on commit 1ea6d27

Please sign in to comment.