• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

casbin / node-casbin / 20033901959

08 Dec 2025 03:45PM UTC coverage: 78.24%. First build
20033901959

Pull #524

github

web-flow
Merge 93c23fa2e into 52ca51b9a
Pull Request #524: Add methods to retrieve domains for users and roles

842 of 1174 branches covered (71.72%)

Branch coverage included in aggregate %.

24 of 26 new or added lines in 2 files covered. (92.31%)

1621 of 1974 relevant lines covered (82.12%)

348.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.07
/src/rbac/defaultRoleManager.ts
1
// Copyright 2018 The Casbin Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//      http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14

15
import { RoleManager } from './roleManager';
16
import { getLogger, logPrint } from '../log';
32✔
17

18
export type MatchingFunc = (arg1: string, arg2: string) => boolean;
19

20
// DEFAULT_DOMAIN defines the default domain space.
21
const DEFAULT_DOMAIN = 'casbin::default';
32✔
22

23
// loadOrDefault returns the existing value for the key if present.
24
// Otherwise, it stores and returns the given value.
25
function loadOrDefault<K, V>(map: Map<K, V>, key: K, value: V): V {
26
  const read = map.get(key);
5,830✔
27
  if (read === undefined) {
5,830✔
28
    map.set(key, value);
1,470✔
29
    return value;
1,470✔
30
  }
31
  return read;
4,360✔
32
}
33

34
/**
35
 * Role represents the data structure for a role in RBAC.
36
 */
37
class Role {
38
  public name: string;
39
  private roles: Role[];
40

41
  constructor(name: string) {
42
    this.name = name;
3,352✔
43
    this.roles = [];
3,352✔
44
  }
45

46
  public addRole(role: Role): void {
47
    if (this.roles.some((n) => n.name === role.name)) {
940✔
48
      return;
52✔
49
    }
50
    this.roles.push(role);
888✔
51
  }
52

53
  public deleteRole(role: Role): void {
54
    this.roles = this.roles.filter((n) => n.name !== role.name);
28✔
55
  }
56

57
  public hasRole(name: string, hierarchyLevel: number): boolean {
58
    if (this.name === name) {
1,830✔
59
      return true;
360✔
60
    }
61
    if (hierarchyLevel <= 0) {
1,470!
62
      return false;
×
63
    }
64
    for (const role of this.roles) {
1,470✔
65
      if (role.hasRole(name, hierarchyLevel - 1)) {
782✔
66
        return true;
424✔
67
      }
68
    }
69

70
    return false;
1,046✔
71
  }
72

73
  public hasDirectRole(name: string): boolean {
74
    return this.roles.some((n) => n.name === name);
250✔
75
  }
76

77
  public toString(): string {
78
    return this.name + this.roles.join(', ');
×
79
  }
80

81
  public getRoles(): string[] {
82
    return this.roles.map((n) => n.name);
898✔
83
  }
84
}
85

86
class Roles extends Map<string, Role> {
87
  constructor() {
88
    super();
4,526✔
89
  }
90

91
  public hasRole(name: string, matchingFunc?: MatchingFunc): boolean {
92
    let ok = false;
3,078✔
93
    if (matchingFunc) {
3,078✔
94
      this.forEach((value, key) => {
136✔
95
        if (matchingFunc(name, key)) {
712✔
96
          ok = true;
126✔
97
        }
98
      });
99
    } else {
100
      return this.has(name);
2,942✔
101
    }
102
    return ok;
136✔
103
  }
104

105
  public createRole(name: string, matchingFunc?: MatchingFunc): Role {
106
    const role = loadOrDefault(this, name, new Role(name));
2,368✔
107
    if (matchingFunc) {
2,368✔
108
      this.forEach((value, key) => {
668✔
109
        if (matchingFunc(name, key) && name !== key) {
2,534✔
110
          // Add new role to matching role
111
          const role1 = loadOrDefault(this, key, new Role(key));
88✔
112
          role.addRole(role1);
88✔
113
        }
114
      });
115
    }
116
    return role;
2,368✔
117
  }
118
}
119

120
// RoleManager provides a default implementation for the RoleManager interface
121
export class DefaultRoleManager implements RoleManager {
32✔
122
  private allDomains: Map<string, Roles>;
123
  private maxHierarchyLevel: number;
124
  private hasPattern = false;
1,642✔
125
  private hasDomainPattern = false;
1,642✔
126
  private hasDomainHierarchy = false;
1,642✔
127
  private domainHierarchyManager: RoleManager;
128
  private matchingFunc: MatchingFunc;
129
  private domainMatchingFunc: MatchingFunc;
130

131
  /**
132
   * DefaultRoleManager is the constructor for creating an instance of the
133
   * default RoleManager implementation.
134
   *
135
   * @param maxHierarchyLevel the maximized allowed RBAC hierarchy level.
136
   */
137
  constructor(maxHierarchyLevel: number) {
138
    this.allDomains = new Map<string, Roles>();
1,642✔
139
    this.allDomains.set(DEFAULT_DOMAIN, new Roles());
1,642✔
140
    this.maxHierarchyLevel = maxHierarchyLevel;
1,642✔
141
  }
142

143
  /**
144
   * addMatchingFunc support use pattern in g
145
   * @param name name
146
   * @param fn matching function
147
   * @deprecated
148
   */
149
  public async addMatchingFunc(name: string, fn: MatchingFunc): Promise<void>;
150

151
  /**
152
   * addMatchingFunc support use pattern in g
153
   * @param fn matching function
154
   */
155
  public async addMatchingFunc(fn: MatchingFunc): Promise<void>;
156

157
  /**
158
   * addMatchingFunc support use pattern in g
159
   * @param name name
160
   * @param fn matching function
161
   * @deprecated
162
   */
163
  public async addMatchingFunc(name: string | MatchingFunc, fn?: MatchingFunc): Promise<void> {
164
    this.hasPattern = true;
10✔
165
    if (typeof name === 'string' && fn) {
10!
166
      this.matchingFunc = fn;
×
167
    } else if (typeof name === 'function') {
10!
168
      this.matchingFunc = name;
10✔
169
    } else {
170
      throw new Error('error: domain should be 1 parameter');
×
171
    }
172
  }
173

174
  /**
175
   * addDomainMatchingFunc support use domain pattern in g
176
   * @param fn domain matching function
177
   * ```
178
   */
179
  public async addDomainMatchingFunc(fn: MatchingFunc): Promise<void> {
180
    this.hasDomainPattern = true;
6✔
181
    this.domainMatchingFunc = fn;
6✔
182
  }
183

184
  /**
185
   * addDomainHierarchy sets a rolemanager to define role inheritance between domains
186
   * @param rm RoleManager to define domain hierarchy
187
   */
188
  public async addDomainHierarchy(rm: RoleManager): Promise<void> {
189
    if (!rm?.syncedHasLink) throw Error('Domain hierarchy must be syncronous.');
2!
190
    this.hasDomainHierarchy = true;
2✔
191
    this.domainHierarchyManager = rm;
2✔
192
  }
193

194
  private generateTempRoles(domain: string): Roles {
195
    if (!this.hasPattern && !this.hasDomainPattern && !this.hasDomainHierarchy) {
1,910✔
196
      return loadOrDefault(this.allDomains, domain, new Roles());
1,706✔
197
    }
198

199
    const extraDomain = new Set([domain]);
204✔
200
    if (this.hasDomainPattern || this.hasDomainHierarchy) {
204✔
201
      this.allDomains.forEach((value, key) => {
142✔
202
        if (
644✔
203
          (this.hasDomainPattern && this.domainMatchingFunc(domain, key)) ||
1,880✔
204
          (this.domainHierarchyManager?.syncedHasLink && this.domainHierarchyManager.syncedHasLink(key, domain))
1,776✔
205
        ) {
206
          extraDomain.add(key);
250✔
207
        }
208
      });
209
    }
210

211
    const allRoles = new Roles();
204✔
212
    extraDomain.forEach((dom) => {
204✔
213
      loadOrDefault(this.allDomains, dom, new Roles()).forEach((value, key) => {
324✔
214
        const role1 = allRoles.createRole(value.name, this.matchingFunc);
748✔
215
        value.getRoles().forEach((n) => {
748✔
216
          role1.addRole(allRoles.createRole(n, this.matchingFunc));
432✔
217
        });
218
      });
219
    });
220
    return allRoles;
204✔
221
  }
222

223
  /**
224
   * addLink adds the inheritance link between role: name1 and role: name2.
225
   * aka role: name1 inherits role: name2.
226
   * domain is a prefix to the roles.
227
   */
228
  public async addLink(name1: string, name2: string, ...domain: string[]): Promise<void> {
229
    if (domain.length === 0) {
420✔
230
      domain = [DEFAULT_DOMAIN];
348✔
231
    } else if (domain.length > 1) {
72!
232
      throw new Error('error: domain should be 1 parameter');
×
233
    }
234

235
    const allRoles = loadOrDefault(this.allDomains, domain[0], new Roles());
420✔
236

237
    const role1 = loadOrDefault(allRoles, name1, new Role(name1));
420✔
238
    const role2 = loadOrDefault(allRoles, name2, new Role(name2));
420✔
239
    role1.addRole(role2);
420✔
240
  }
241

242
  /**
243
   * clear clears all stored data and resets the role manager to the initial state.
244
   */
245
  public async clear(): Promise<void> {
246
    this.allDomains = new Map();
202✔
247
    this.allDomains.set(DEFAULT_DOMAIN, new Roles());
202✔
248
  }
249

250
  /**
251
   * deleteLink deletes the inheritance link between role: name1 and role: name2.
252
   * aka role: name1 does not inherit role: name2 any more.
253
   * domain is a prefix to the roles.
254
   */
255
  public async deleteLink(name1: string, name2: string, ...domain: string[]): Promise<void> {
256
    if (domain.length === 0) {
28✔
257
      domain = [DEFAULT_DOMAIN];
24✔
258
    } else if (domain.length > 1) {
4!
259
      throw new Error('error: domain should be 1 parameter');
×
260
    }
261

262
    const allRoles = loadOrDefault(this.allDomains, domain[0], new Roles());
28✔
263

264
    if (!allRoles.has(name1) || !allRoles.has(name2)) {
28!
265
      return;
×
266
    }
267

268
    const role1 = loadOrDefault(allRoles, name1, new Role(name1));
28✔
269
    const role2 = loadOrDefault(allRoles, name2, new Role(name2));
28✔
270
    role1.deleteRole(role2);
28✔
271
  }
272

273
  /**
274
   * hasLink determines whether role: name1 inherits role: name2.
275
   * domain is a prefix to the roles.
276
   */
277
  public syncedHasLink(name1: string, name2: string, ...domain: string[]): boolean {
278
    if (domain.length === 0) {
2,124✔
279
      domain = [DEFAULT_DOMAIN];
1,878✔
280
    } else if (domain.length > 1) {
246!
281
      throw new Error('error: domain should be 1 parameter');
×
282
    }
283

284
    if (name1 === name2) {
2,124✔
285
      return true;
488✔
286
    }
287

288
    const allRoles = this.generateTempRoles(domain[0]);
1,636✔
289

290
    if (!allRoles.hasRole(name1, this.matchingFunc) || !allRoles.hasRole(name2, this.matchingFunc)) {
1,636✔
291
      return false;
588✔
292
    }
293

294
    const role1 = allRoles.createRole(name1, this.matchingFunc);
1,048✔
295
    return role1.hasRole(name2, this.maxHierarchyLevel);
1,048✔
296
  }
297

298
  public async hasLink(name1: string, name2: string, ...domain: string[]): Promise<boolean> {
299
    return new Promise((resolve) => resolve(this.syncedHasLink(name1, name2, ...domain)));
1,122✔
300
  }
301

302
  /**
303
   * getRoles gets the roles that a subject inherits.
304
   * domain is a prefix to the roles.
305
   */
306
  public async getRoles(name: string, ...domain: string[]): Promise<string[]> {
307
    if (domain.length === 0) {
188✔
308
      domain = [DEFAULT_DOMAIN];
126✔
309
    } else if (domain.length > 1) {
62!
310
      throw new Error('error: domain should be 1 parameter');
×
311
    }
312

313
    const allRoles = this.generateTempRoles(domain[0]);
188✔
314

315
    if (!allRoles.hasRole(name, this.matchingFunc)) {
188✔
316
      return [];
48✔
317
    }
318

319
    return allRoles.createRole(name, this.matchingFunc).getRoles();
140✔
320
  }
321

322
  /**
323
   * getUsers gets the users that inherits a subject.
324
   * domain is an unreferenced parameter here, may be used in other implementations.
325
   */
326
  public async getUsers(name: string, ...domain: string[]): Promise<string[]> {
327
    if (domain.length === 0) {
86✔
328
      domain = [DEFAULT_DOMAIN];
66✔
329
    } else if (domain.length > 1) {
20!
330
      throw new Error('error: domain should be 1 parameter');
×
331
    }
332

333
    const allRoles = this.generateTempRoles(domain[0]);
86✔
334

335
    if (!allRoles.hasRole(name, this.matchingFunc)) {
86✔
336
      return [];
34✔
337
    }
338
    const users = [];
52✔
339
    for (const user of allRoles.values()) {
52✔
340
      if (user.hasDirectRole(name)) users.push(user.name);
248✔
341
    }
342
    return users;
52✔
343
  }
344

345
  /**
346
   * printRoles prints all the roles to log.
347
   */
348
  public async printRoles(): Promise<void> {
349
    if (getLogger().isEnable()) {
226!
350
      [...this.allDomains.values()].forEach((n) => {
×
351
        logPrint(n.toString());
×
352
      });
353
    }
354
  }
355

356
  /**
357
   * getDomains gets domains that a user has.
358
   */
359
  public async getDomains(name: string): Promise<string[]> {
360
    const domains: string[] = [];
6✔
361
    this.allDomains.forEach((roles, domain) => {
6✔
362
      // Skip the default domain if there are other domains
363
      if (domain === DEFAULT_DOMAIN && this.allDomains.size > 1) {
24✔
364
        return;
6✔
365
      }
366
      const role = roles.get(name);
18✔
367
      if (role && (role.getRoles().length > 0 || this.hasUserOrRole(roles, name))) {
18✔
368
        domains.push(domain);
10✔
369
      }
370
    });
371
    return domains;
6✔
372
  }
373

374
  /**
375
   * getAllDomains gets all domains.
376
   */
377
  public async getAllDomains(): Promise<string[]> {
378
    const domains = Array.from(this.allDomains.keys());
2✔
379
    // Filter out the default domain if there are other domains
380
    if (domains.length > 1) {
2!
381
      return domains.filter((d) => d !== DEFAULT_DOMAIN);
6✔
382
    }
NEW
383
    return domains;
×
384
  }
385

386
  private hasUserOrRole(roles: Roles, name: string): boolean {
387
    for (const role of roles.values()) {
2✔
388
      if (role.hasDirectRole(name)) {
2!
389
        return true;
2✔
390
      }
391
    }
NEW
392
    return false;
×
393
  }
394
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc