1+ /**
2+ * The PermissionUtil class provides utility methods for handling
3+ * permission strings and operations, including splitting, joining,
4+ * escaping, and unescaping permission components. It also includes
5+ * functionality to convert permission reading structures into options.
6+ */
7+ export class PermissionUtil {
8+ /**
9+ * Unescapes a permission component string, converting escape sequences to their literal characters.
10+ * @param {string } component - The escaped permission component string.
11+ * @returns {string } The unescaped permission component.
12+ */
13+ static unescape_permission_component ( component ) {
14+ let unescaped_str = '' ;
15+ // Constant for unescaped permission component string
16+ const STATE_NORMAL = { } ;
17+ // Constant for escaping special characters in permission strings
18+ const STATE_ESCAPE = { } ;
19+ let state = STATE_NORMAL ;
20+ const const_escapes = { C : ':' } ;
21+ for ( let i = 0 ; i < component . length ; i ++ ) {
22+ const c = component [ i ] ;
23+ if ( state === STATE_NORMAL ) {
24+ if ( c === '\\' ) {
25+ state = STATE_ESCAPE ;
26+ } else {
27+ unescaped_str += c ;
28+ }
29+ } else if ( state === STATE_ESCAPE ) {
30+ unescaped_str += Object . prototype . hasOwnProperty . call ( const_escapes , c )
31+ ? const_escapes [ c ] : c ;
32+ state = STATE_NORMAL ;
33+ }
34+ }
35+ return unescaped_str ;
36+ }
37+
38+ /**
39+ * Escapes special characters in a permission component string for safe joining.
40+ * @param {string } component - The permission component string to escape.
41+ * @returns {string } The escaped permission component.
42+ */
43+ static escape_permission_component ( component ) {
44+ let escaped_str = '' ;
45+ for ( let i = 0 ; i < component . length ; i ++ ) {
46+ const c = component [ i ] ;
47+ if ( c === ':' ) {
48+ escaped_str += '\\C' ;
49+ continue ;
50+ }
51+ escaped_str += c ;
52+ }
53+ return escaped_str ;
54+ }
55+
56+ /**
57+ * Splits a permission string into its component parts, unescaping each component.
58+ * @param {string } permission - The permission string to split.
59+ * @returns {string[] } Array of unescaped permission components.
60+ */
61+ static split ( permission ) {
62+ return permission
63+ . split ( ':' )
64+ . map ( PermissionUtil . unescape_permission_component )
65+ ;
66+ }
67+
68+ /**
69+ * Joins permission components into a single permission string, escaping as needed.
70+ * @param {...string } components - The permission components to join.
71+ * @returns {string } The escaped, joined permission string.
72+ */
73+ static join ( ...components ) {
74+ return components
75+ . map ( PermissionUtil . escape_permission_component )
76+ . join ( ':' )
77+ ;
78+ }
79+
80+ /**
81+ * Converts a permission reading structure into an array of option objects.
82+ * Recursively traverses the reading tree to collect all options with their associated path and data.
83+ * @param {Array<Object> } reading - The permission reading structure to convert.
84+ * @param {Object } [parameters={}] - Optional parameters for the conversion.
85+ * @param {Array<Object> } [options=[]] - Accumulator for options (used internally for recursion).
86+ * @param {Array<any> } [extras=[]] - Extra data to include (used internally for recursion).
87+ * @param {Array<Object> } [path=[]] - Current path in the reading tree (used internally for recursion).
88+ * @returns {Array<Object> } Array of option objects with path and data.
89+ */
90+ static reading_to_options (
91+ // actual arguments
92+ reading , parameters = { } ,
93+ // recursion state
94+ options = [ ] , extras = [ ] , path = [ ] ,
95+ ) {
96+ const to_path_item = finding => ( {
97+ key : finding . key ,
98+ holder : finding . holder_username ,
99+ data : finding . data ,
100+ } ) ;
101+ for ( let finding of reading ) {
102+ if ( finding . $ === 'option' ) {
103+ path = [ to_path_item ( finding ) , ...path ] ;
104+ options . push ( {
105+ ...finding ,
106+ data : [
107+ ...( finding . data ? [ finding . data ] : [ ] ) ,
108+ ...extras ,
109+ ] ,
110+ path,
111+ } ) ;
112+ }
113+ if ( finding . $ === 'path' ) {
114+ if ( finding . has_terminal === false ) continue ;
115+ const new_extras = ( finding . data ) ? [
116+ finding . data ,
117+ ...extras ,
118+ ] : [ ] ;
119+ const new_path = [ to_path_item ( finding ) , ...path ] ;
120+ this . reading_to_options ( finding . reading , parameters , options , new_extras , new_path ) ;
121+ }
122+ }
123+ return options ;
124+ }
125+ }
126+
127+ /**
128+ * Permission rewriters are used to map one set of permission strings to another.
129+ * These are invoked during permission scanning and when permissions are granted or revoked.
130+ *
131+ * For example, Puter's filesystem uses this to map 'fs:/some/path:mode' to
132+ * 'fs:SOME-UUID:mode'.
133+ *
134+ * A rewriter is constructed using the static method PermissionRewriter.create({ matcher, rewriter }).
135+ * The matcher is a function that takes a permission string and returns true if the rewriter should be applied.
136+ * The rewriter is a function that takes a permission string and returns the rewritten permission string.
137+ */
138+ export class PermissionRewriter {
139+ static create ( { id, matcher, rewriter } ) {
140+ return new PermissionRewriter ( { id, matcher, rewriter } ) ;
141+ }
142+
143+ constructor ( { id, matcher, rewriter } ) {
144+ this . id = id ;
145+ this . matcher = matcher ;
146+ this . rewriter = rewriter ;
147+ }
148+
149+ matches ( permission ) {
150+ return this . matcher ( permission ) ;
151+ }
152+
153+ /**
154+ * Determines if the given permission matches the criteria set for this rewriter.
155+ *
156+ * @param {string } permission - The permission string to check.
157+ * @returns {boolean } - True if the permission matches, false otherwise.
158+ */
159+ async rewrite ( permission ) {
160+ return await this . rewriter ( permission ) ;
161+ }
162+ }
163+
164+ /**
165+ * Permission implicators are used to manage implicit permissions.
166+ * It defines a method to check if a given permission is implicitly granted to an actor.
167+ *
168+ * For example, Puter's filesystem uses this to grant permission to a file if the specified
169+ * 'actor' is the owner of the file.
170+ *
171+ * An implicator is constructed using the static method PermissionImplicator.create({ matcher, checker }).
172+ * `matcher is a function that takes a permission string and returns true if the implicator should be applied.
173+ * `checker` is a function that takes an actor and a permission string and returns true if the permission is implied.
174+ * The actor and permission are passed to checker({ actor, permission }) as an object.
175+ */
176+ export class PermissionImplicator {
177+ static create ( { id, matcher, checker, ...options } ) {
178+ return new PermissionImplicator ( { id, matcher, checker, options } ) ;
179+ }
180+
181+ constructor ( { id, matcher, checker, options } ) {
182+ this . id = id ;
183+ this . matcher = matcher ;
184+ this . checker = checker ;
185+ this . options = options ;
186+ }
187+
188+ matches ( permission ) {
189+ return this . matcher ( permission ) ;
190+ }
191+
192+ /**
193+ * Check if the permission is implied by this implicator
194+ * @param {Actor } actor
195+ * @param {string } permission
196+ * @returns
197+ */
198+ /**
199+ * Rewrites a permission string if it matches any registered rewriter.
200+ * @param {string } permission - The permission string to potentially rewrite.
201+ * @returns {Promise<string> } The possibly rewritten permission string.
202+ */
203+ async check ( { actor, permission, recurse } ) {
204+ return await this . checker ( { actor, permission, recurse } ) ;
205+ }
206+ }
207+
208+ /**
209+ * Permission exploders are used to map any permission to a list of permissions
210+ * which are considered to imply the specified permission.
211+ *
212+ * It uses a matcher function to determine if a permission should be exploded
213+ * and an exploder function to perform the expansion.
214+ *
215+ * The exploder is constructed using the static method PermissionExploder.create({ matcher, explode }).
216+ * The `matcher` is a function that takes a permission string and returns true if the exploder should be applied.
217+ * The `explode` is a function that takes an actor and a permission string and returns a list of implied permissions.
218+ * The actor and permission are passed to explode({ actor, permission }) as an object.
219+ */
220+ export class PermissionExploder {
221+ static create ( { id, matcher, exploder } ) {
222+ return new PermissionExploder ( { id, matcher, exploder } ) ;
223+ }
224+
225+ constructor ( { id, matcher, exploder } ) {
226+ this . id = id ;
227+ this . matcher = matcher ;
228+ this . exploder = exploder ;
229+ }
230+
231+ matches ( permission ) {
232+ return this . matcher ( permission ) ;
233+ }
234+
235+ /**
236+ * Explodes a permission into a set of implied permissions.
237+ *
238+ * This method takes a permission string and an actor object,
239+ * then uses the associated exploder function to derive additional
240+ * permissions that are implied by the given permission.
241+ *
242+ * @param {Object } options - The options object containing:
243+ * @param {Actor } options.actor - The actor requesting the permission explosion.
244+ * @param {string } options.permission - The base permission to be exploded.
245+ * @returns {Promise<Array<string>> } A promise resolving to an array of implied permissions.
246+ */
247+ async explode ( { actor, permission } ) {
248+ return await this . exploder ( { actor, permission } ) ;
249+ }
250+ }
0 commit comments