diff --git a/src/backend/src/CoreModule.js b/src/backend/src/CoreModule.js index f78452430..13558f2ca 100644 --- a/src/backend/src/CoreModule.js +++ b/src/backend/src/CoreModule.js @@ -317,6 +317,9 @@ const install = async ({ services, app, useapi }) => { const { FeatureFlagService } = require('./services/FeatureFlagService'); services.registerService('feature-flag', FeatureFlagService); + + const { KernelInfoService } = require('./services/KernelInfoService'); + services.registerService('kernel-info', KernelInfoService); } const install_legacy = async ({ services }) => { diff --git a/src/backend/src/Kernel.js b/src/backend/src/Kernel.js index 60f0be7bc..01062fdfa 100644 --- a/src/backend/src/Kernel.js +++ b/src/backend/src/Kernel.js @@ -136,6 +136,7 @@ class Kernel extends AdvancedBase { // Internal modules for ( const module of this.modules ) { + services.registerModule(module.constructor.name, module); await module.install(Context.get()); } diff --git a/src/backend/src/data/hardcoded-permissions.js b/src/backend/src/data/hardcoded-permissions.js index 65cce827c..abaa2940b 100644 --- a/src/backend/src/data/hardcoded-permissions.js +++ b/src/backend/src/data/hardcoded-permissions.js @@ -78,6 +78,7 @@ const hardcoded_user_group_permissions = { 'driver': {}, 'service': {}, 'feature': {}, + 'kernel-info': {}, }, 'b7220104-7905-4985-b996-649fdcdb3c8f': { 'service:hello-world:ii:hello-world': policy_perm('temp.es'), diff --git a/src/backend/src/services/Container.js b/src/backend/src/services/Container.js index d9ba8b1ec..724152326 100644 --- a/src/backend/src/services/Container.js +++ b/src/backend/src/services/Container.js @@ -29,7 +29,33 @@ class Container { this.instances_ = {}; this.implementors_ = {}; this.ready = new TeePromise(); + + this.modname_ = null; + this.modules_ = {} } + + registerModule (name, module) { + this.modules_[name] = { + services_l: [], + services_m: {}, + module + }; + this.setModuleName(name); + } + + /** + * Sets the name of the current module registering services. + * + * Note: this is an antipattern; it would be a bit better to + * provide the module name while registering a service, but + * this requires making an implementor of Container's interface + * with this as a hidden variable so as not to break existing + * modules. + */ + setModuleName (name) { + this.modname_ = name; + } + /** * registerService registers a service with the servuces container. * @@ -44,6 +70,12 @@ class Container { : new cls({ services: this, config, my_config, name, args }) ; this.instances_[name] = instance; + if ( this.modname_ ) { + const mod_entry = this.modules_[this.modname_]; + mod_entry.services_l.push(name); + mod_entry.services_m[name] = true; + } + if ( !(instance instanceof AdvancedBase) ) return; const traits = instance.list_traits(); diff --git a/src/backend/src/services/KernelInfoService.js b/src/backend/src/services/KernelInfoService.js new file mode 100644 index 000000000..e8807f217 --- /dev/null +++ b/src/backend/src/services/KernelInfoService.js @@ -0,0 +1,117 @@ +const configurable_auth = require("../middleware/configurable_auth"); +const { Context } = require("../util/context"); +const { Endpoint } = require("../util/expressutil"); +const BaseService = require("./BaseService"); +const { Interface } = require("./drivers/meta/Construct"); + +const PERM_SEE_ALL = 'kernel-info:see-all-services'; +const PERM_SEE_DRIVERS = 'kernel-info:see-all-drivers'; + +class KernelInfoService extends BaseService { + async _init () { + // + } + + ['__on_install.routes'] (_, { app }) { + const router = (() => { + const require = this.require; + const express = require('express'); + return express.Router(); + })(); + + app.use('/', router); + + Endpoint({ + route: '/lsmod', + methods: ['GET', 'POST'], + mw: [ + configurable_auth(), + ], + handler: async (req, res) => { + const svc_permission = this.services.get('permission'); + + const actor = Context.get('actor'); + const can_see_all = actor && + await svc_permission.check(actor, PERM_SEE_ALL); + const can_see_drivers = actor && + await svc_permission.check(actor, PERM_SEE_DRIVERS); + + const interfaces = {}; + const svc_registry = this.services.get('registry'); + const col_interfaces = svc_registry.get('interfaces'); + for ( const interface_name of col_interfaces.keys() ) { + const iface = col_interfaces.get(interface_name); + console.log('-->', interface_name, iface); + if ( iface === undefined ) continue; + if ( iface.no_sdk ) continue; + interfaces[interface_name] = { + spec: (new Interface( + iface, + { name: interface_name } + )).serialize(), + implementors: {} + } + } + + const services = []; + const modules = []; + for ( const k in this.services.modules_ ) { + const module_info = { + name: k, + services: [] + }; + + modules.push(module_info); + + for ( const s_k of this.services.modules_[k].services_l ) { + const service_info = { + name: s_k, + traits: [] + }; + services.push(service_info); + + const service = this.services.get(s_k); + if ( service.list_traits ) { + const traits = service.list_traits(); + for ( const trait of traits ) { + const corresponding_iface = interfaces[trait]; + if ( ! corresponding_iface ) continue; + corresponding_iface.implementors[s_k] = {}; + } + service_info.traits = service.list_traits(); + } + } + } + + // If actor doesn't have permission to see all drivers, + // (granted by either "can_see_all" or "can_see_drivers") + if ( ! can_see_all && ! can_see_drivers ) { + // only show interfaces with at least one implementation + // that the actor has permission to use + for ( const iface_name in interfaces ) { + for ( const impl_name in interfaces[iface_name].implementors ) { + const perm = `service:${impl_name}:ii:${iface_name}`; + const can_see_this = actor && + await svc_permission.check(actor, perm); + if ( ! can_see_this ) { + delete interfaces[iface_name].implementors[impl_name]; + } + } + if ( Object.keys(interfaces[iface_name].implementors).length < 1 ) { + delete interfaces[iface_name]; + } + } + } + + res.json({ + interfaces, + ...(can_see_all ? { services } : {}) + }); + } + }).attach(router); + } +} + +module.exports = { + KernelInfoService, +}; diff --git a/src/backend/src/services/RegistryService.js b/src/backend/src/services/RegistryService.js index 789e5ecb4..6b93e4abb 100644 --- a/src/backend/src/services/RegistryService.js +++ b/src/backend/src/services/RegistryService.js @@ -49,7 +49,8 @@ class MapCollection extends AdvancedBase { } keys () { - return this.kv.keys(`registry:map:${this.map_id}:*`); + const keys = this.kv.keys(`registry:map:${this.map_id}:*`); + return keys.map(k => k.slice(`registry:map:${this.map_id}:`.length)); } _mk_key (key) { diff --git a/src/backend/src/services/auth/PermissionService.js b/src/backend/src/services/auth/PermissionService.js index bb6025859..38e5b88c8 100644 --- a/src/backend/src/services/auth/PermissionService.js +++ b/src/backend/src/services/auth/PermissionService.js @@ -214,6 +214,14 @@ class PermissionService extends BaseService { } return permission; } + + async check (actor, permission_options) { + // TODO: optimized implementation for check instead of + // delegating to the scan() method + const reading = await this.scan(actor, permission_options); + const options = PermissionUtil.reading_to_options(reading); + return options.length > 0; + } async scan (actor, permission_options) { const reading = [];