Skip to content

Commit f2f3d6f

Browse files
committed
feat: add protected apps
1 parent 7006dcc commit f2f3d6f

File tree

5 files changed

+221
-0
lines changed

5 files changed

+221
-0
lines changed

packages/backend/src/CoreModule.js

+11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
const { AdvancedBase } = require("@heyputer/puter-js-common");
2020
const { NotificationES } = require("./om/entitystorage/NotificationES");
21+
const { ProtectedAppES } = require("./om/entitystorage/ProtectedAppES");
2122
const { Context } = require('./util/context');
2223

2324

@@ -51,6 +52,12 @@ const install = async ({ services, app, useapi }) => {
5152

5253
def('puter.middlewares.auth', require('./middleware/auth2'));
5354
});
55+
56+
// === LIBRARIES ===
57+
const ArrayUtil = require('./libraries/ArrayUtil');
58+
services.registerService('util-array', ArrayUtil);
59+
60+
// === SERVICES ===
5461

5562
// /!\ IMPORTANT /!\
5663
// For new services, put the import immediate above the
@@ -153,6 +160,7 @@ const install = async ({ services, app, useapi }) => {
153160
WriteByOwnerOnlyES,
154161
ValidationES,
155162
SetOwnerES,
163+
ProtectedAppES,
156164
MaxLimitES, { max: 5000 },
157165
]),
158166
});
@@ -269,6 +277,9 @@ const install = async ({ services, app, useapi }) => {
269277

270278
const { NotificationService } = require('./services/NotificationService');
271279
services.registerService('notification', NotificationService);
280+
281+
const { ProtectedAppService } = require('./services/ProtectedAppService');
282+
services.registerService('__protected-app', ProtectedAppService);
272283
}
273284

274285
const install_legacy = async ({ services }) => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const BaseService = require("../services/BaseService");
2+
3+
class Library extends BaseService {
4+
//
5+
}
6+
7+
module.exports = Library;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const Library = require("../definitions/Library");
2+
3+
class ArrayUtil extends Library {
4+
/**
5+
*
6+
* @param {*} marked_map
7+
* @param {*} subject
8+
*/
9+
remove_marked_items (marked_map, subject) {
10+
for ( let i=0 ; i < marked_map.length ; i++ ) {
11+
let ii = marked_map[i];
12+
// track: type check
13+
if ( ! Number.isInteger(ii) ) {
14+
throw new Error(
15+
'marked_map can only contain integers'
16+
);
17+
}
18+
// track: bounds check
19+
if ( ii < 0 && ii >= subject.length ) {
20+
throw new Error(
21+
'each item in `marked_map` must be within that bounds ' +
22+
'of `subject`'
23+
);
24+
}
25+
}
26+
27+
marked_map.sort((a, b) => b - a);
28+
29+
for ( let i=0 ; i < marked_map.length ; i++ ) {
30+
let ii = marked_map[i];
31+
subject.splice(ii, 1);
32+
}
33+
34+
return subject;
35+
}
36+
37+
_test ({ assert }) {
38+
// inner indices
39+
{
40+
const subject = [
41+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
42+
// 0 1 2 3 4 5 6 7
43+
const marked_map = [2, 5];
44+
this.remove_marked_items(marked_map, subject);
45+
assert(() => subject.join('') === 'abdegh');
46+
}
47+
// left edge
48+
{
49+
const subject = [
50+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
51+
// 0 1 2 3 4 5 6 7
52+
const marked_map = [0]
53+
this.remove_marked_items(marked_map, subject);
54+
assert(() => subject.join('') === 'bcdefgh');
55+
}
56+
// right edge
57+
{
58+
const subject = [
59+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
60+
// 0 1 2 3 4 5 6 7
61+
const marked_map = [7]
62+
this.remove_marked_items(marked_map, subject);
63+
assert(() => subject.join('') === 'abcdefg');
64+
}
65+
// both edges
66+
{
67+
const subject = [
68+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
69+
// 0 1 2 3 4 5 6 7
70+
const marked_map = [0, 7]
71+
this.remove_marked_items(marked_map, subject);
72+
assert(() => subject.join('') === 'bcdefg');
73+
}
74+
}
75+
}
76+
77+
module.exports = ArrayUtil;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const { AppUnderUserActorType, UserActorType } = require("../../services/auth/Actor");
2+
const { Context } = require("../../util/context");
3+
const { BaseES } = require("./BaseES");
4+
5+
class ProtectedAppES extends BaseES {
6+
async select (options){
7+
const results = await this.upstream.select(options);
8+
9+
const actor = Context.get('actor');
10+
const services = Context.get('services');
11+
12+
const to_delete = [];
13+
for ( let i=0 ; i < results.length ; i++ ) {
14+
const entity = results[i];
15+
16+
if ( ! await this.check_({ actor, services }, entity) ) {
17+
continue;
18+
}
19+
20+
to_delete.push(i);
21+
}
22+
23+
const svc_utilArray = services.get('util-array');
24+
svc_utilArray.remove_marked_items(to_delete, results);
25+
26+
return results;
27+
}
28+
29+
async read (uid){
30+
const entity = await this.upstream.read(uid);
31+
if ( ! entity ) return null;
32+
33+
const actor = Context.get('actor');
34+
const services = Context.get('services');
35+
36+
if ( await this.check_({ actor, services }, entity) ) {
37+
return null;
38+
}
39+
40+
return entity;
41+
}
42+
43+
/**
44+
* returns true if the entity should not be sent downstream
45+
*/
46+
async check_ ({ actor, services }, entity) {
47+
// track: ruleset
48+
{
49+
// if it's not a protected app, no worries
50+
if ( ! await entity.get('protected') ) return;
51+
52+
// if actor is this app, no worries
53+
if (
54+
actor.type instanceof AppUnderUserActorType &&
55+
await entity.get('uid') === actor.type.app.uid
56+
) return;
57+
58+
// if actor is owner of this app, no worries
59+
if (
60+
actor.type instanceof UserActorType &&
61+
(await entity.get('owner')).id === actor.type.user.id
62+
) return;
63+
}
64+
65+
// now we need to check for permission
66+
const app_uid = await entity.get('uid');
67+
const svc_permission = services.get('permission');
68+
const permission_to_check = `app:uid#${app_uid}:access`;
69+
const perm = await svc_permission.check(
70+
actor, permission_to_check,
71+
);
72+
73+
if ( perm ) return;
74+
75+
// `true` here means "do not send downstream"
76+
return true;
77+
}
78+
};
79+
80+
module.exports = {
81+
ProtectedAppES,
82+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const { get_app } = require("../helpers");
2+
const { UserActorType } = require("./auth/Actor");
3+
const { PermissionImplicator, PermissionUtil } = require("./auth/PermissionService");
4+
const BaseService = require("./BaseService");
5+
6+
class ProtectedAppService extends BaseService {
7+
async _init () {
8+
const svc_permission = this.services.get('permission');
9+
10+
// track: object description in comment
11+
// Owner of procted app has implicit permission to access it
12+
svc_permission.register_implicator(PermissionImplicator.create({
13+
matcher: permission => {
14+
return permission.startsWith('app:');
15+
},
16+
checker: async ({ actor, permission }) => {
17+
if ( !(actor.type instanceof UserActorType) ) {
18+
return undefined;
19+
}
20+
21+
const parts = PermissionUtil.split(permission);
22+
if ( parts.length !== 3 ) return undefined;
23+
24+
const [_, uid_part, lvl] = parts;
25+
if ( lvl !== 'access' ) return undefined;
26+
27+
// track: slice a prefix
28+
const uid = uid_part.slice('uid#'.length);
29+
30+
const app = await get_app({ uid });
31+
32+
if ( app.owner_user_id !== actor.type.user.id ) {
33+
return undefined;
34+
}
35+
36+
return {};
37+
},
38+
}));
39+
}
40+
}
41+
42+
module.exports = {
43+
ProtectedAppService,
44+
};

0 commit comments

Comments
 (0)