eiam-scan-resource-controls
v0.0.2
Published
Registers every checkRole permission code in the database automatically: a CLI that scans the source at build time, a runtime scan on route changes, and an optional per-control element that also replaces checkRole.
Maintainers
Readme
eiam-scan-resource-controls
Add a checkRole in the markup — the permission registers itself. No manual entry in
Roles Manager, no code change anywhere else.
Three ways to use it — pick one (or combine 1 + 2)
| # | Way | What you change | Coverage |
| --- | --- | --- | --- |
| 1 | CLI at build time — npx eiam-scan-permissions | one npm script | every code in the source, even on pages nobody opened |
| 2 | Runtime scan — startScanning() in main.ts | 2 lines, once | only the pages users actually visit |
| 3 | Per control — <eiam-scan-resource-controls code="CADP"> | every control (also replaces checkRole) | the controls you converted |
Recommended: 1 as the base, 2 on top. The CLI guarantees complete coverage and runs in seconds; the runtime scan catches codes that are only known at runtime.
1. CLI — the fast, complete way
npx eiam-scan-permissions --dir ./src --api-url https://localhost:5000 --application-id 50It reads every .html and .ts under --dir, pulls out every code declared on the
directive, and posts them all in one request. On this project that is 206 codes from 806
files in a couple of seconds, without opening a single page.
Scanned 806 file(s) under D:\Git\EIAM-Git-New\Frontend\EIAM -Setup\src
Directives: checkRole, check-role, data-permission
Found 206 permission code(s) in 36 file(s)Wire it into the app's package.json so it runs on every build and you never think about
it again:
"scripts": {
"build": "ng build && npm run scan:permissions",
"scan:permissions": "eiam-scan-permissions --dir ./src --api-url https://localhost:5000 --application-id 50"
}Options
| Option | Default | Description |
| --- | --- | --- |
| --dir | cwd | Folder to scan. |
| --directives | checkRole,check-role,data-permission | The directive name(s) to look for — change it if you rename the directive. |
| --extensions | .html,.ts | File types to read. |
| --api-url | $EIAM_API_URL | Where to post. Without it nothing is sent. |
| --application-id | — | Sent as the header and in the body. |
| --endpoint | /api/RolesManagement/ScanPermissions | Scan API path. |
| --permission-type | 1 | EPermissionType. |
| --group-id | — | Permission group. |
| --out | — | Write the codes to a JSON file instead of / as well as posting. |
| --dry-run | — | Print what was found and send nothing. |
| --verbose | — | List the codes per file. |
| --insecure | — | Accept a self-signed certificate (local https servers). |
It reads checkRole="CADP", checkRole='CADP', checkRole="A,B" and [checkRole]="'CADP'".
Values that only exist at runtime (checkRole="{{ code }}", [checkRole]="user.perm")
cannot be read from the source, so they are reported separately rather than guessed —
those are the ones way 2 covers.
2. Runtime scan — for codes that are built at runtime
Two lines in src/main.ts, nothing else in the whole application:
import { startScanning } from 'eiam-scan-resource-controls';
startScanning({ apiUrl: window['config']?.apiUrl, applicationId: environment.eiamSetupAppId });On every route change it reads the rendered markup and registers anything new. Codes
already sent are cached in sessionStorage, so navigating around costs nothing.
Because the checkRole directive removes an element when the user lacks the code, the
scanner watches removedNodes as well as addedNodes — otherwise it would only ever see
the permissions the signed-in user already holds.
| Option | Default | Description |
| --- | --- | --- |
| permissionAttributes | checkRole,checkrole,check-role,code,data-permission | Attributes to scan. |
| scanDelay | 800 | Wait after a route change before scanning. |
| observeWindow | 8000 | Keep watching the route for lazily rendered content. |
Plus every option from Shared configuration below. stopScanning() and scanNow() are
exported too.
3. Per control — optional
Replaces checkRole and registers the permission at the same time.
Wrap a control, give it a code, and the package does both jobs:
- registers the code in the database, so it appears in Roles Manager and can be attached to a role in Add/Edit Role without anyone typing it in by hand;
- hides the control when the signed-in user does not hold the code — the same rule
the
checkRoledirective applies today.
It is the localization scan (Localization/ScanResource) idea applied to authorization
controls: the markup is the source of truth, the database follows it.
<!-- before -->
<button checkRole="CADP">Add</button>
<!-- after -->
<eiam-scan-resource-controls code="CADP">
<button>Add</button>
</eiam-scan-resource-controls>Nothing else changes in the component: no imports, no ngOnInit, no service.
Install
npm i eiam-scan-resource-controlsThe CLI (way 1) needs nothing else — npx eiam-scan-permissions works straight away.
Ways 2 and 3 need the element registered and the API configured once, in
src/main.ts:
import { defineCustomElements as defineScanResourceControls } from 'eiam-scan-resource-controls/loader';
// Read by every control on every page; no control repeats these settings.
(window as any).EIAMScanResourceControls = {
apiUrl: window['config']?.apiUrl || '',
applicationId: environment.eiamSetupAppId,
cultureCode: 'en-US',
isMicroservice: false,
};
platformBrowserDynamic().bootstrapModule(AppModule).then(() => {
defineFavoritePagesElements(window);
defineUserPreferenceElements(window);
defineScanResourceControls(window); // <-- add this
}).catch(err => console.log(err));AppModule already declares CUSTOM_ELEMENTS_SCHEMA, so no other Angular change is needed.
The tag. The class is
EIAMScanResourceControls; the HTML tag iseiam-scan-resource-controls. It has to be: the HTML parser lower-cases tag names and the custom element registry rejects a name without a hyphen, so<EIAMScanResourceControls>would be parsed as an unknown element and never upgrade — it would silently do nothing. This is the same name in the only casing the platform accepts, and it matcheseiam-favorite-pages/eiam-user-preference.
Migrating from checkRole
| Today | With the package |
| --- | --- |
| <button checkRole="CADP">Add</button> | <eiam-scan-resource-controls code="CADP"><button>Add</button></eiam-scan-resource-controls> |
| <a checkRole="CEORG,CVORGB">Edit</a> (all required) | <eiam-scan-resource-controls code="CEORG,CVORGB"><a>Edit</a></eiam-scan-resource-controls> |
| <a checkRole="CEORG,CVORGB" [minRoleCheck]="true"> (any) | <eiam-scan-resource-controls code="CEORG,CVORGB" any="true"> |
Behaviour is matched deliberately: codes are trimmed and upper-cased, every code is
required unless any is set, and a denied control is removed from the DOM, not merely
hidden — exactly what RolePermissionDirective does with nativeElement.remove().
Migrating gradually? Add scan-only="true" and keep the existing checkRole on the
inner element. The package then only registers the code and lets the old directive keep
doing the hiding, so a page can be converted with zero behavioural risk:
<eiam-scan-resource-controls code="CADP" scan-only="true">
<button checkRole="CADP">Add</button>
</eiam-scan-resource-controls>Properties
| Property | Attribute | Type | Default | Description |
| --- | --- | --- | --- | --- |
| code | code | string | — | The permission code, or several separated by commas. |
| name | name | string | — | Friendly name stored with the permission. Defaults to the code. |
| any | any | boolean | false | With several codes, require only one (like minRoleCheck). |
| permissionSource | permission-source | 'roleInfo' \| 'userPermissions' \| 'both' | 'roleInfo' | Which granted-code list is checked. |
| scanOnly | scan-only | boolean | false | Register the code but never hide the control. |
| denyMode | deny-mode | 'remove' \| 'hide' | 'remove' | How a denied control disappears. |
| disabled | disabled | boolean | false | Skips the check and the registration entirely. |
Methods
| Method | Description |
| --- | --- |
| refresh(): Promise<boolean> | Re-runs the permission check, e.g. after permissions were reloaded. |
| flushRegistrations(): Promise<void> | Sends queued codes now instead of waiting for the batch delay. |
| getCodes(): Promise<string[]> | The normalised codes this control requires. |
Window events
| Event | Detail |
| --- | --- |
| eiamPermissionsScanned | { slug, permissions: [{ code, name }] } |
| eiamPermissionsScanFailed | { slug, error } |
Shared configuration
Set window.EIAMScanResourceControls (as above) or call configure() once:
import { configure } from 'eiam-scan-resource-controls';
configure({ apiUrl: '...', applicationId: 50, grantedCodes: myPermissions });| Option | Default | Description |
| --- | --- | --- |
| apiUrl | — | Base API url. Without it nothing is sent; codes stay queued. |
| applicationId | — | Sent as the applicationId header and in the body. |
| cultureCode | 'en-US' | Sent as the CultureCode header. |
| isMicroservice | false | Builds the url as {apiUrl}/{service}/{endpoint}. |
| endpoint | '/api/RolesManagement/ScanPermissions' | Where codes are registered. |
| service | 'security' | Service segment, used only when isMicroservice is true. |
| permissionType | 1 | EPermissionType stored with each code. |
| groupId | — | Optional permission group for the scanned codes. |
| grantedCodes | — | The user's codes. When omitted they are read from storage (see below). |
| batchDelay | 300 | Milliseconds to collect controls before sending one request. |
| disabled | false | Turns registration off; permission checking still works. |
| debug | false | Logs every batch to the console. |
Where granted codes come from
Unless grantedCodes is configured, the component reads the same places the application
already writes to:
permissionSource="roleInfo"(default) —localStorage.user.RoleInfo, whatRolePermissionDirectiveuses today.permissionSource="userPermissions"—sessionStorage.userPermissions, written byRolePermisssionServicefrom theApplications/GetSlugInforesponse.permissionSource="both"— the union.
Feeding configure({ grantedCodes }) from RolePermisssionService.permissions$ is the
cleanest option: it removes the dependency on storage key names entirely.
How registration works
Every control on a page queues its code into one shared queue. After batchDelay (300 ms)
the queue posts one request for the whole page — a page with forty controls costs one
call, not forty. Registered codes are cached in sessionStorage.eiamScannedPermissions, so
revisiting a page sends nothing at all. A failed request drops the cache entry and the
codes are retried on the next page.
POST {apiUrl}/api/RolesManagement/ScanPermissions
Content-Type: application/json
CultureCode: en-US
Source: 2
applicationId: 50
credentials: include{
"slug": "users-permissions/roles",
"applicationId": 50,
"type": 1,
"groupId": null,
"permissions": [
{ "code": "CADP", "name": "CADP" },
{ "code": "CEP", "name": "Edit page" }
]
}Backend action this needs — NOT YET IMPLEMENTED
RolesManagementController has AddPermission (one permission per call,
AddPermissionsViewModel = Name, Code, Type, GroupId, RelatedActionIds) but no
batch scan action. Add one that upserts by Code and ignores codes that already exist,
mirroring LocalizationController.ScanResource:
[HttpPost]
[Route("ScanPermissions")]
public async Task<IActionResult> ScanPermissions(ScanPermissionsViewModel obj)
=> Ok(await _permissionService.ScanPermissionsAsync(obj));
public class ScanPermissionsViewModel
{
public string Slug { get; set; }
public long? ApplicationId { get; set; }
public EPermissionType Type { get; set; }
public long? GroupId { get; set; }
public List<ScanPermissionVM> Permissions { get; set; }
}
public class ScanPermissionVM
{
public string Code { get; set; } // max 50, matches Permission.Code
public string Name { get; set; } // max 450
}Until that action exists the component still hides and shows controls correctly; the
registration call just fails and emits eiamPermissionsScanFailed. Nothing on the page
breaks. If you would rather not add an endpoint, point endpoint at an existing action
and adjust the body in flush().
Layout
The element is display: contents, so wrapping an <li> inside a <ul>, or a flex/grid
child, does not change the layout. If a stylesheet targets a direct child selector
across the wrapper (ul > li, .btn-group > .btn), that one selector will no longer
match — those are the only cases that need a CSS touch-up.
Develop
npm install # PUPPETEER_SKIP_DOWNLOAD=1 if the Chrome download is blocked
npm run build
npm test # 23 unit tests
npm start # dev server; src/index.html shows every mode side by side