Skip to content

Commit a879c83

Browse files
authored
Merge branch 'main' into HeyPuter#875
2 parents fb34680 + 63c5aa2 commit a879c83

File tree

14 files changed

+509
-512
lines changed

14 files changed

+509
-512
lines changed

src/backend/src/services/auth/PermissionService.js

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,16 @@ const implicit_user_permissions = {
3737

3838

3939
/**
40-
* The PermissionService class manages the core functionality for handling permissions within the Puter ecosystem.
41-
* It provides methods for granting, revoking, and checking permissions for various entities such as users and applications.
42-
* This service interacts with the database to store, retrieve, and audit permission changes, and also handles complex permission logic like rewriting, implication, and explosion of permissions.
43-
*/
40+
* Permission rewriters are used to map one set of permission strings to another.
41+
* These are invoked during permission scanning and when permissions are granted or revoked.
42+
*
43+
* For example, Puter's filesystem uses this to map 'fs:/some/path:mode' to
44+
* 'fs:SOME-UUID:mode'.
45+
*
46+
* A rewriter is constructed using the static method PermissionRewriter.create({ matcher, rewriter }).
47+
* The matcher is a function that takes a permission string and returns true if the rewriter should be applied.
48+
* The rewriter is a function that takes a permission string and returns the rewritten permission string.
49+
*/
4450
class PermissionRewriter {
4551
static create ({ id, matcher, rewriter }) {
4652
return new PermissionRewriter({ id, matcher, rewriter });
@@ -70,11 +76,17 @@ class PermissionRewriter {
7076

7177

7278
/**
73-
* The PermissionImplicator class is used to manage implicit permissions.
74-
* It defines methods to match and check if a given permission is implicitly granted to an actor.
75-
* @class
76-
* @name PermissionImplicator
77-
*/
79+
* Permission implicators are used to manage implicit permissions.
80+
* It defines a method to check if a given permission is implicitly granted to an actor.
81+
*
82+
* For example, Puter's filesystem uses this to grant permission to a file if the specified
83+
* 'actor' is the owner of the file.
84+
*
85+
* An implicator is constructed using the static method PermissionImplicator.create({ matcher, checker }).
86+
* `matcher is a function that takes a permission string and returns true if the implicator should be applied.
87+
* `checker` is a function that takes an actor and a permission string and returns true if the permission is implied.
88+
* The actor and permission are passed to checker({ actor, permission }) as an object.
89+
*/
7890
class PermissionImplicator {
7991
static create ({ id, matcher, checker }) {
8092
return new PermissionImplicator({ id, matcher, checker });
@@ -108,12 +120,17 @@ class PermissionImplicator {
108120

109121

110122
/**
111-
* The PermissionExploder class is responsible for expanding permissions into a set of more granular permissions.
112-
* It uses a matcher function to determine if a permission should be exploded and an exploder function to perform the expansion.
113-
* This class is part of the permission management system, allowing for dynamic and complex permission structures.
114-
*
115-
* @class PermissionExploder
116-
*/
123+
* Permission exploders are used to map any permission to a list of permissions
124+
* which are considered to imply the specified permission.
125+
*
126+
* It uses a matcher function to determine if a permission should be exploded
127+
* and an exploder function to perform the expansion.
128+
*
129+
* The exploder is constructed using the static method PermissionExploder.create({ matcher, explode }).
130+
* The `matcher` is a function that takes a permission string and returns true if the exploder should be applied.
131+
* The `explode` is a function that takes an actor and a permission string and returns a list of implied permissions.
132+
* The actor and permission are passed to explode({ actor, permission }) as an object.
133+
*/
117134
class PermissionExploder {
118135
static create ({ id, matcher, exploder }) {
119136
return new PermissionExploder({ id, matcher, exploder });
@@ -952,14 +969,26 @@ class PermissionService extends BaseService {
952969
}
953970

954971

955-
register_rewriter (translator) {
956-
if ( ! (translator instanceof PermissionRewriter) ) {
957-
throw new Error('translator must be a PermissionRewriter');
972+
/**
973+
* Register a permission rewriter. For details see the documentation on the
974+
* PermissionRewriter class.
975+
*
976+
* @param {PermissionRewriter} rewriter - The permission rewriter to register
977+
*/
978+
register_rewriter (rewriter) {
979+
if ( ! (rewriter instanceof PermissionRewriter) ) {
980+
throw new Error('rewriter must be a PermissionRewriter');
958981
}
959982

960-
this._permission_rewriters.push(translator);
983+
this._permission_rewriters.push(rewriter);
961984
}
962985

986+
/**
987+
* Register a permission implicator. For details see the documentation on the
988+
* PermissionImplicator class.
989+
*
990+
* @param {PermissionImplicator} implicator - The permission implicator to register
991+
*/
963992
register_implicator (implicator) {
964993
if ( ! (implicator instanceof PermissionImplicator) ) {
965994
throw new Error('implicator must be a PermissionImplicator');
@@ -968,6 +997,12 @@ class PermissionService extends BaseService {
968997
this._permission_implicators.push(implicator);
969998
}
970999

1000+
/**
1001+
* Register a permission exploder. For details see the documentation on the
1002+
* PermissionExploder class.
1003+
*
1004+
* @param {PermissionExploder} exploder - The permission exploder to register
1005+
*/
9711006
register_exploder (exploder) {
9721007
if ( ! (exploder instanceof PermissionExploder) ) {
9731008
throw new Error('exploder must be a PermissionExploder');

src/backend/src/services/auth/VirtualGroupService.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ class VirtualGroupService extends BaseService {
2121
this.membership_implicators_ = [];
2222
}
2323

24+
/**
25+
* Registers a function that reports one or more groups that an actor
26+
* should be considered a member of.
27+
*
28+
* @note this only applies to virtual groups, not persistent groups.
29+
*
30+
* @param {*} implicator
31+
*/
2432
register_membership_implicator (implicator) {
2533
this.membership_implicators_.push(implicator);
2634
}

src/backend/src/unstructured/permission-scan-lib.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@
1010
const remove_paths_through_user = ({ reading, user }) => {
1111
const no_cycle_reading = [];
1212

13-
for ( let i = 0 ; i < reading.length ; i++ ) {
14-
const node = reading[i];
15-
16-
console.log('checking node...', node);
17-
13+
for ( const node of reading ) {
1814
if ( node.$ === 'path' ) {
1915
if (
2016
node.issuer_username === user.username
@@ -33,14 +29,11 @@ const remove_paths_through_user = ({ reading, user }) => {
3329
no_cycle_reading.push(node);
3430
}
3531

36-
console.log('\x1B[36;1m ====', reading.length - no_cycle_reading.length, 'nodes filtered out ====\x1B[0m');
37-
3832
return no_cycle_reading;
3933
};
4034

4135
const reading_has_terminal = ({ reading }) => {
42-
for ( let i = 0 ; i < reading.length ; i++ ) {
43-
const node = reading[i];
36+
for ( const node of reading ) {
4437
if ( node.has_terminal ) {
4538
return true;
4639
}

src/backend/src/unstructured/permission-scanners.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,26 @@ const { reading_has_terminal } = require("./permission-scan-lib");
1414
"Ctrl+K, Ctrl+J" or "⌘K, ⌘J";
1515
*/
1616

17+
/**
18+
* Permission Scanners
19+
* @usedBy scan-permission.js
20+
*
21+
* These are all the different ways an entity (user or app) can have a permission.
22+
* This list of scanners is iterated over and invoked by scan-permission.js.
23+
*
24+
* Each `scan` function is passed a sequence scope. The instance attached to the
25+
* sequence scope is PermissionService itself, so any `a.iget('something')` is
26+
* accessing the member 'something' of the PermissionService instance.
27+
*/
1728
const PERMISSION_SCANNERS = [
1829
{
1930
name: 'implied',
31+
documentation: `
32+
Scans for permissions that are implied by "permission implicators".
33+
34+
Permission implicators are added by other services via
35+
PermissionService's \`register_implicator\` method.
36+
`,
2037
async scan (a) {
2138
const reading = a.get('reading');
2239
const { actor, permission_options } = a.values();
@@ -46,6 +63,9 @@ const PERMISSION_SCANNERS = [
4663
},
4764
{
4865
name: 'user-user',
66+
documentation: `
67+
User-to-User permissions are permission granted form one user to another.
68+
`,
4969
async scan (a) {
5070
const { reading, actor, permission_options, state } = a.values();
5171
if ( !(actor.type instanceof UserActorType) ) {
@@ -96,7 +116,6 @@ const PERMISSION_SCANNERS = [
96116

97117
if ( should_continue ) continue;
98118

99-
// const issuer_perm = await this.check(issuer_actor, row.permission);
100119
const issuer_reading = await a.icall(
101120
'scan', issuer_actor, row.permission, undefined, state);
102121

@@ -117,6 +136,13 @@ const PERMISSION_SCANNERS = [
117136
},
118137
{
119138
name: 'hc-user-group-user',
139+
documentation: `
140+
These are user-to-group permissions that are defined in the
141+
hardcoded_user_group_permissions section of "hardcoded-permissions.js".
142+
143+
These are typically used to grant permissions from the system user to
144+
the default groups: "admin", "user", and "temp".
145+
`,
120146
async scan (a) {
121147
const { reading, actor, permission_options } = a.values();
122148
if ( !(actor.type instanceof UserActorType) ) {
@@ -167,6 +193,11 @@ const PERMISSION_SCANNERS = [
167193
},
168194
{
169195
name: 'user-group-user',
196+
documentation: `
197+
This scans for permissions that are granted to the user because a
198+
group they are a member of was granted this permission by another
199+
user.
200+
`,
170201
async scan (a) {
171202
const { reading, actor, permission_options } = a.values();
172203
if ( !(actor.type instanceof UserActorType) ) {
@@ -223,6 +254,16 @@ const PERMISSION_SCANNERS = [
223254
},
224255
{
225256
name: 'user-virtual-group-user',
257+
documentation: `
258+
These are groups with computed membership. Permissions are not granted
259+
to these groups; instead the groups are defined with a list of
260+
permissions that are granted to the group members.
261+
262+
Services can define "virtual groups" via the "virtual-group" service.
263+
Services can also register membership implicators for virtual groups
264+
which will compute on the fly whether or not an actor should be
265+
considered a member of the group.
266+
`,
226267
async scan (a) {
227268
const svc_virtualGroup = await a.iget('services').get('virtual-group');
228269
const { reading, actor, permission_options } = a.values();
@@ -248,6 +289,10 @@ const PERMISSION_SCANNERS = [
248289
},
249290
{
250291
name: 'user-app',
292+
documentation: `
293+
If the actor is an app, this scans for permissions granted to the app
294+
because the user has the permission and granted it to the app.
295+
`,
251296
async scan (a) {
252297
const { reading, actor, permission_options } = a.values();
253298
if ( !(actor.type instanceof AppUnderUserActorType) ) {

src/backend/src/util/context.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ class Context {
5252
);
5353
}
5454

55-
// x = globalThis.root_context ?? this.create({});
5655
x = this.root.sub({}, this.USE_NAME_FALLBACK);
5756
}
5857
if ( x && k ) return x.get(k);
@@ -180,7 +179,6 @@ class Context {
180179
});
181180
}
182181
abind (cb) {
183-
const als = this.constructor.contextAsyncLocalStorage;
184182
return async (...args) => {
185183
return await this.arun(async () => {
186184
return await cb(...args);

src/backend/src/util/fuzz.js

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,6 @@
1616
* You should have received a copy of the GNU Affero General Public License
1717
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1818
*/
19-
// function fuzz_number(n) {
20-
// if (n === 0) return 0;
21-
22-
// // let randomized = n + (Math.random() - 0.5) * n * 0.2;
23-
// let randomized = n;
24-
// let magnitude = Math.floor(Math.log10(randomized));
25-
// let factor = Math.pow(10, magnitude);
26-
// return Math.round(randomized / factor) * factor;
27-
// }
2819

2920
function fuzz_number(num) {
3021
// If the number is 0, then return 0
@@ -46,47 +37,6 @@ function fuzz_number(num) {
4637
return Math.round(num / factor) * factor;
4738
}
4839

49-
// function fuzz_number(number) {
50-
// if (isNaN(number)) {
51-
// return 'Invalid number';
52-
// }
53-
54-
// let formattedNumber;
55-
// if (number >= 1000000) {
56-
// // For millions, we want to show one decimal place
57-
// formattedNumber = (number / 1000000).toFixed(0) + 'm';
58-
// } else if (number >= 1000) {
59-
// // For thousands, we want to show one decimal place
60-
// formattedNumber = (number / 1000).toFixed(0) + 'k';
61-
// } else if (number >= 500) {
62-
// // For hundreds, we want to show no decimal places
63-
// formattedNumber = '500+';
64-
// } else if (number >= 100) {
65-
// // For hundreds, we want to show no decimal places
66-
// formattedNumber = '100+';
67-
// } else if (number >= 50) {
68-
// // For hundreds, we want to show no decimal places
69-
// formattedNumber = '50+';
70-
// } else if (number >= 10) {
71-
// // For hundreds, we want to show no decimal places
72-
// formattedNumber = '10+';
73-
// }
74-
// else {
75-
// // For numbers less than 10, we show the number as is.
76-
// formattedNumber = '1+';
77-
// }
78-
79-
// // If the decimal place is 0 (e.g., 5.0k), we remove the decimal part (to have 5k instead)
80-
// formattedNumber = formattedNumber.replace(/\.0(?=[k|m])/, '');
81-
82-
// // Append the plus sign for numbers 1000 and greater, denoting the number is 'this value or more'.
83-
// if (number >= 1000) {
84-
// formattedNumber += '+';
85-
// }
86-
87-
// return formattedNumber;
88-
// }
89-
9040
module.exports = {
9141
fuzz_number
9242
};

src/backend/src/util/lockutil.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
*/
1919
const { TeePromise } = require('@heyputer/putility').libs.promise;
2020

21+
/**
22+
* RWLock is a read-write lock that allows multiple readers or a single writer.
23+
*/
2124
class RWLock {
2225
static TYPE_READ = Symbol('read');
2326
static TYPE_WRITE = Symbol('write');
@@ -45,11 +48,6 @@ class RWLock {
4548
this.check_queue_();
4649
}
4750
check_queue_ () {
48-
// console.log('check_queue_', {
49-
// readers_: this.readers_,
50-
// writer_: this.writer_,
51-
// queue: this.queue.map(item => item.type),
52-
// });
5351
if ( this.queue.length === 0 ) {
5452
if ( this.readers_ === 0 && ! this.writer_ ) {
5553
this.on_empty_();

src/backend/src/util/otelutil.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ class ParallelTasks {
100100
return;
101101
}
102102

103-
// const span = this.tracer.startSpan(name);
104103
this.promises.push(this.run_(name, fn));
105104
}
106105

src/backend/src/util/queuing.js

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/backend/src/util/retryutil.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const simple_retry = async function simple_retry (func, max_tries, interval) {
5252
};
5353

5454
const poll = async function poll({ poll_fn, schedule_fn }) {
55-
let delay = undefined;
55+
let delay;
5656

5757
while ( true ) {
5858
const is_done = await poll_fn();

0 commit comments

Comments
 (0)