Roles Module
Fusion Framework Roles module
@equinor/fusion-framework-module-roles makes the Fusion Roles V2 service available as a
typed Fusion Framework module. Use it when a host, portal, or application needs authenticated
RolesV2 requests without resolving service discovery or constructing an HTTP client itself.
During configuration, RolesModuleConfigurator creates its default client by resolving therolesv2 service. An app-scoped service discovery provider takes precedence; otherwise the
configurator uses ref.serviceDiscovery inherited from the parent framework. During module
initialization, the client receives a current-account resolver and then reads the selected
authentication account for every operation. Account identifiers remain internal to the module.
When the event and telemetry modules are enabled, the provider also reports operation outcomes.
Enable Roles V2 and require roles during initialization
Enable authentication before the roles module. The default client also needs service discovery,
either in the same module scope or inherited from the parent framework; configure a custom client
when service discovery is unavailable:
import { enableRoles } from '@equinor/fusion-framework-module-roles';
enableRoles(configurator, (builder) => {
builder.requireAccessRoles(['Reports.Read', 'Reports.Export']);
});RolesModuleConfigurator.requireAccessRoles checks the configured access-role names before module
initialization completes. Every configured role must be active for the signed-in account. Role
names use exact, case-sensitive matching against the Roles V2 accessRoleName.
Initialization throws RequiredAccessRolesError containing all missing role names when the account
does not satisfy the requirements. This name-only check follows the Roles V2 generic authorization
requirement and accepts global active assignments only; scoped assignments require a separate,
scope-aware authorization flow. Omit requireAccessRoles, or call enableRoles(configurator)
without a configuration callback, when initialization should not enforce an access-role guard.
Multiple requireAccessRoles calls accumulate requirements. The method also accepts a builder callback
when the required roles depend on configuration context.
Applications and portal modules can call enableRoles without configuring their own service
discovery. RolesModuleConfigurator creates a local default client through inheritedref.serviceDiscovery, and module initialization creates a local RolesProvider.
After provider construction, module initialization verifies the configured requirements withRolesProvider.hasAccessRole(requiredAccessRoles, { assert: true, required: true }) before returning it.
Show active, claimable, and consolidated role assignments
The provider resolves the signed-in Fusion account through the auth module:
const [activeRoles, claimableRoles, consolidatedRoles] = await Promise.all([
framework.modules.roles.getActiveAccessRoleAssignments(),
framework.modules.roles.getConsolidatedClaimableRoleAssignments(),
framework.modules.roles.getConsolidatedRoleAssignments(),
]);
const canReadReports = await framework.modules.roles.hasAccessRole(['Reports.Read'], {
required: true,
});
const canClaimReportReader = await framework.modules.roles.hasClaimableRoleAssignmentForAccessRole('Reports.Read');
const requiredRoleStatuses = await framework.modules.roles.getRequiredAccessRoleStatuses([
'Reports.Read',
'Reports.Export',
]);hasClaimableRoleAssignmentForAccessRole follows every page of the account's claimable assignments
and expands accessRoleMappings. It returns true only for a global assignment that is currently
inside its validity window, is not already active, and grants the requested access-role name.getRequiredAccessRoleStatuses reports whether each requested access role exists and which global
claimable assignments can currently grant it. Hosts can use these statuses to explain or recover
from failed initialization requirements.
getActiveAccessRoleAssignments reads /active-access-role-assignments: currently effective,
deduplicated access-role assignments with provenance dropped. Its assignmentType cannot reliably
distinguish a standing grant from an activated claim. Use getConsolidatedRoleAssignments as the
authoritative source for standing, non-claimable assignment state: it reads the/consolidated-role-assignments endpoint directly rather than inferring provenance from active
assignments. These assignments are not claimable, but Roles V2 never calls them permanent — they may
still be validity-bounded. getConsolidatedClaimableRoleAssignments returns assigned claimable
roles, including assignments that can be future, expired, or already active; it is authoritative
for assignment and claimed state, not a filtered list of roles currently eligible for activation.
Unlike active and claimable assignments, consolidated role assignments carry no isActive flag;
callers that need current effectiveness must compute it from validFrom/validTo.
Request and response validation errors from @equinor/fusion-services and HTTP request errors
are preserved as the cause of a RolesError.
The built-in client caches active-access, claimable, and consolidated role-assignment reads, plus
claim-eligibility results, for one minute through @equinor/fusion-query. Concurrent matching reads
share the same request. A successful claim or deactivation invalidates the active-access, claimable,
and claim-eligibility caches, but deliberately leaves the consolidated-role-assignment cache
untouched: those assignments are outside the scope of claim and deactivate mutations.
Pass { refresh: true } to any of the three collection reads to invalidate that collection's cache
before loading it again:
const activeRoles = await framework.modules.roles.getActiveAccessRoleAssignments({ refresh: true });Claim a role
const activation = await framework.modules.roles.activateClaimableRoleAssignment({
assignmentId: claimableRoleId,
reason: 'Support incident response',
hours: 4,
});
await framework.modules.roles.deactivateClaimableRoleAssignment({
assignmentId: claimableRoleId,
});Roles V2 requires a non-empty activation reason of at most 500 characters and an integer hours
value from 1 through 24. The typed client validates these constraints before sending the request.
A successful claim or deactivation invalidates the active-access, claimable, and claim-eligibility
caches so the next request refreshes them. The consolidated-role-assignment cache is deliberately
excluded, since those assignments are unaffected by claim and deactivate mutations.
Before activation, the provider dispatches a cancelable onRoles.activateClaimableRoleAssignment event containing the claim
input. A listener can call preventDefault() to stop the Roles V2 request:
framework.modules.event.addEventListener('onRoles.activateClaimableRoleAssignment', (event) => {
if (!mayActivateRole(event.detail.assignmentId)) {
event.preventDefault();
}
});The provider records success events and failure exceptions through the telemetry module. Its
telemetry contains stable operation names and outcomes, but excludes account and role identifiers.
Supply an internal client without service discovery
enableRoles(configurator, (builder) => {
builder.setClient(async () => {
return createMyRolesClient();
});
});RolesModuleConfigurator.setClient accepts either an IRolesClient instance or a builder callback
that resolves one during configuration. It bypasses service discovery for hosts that create their
own client, test environments, and custom transports.
Client operations return cold RxJS observables; the provider subscribes and exposes
Promises for unpaged operations and an async iterator for paginated access roles.
Custom clients must emit one result and complete, or report failure through the
observable error channel. The built-in client uses the typed service endpoints' json$ transport.
Module initialization calls IRolesClient.initialize for custom and built-in clients.RolesClientInitializeOptions.resolveCurrentAccountIdentifier returns the account selected when
the operation executes. Custom clients should retain that resolver and call it per operation rather
than storing one account identifier during initialization. This keeps account changes current and
prevents one account's role data from being used for another account.
Direct RolesClient construction also requires an account resolver as its second argument:
const client = new RolesClient(httpClient, resolveCurrentAccountIdentifier);When the client is supplied through RolesModuleConfigurator.setClient, module.initialize callsclient.initialize with the framework resolver before constructing the provider. Custom and mock
clients can extend RolesClient; its transport, account resolver, query resources, and request
helpers are protected extension points.
Consume paginated access roles
framework.modules.roles.getAccessRoles() returns an async iterator of access-role pages.RolesClient.getAccessRoles({ top, skip }, signal) emits a single service page as an observable,
including its continuation metadata. The provider converts those page observables into the
async iterator and requests the next offset only when the consumer advances it.
The provider does not accumulate the registry or prefetch pages: consumers control memory use
and request volume by advancing the iterator and processing each page before requesting the next.
for await (const page of framework.modules.roles.getAccessRoles(abortController.signal)) {
await processPage(page);
if (hasEnoughResults()) {
break;
}
}Breaking iteration prevents further requests; an optional AbortSignal also cancels an in-flight
page. Page failures reject iteration with a RolesError. Active access-role assignments,
consolidated claimable-role assignments, and consolidated role assignments remain Promise-based
arrays because those service endpoints are not paginated.getRequiredAccessRoleStatuses is a bounded lookup of explicitly requested names, not a registry listing;
it retains only matching roles and stops paging once every requested name is found.
This module supports role-aware user interfaces. A trusted backend must still enforce
authorization for protected operations.
Handle Roles errors
Every provider failure extends RolesError. Use RolesError.is(error) to narrow an unknown thrown
value without parsing its message:
import { RolesError } from '@equinor/fusion-framework-module-roles/errors';
try {
await framework.modules.roles.activateClaimableRoleAssignment({
assignmentId: claimableRoleId,
reason: 'Support incident response',
hours: 4,
});
} catch (error) {
if (RolesError.is(error)) {
reportRolesFailure(error);
}
}RequiredAccessRolesErrorreports missing bootstrap roles and missing active account requirements.ActivateClaimableRoleAssignmentErrorreports claim cancellation, event dispatch, and Roles V2 activation failures.DeactivateClaimableRoleAssignmentErrorreports Roles V2 deactivation failures.RolesErrorreports other provider, configuration, client, and request failures.
Wrapped service and transport errors remain available through error.cause.
Test the module
Choose the boundary the test needs to exercise.
Static provider data
Use the /mock entry point for component and application tests that need known role lists without
HTTP, authentication, or service discovery. It runs the production module initializer and provider
with a static internal client:
import { enableRolesMock } from '@equinor/fusion-framework-module-roles/mock';
enableRolesMock(configurator, (mock) => {
mock
.setActiveAccessRoleAssignments([{ systemName: 'Reports', accessRoleName: 'Reports.Read' }])
.setConsolidatedClaimableRoleAssignments([
{ id: 'assignment-id', claimableRole: { id: 'role-id' } },
])
.setConsolidatedRoleAssignments([{ id: 'assigned-assignment-id', role: { id: 'role-id' } }])
.requireAccessRoles(['Reports.Read']);
});Consumers still receive the production RolesProvider. Override a provider operation with the
test runner when a test needs behavior beyond static reads:
vi.spyOn(framework.modules.roles, 'activateClaimableRoleAssignment').mockResolvedValue({
id: 'activation-id',
});RolesMockConfigurator retains requireAccessRoles and setClient. Supply a custom client throughsetClient only when the test needs full control of the client lifecycle.
Real client with generated HTTP responses
Use the normal Roles module when the test needs to cover RolesClient request paths, account
resolution, response validation, or caching. The Fusion OpenAPI mock server includes the rolesv2
contract and generates responses for its operations:
pnpm exec fusion-mock --preset=fusion --port 4010Point the test framework's service discovery endpoint athttp://localhost:4010/@fusion-mock/discovery, then enable Roles normally:
import { enableRoles } from '@equinor/fusion-framework-module-roles';
enableRoles(configurator);The normal RolesModuleConfigurator resolves rolesv2 from that discovery response and creates the
real RolesClient; only the HTTP backend is mocked. Use the mock server control API to override an
operation for an error or edge case, and reset it between tests. See@equinor/fusion-openapi-mock-server for server lifecycle and override APIs.