Custom site admin filter
SA can only see his data or user data where he is SA
Refactor remove entity properties: require filters
The removal of properties should be done at the end of auth checks
Common utils function to remove properties
MODIFY
src/authorization/AuthorizationsDefinition.ts
src/authorization/AuthorizationsDefinition.ts
@@ -406,6 +406,17 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
Action.CREATE, Action.READ, Action.REMOTE_START_TRANSACTION, Action.REMOTE_STOP_TRANSACTION, Action.UNLOCK_CONNECTOR, Action.EXPORT, Action.RESERVE_NOW,
]
},
+ {
+ resource: Entity.CHARGING_STATION,
+ action: [Action.VIEW_USER_DATA],
+ condition: {
+ Fn: 'custom:dynamicAuthorizations',
+ args: {
+ asserts: [],
+ filters: []
+ }
+ },
+ },
{
resource: Entity.CONNECTOR,
action: [Action.STOP_TRANSACTION, Action.START_TRANSACTION],
@@ -428,6 +439,17 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
}
},
},
+ {
+ resource: Entity.CONNECTOR,
+ action: [Action.VIEW_USER_DATA],
+ condition: {
+ Fn: 'custom:dynamicAuthorizations',
+ args: {
+ asserts: [],
+ filters: []
+ }
+ },
+ },
{ resource: Entity.TRANSACTION, action: [Action.LIST, Action.EXPORT, Action.IN_ERROR] },
{
resource: Entity.TRANSACTION,
@@ -992,6 +1014,17 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
}
},
},
+ {
+ resource: Entity.CHARGING_STATION,
+ action: [Action.VIEW_USER_DATA],
+ condition: {
+ Fn: 'custom:dynamicAuthorizations',
+ args: {
+ asserts: [],
+ filters: ['OwnUser']
+ }
+ },
+ },
{
resource: Entity.CONNECTOR,
action: Action.START_TRANSACTION,
@@ -1014,6 +1047,17 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
}
},
},
+ {
+ resource: Entity.CONNECTOR,
+ action: [Action.VIEW_USER_DATA],
+ condition: {
+ Fn: 'custom:dynamicAuthorizations',
+ args: {
+ asserts: [],
+ filters: ['OwnUser']
+ }
+ },
+ },
{
resource: Entity.CONNECTOR, action: Action.REMOTE_START_TRANSACTION,
condition: {
@@ -1649,6 +1693,16 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
}
}
},
+ {
+ resource: Entity.CHARGING_STATION, action: [Action.VIEW_USER_DATA],
+ condition: {
+ Fn: 'custom:dynamicAuthorizations',
+ args: {
+ asserts: [],
+ filters: ['SitesAdminUsers']
+ }
+ },
+ },
{
resource: Entity.CONNECTOR,
action: [Action.REMOTE_STOP_TRANSACTION, Action.REMOTE_START_TRANSACTION],
@@ -1670,6 +1724,16 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
}
},
},
+ {
+ resource: Entity.CONNECTOR, action: [Action.VIEW_USER_DATA],
+ condition: {
+ Fn: 'custom:dynamicAuthorizations',
+ args: {
+ asserts: [],
+ filters: ['SitesAdminUsers']
+ }
+ },
+ },
{
resource: Entity.CHARGING_PROFILE, action: Action.LIST,
condition: {

MODIFY
src/authorization/DynamicAuthorizationFactory.ts
src/authorization/DynamicAuthorizationFactory.ts
@@ -18,6 +18,8 @@ import PoolCarDynamicAuthorizationAssert from './dynamic-assert/PoolCarDynamicAu
import SiteAreaMandatoryDynamicAuthorizationAssert from './dynamic-assert/SiteAreaMandatoryDynamicAuthorizationAssert';
import SitesAdminDynamicAuthorizationDataSource from './dynamic-data-source/SitesAdminDynamicAuthorizationDataSource';
import SitesAdminDynamicAuthorizationFilter from './dynamic-filters/SitesAdminDynamicAuthorizationFilter';
+import SitesAdminUsersDynamicAuthorizationDataSource from './dynamic-data-source/SitesAdminUsersDynamicAuthorizationDataSource';
+import SitesAdminUsersDynamicAuthorizationFilter from './dynamic-filters/SitesAdminUsersDynamicAuthorizationFilter';
import SitesOwnerDynamicAuthorizationDataSource from './dynamic-data-source/SitesOwnerDynamicAuthorizationDataSource';
import SitesOwnerDynamicAuthorizationFilter from './dynamic-filters/SitesOwnerDynamicAuthorizationFilter';
import Tenant from '../types/Tenant';
@@ -44,6 +46,9 @@ export default class DynamicAuthorizationFactory {
case DynamicAuthorizationFilterName.SITES_ADMIN:
dynamicFilter = new SitesAdminDynamicAuthorizationFilter(tenant, userToken, negateFilter);
break;
+ case DynamicAuthorizationFilterName.SITES_ADMIN_USERS:
+ dynamicFilter = new SitesAdminUsersDynamicAuthorizationFilter(tenant, userToken, negateFilter);
+ break;
case DynamicAuthorizationFilterName.SITES_OWNER:
dynamicFilter = new SitesOwnerDynamicAuthorizationFilter(tenant, userToken, negateFilter);
break;
@@ -106,6 +111,9 @@ export default class DynamicAuthorizationFactory {
case DynamicAuthorizationDataSourceName.SITES_ADMIN:
dataSource = new SitesAdminDynamicAuthorizationDataSource(tenant, user);
break;
+ case DynamicAuthorizationDataSourceName.SITES_ADMIN_USERS:
+ dataSource = new SitesAdminUsersDynamicAuthorizationDataSource(tenant, user);
+ break;
case DynamicAuthorizationDataSourceName.SITES_OWNER:
dataSource = new SitesOwnerDynamicAuthorizationDataSource(tenant, user);
break;

ADD
src/authorization/dynamic-data-source/SitesAdminUsersDynamicAuthorizationDataSource.ts
+import { DynamicAuthorizationDataSourceName, SitesAdminUsersDynamicAuthorizationDataSourceData } from '../../types/Authorization';
+
+import Constants from '../../utils/Constants';
+import DynamicAuthorizationDataSource from '../DynamicAuthorizationDataSource';
+import UserStorage from '../../storage/mongodb/UserStorage';
+
+export default class SitesAdminUsersDynamicAuthorizationDataSource
+ extends DynamicAuthorizationDataSource<SitesAdminUsersDynamicAuthorizationDataSourceData> {
+
+ public getName(): DynamicAuthorizationDataSourceName {
+ return DynamicAuthorizationDataSourceName.SITES_ADMIN_USERS;
+ }
+
+ public async loadData(): Promise<void> {
+ const sitesAdminData: SitesAdminUsersDynamicAuthorizationDataSourceData = {};
+ // Get Site and user ID(s)
+ const data = await this.getSitesAdminUserSiteIDs();
+ // Set
+ sitesAdminData.siteIDs = data.siteIDs;
+ sitesAdminData.userID = data.userID;
+ this.setData(sitesAdminData);
+ }
+
+ private async getSitesAdminUserSiteIDs(): Promise<{siteIDs: string[], userID: string}> {
+ // Get the Site IDs of the Sites for which the user is Site Admin
+ const sites = await UserStorage.getUserSites(this.tenant,
+ {
+ userIDs: [this.userToken.id],
+ siteAdmin: true
+ }, Constants.DB_PARAMS_MAX_LIMIT,
+ ['siteID']
+ );
+ return { siteIDs: sites.result.map((userSite) => userSite.siteID), userID: this.userToken.id };
+ }
+}

MODIFY
src/authorization/dynamic-filters/OwnUserDynamicAuthorizationFilter.ts
src/authorization/dynamic-filters/OwnUserDynamicAuthorizationFilter.ts
@@ -28,10 +28,9 @@ export default class OwnUserDynamicAuthorizationFilter extends DynamicAuthorizat
if (!Utils.isEmptyArray(authorizationFilters.filters.userIDs)) {
authorizationFilters.authorized = true;
}
- // Delete user property in entity in case we are not authorized
+ // Remove sensible data if not authorized and filter is provided
if (!authorizationFilters.authorized) {
- Utils.deleteUserPropertiesFromEntity(entityData);
- Utils.deleteTagPropertiesFromEntity(entityData);
+ Utils.removeSensibeDataFromEntity(extraFilters, entityData);
}
}

MODIFY
src/authorization/dynamic-filters/SitesAdminDynamicAuthorizationFilter.ts
src/authorization/dynamic-filters/SitesAdminDynamicAuthorizationFilter.ts
@@ -34,6 +34,10 @@ export default class SitesAdminDynamicAuthorizationFilter extends DynamicAuthori
authorizationFilters.authorized = true;
}
}
+ // Remove sensible data if not authorized and filter is provided
+ if (!authorizationFilters.authorized) {
+ Utils.removeSensibeDataFromEntity(extraFilters, entityData);
+ }
}
public getApplicableEntities(): Entity[] {

ADD
src/authorization/dynamic-filters/SitesAdminUsersDynamicAuthorizationFilter.ts
+import { AuthorizationFilter, DynamicAuthorizationDataSourceName, Entity } from '../../types/Authorization';
+
+import DynamicAuthorizationFilter from '../DynamicAuthorizationFilter';
+import { EntityData } from '../../types/GlobalType';
+import SitesAdminUsersDynamicAuthorizationDataSource from '../dynamic-data-source/SitesAdminUsersDynamicAuthorizationDataSource';
+import { TenantComponents } from '../../types/Tenant';
+import Utils from '../../utils/Utils';
+
+export default class SitesAdminUsersDynamicAuthorizationFilter extends DynamicAuthorizationFilter {
+ public processFilter(authorizationFilters: AuthorizationFilter, extraFilters: Record<string, any>, entityData?: EntityData): void {
+ // Retrieve site ids where user is site admin and logged user id
+ const sitesAdminUsersDataSource = this.getDataSource(
+ DynamicAuthorizationDataSourceName.SITES_ADMIN_USERS) as SitesAdminUsersDynamicAuthorizationDataSource;
+ const { siteIDs, userID } = sitesAdminUsersDataSource.getData();
+ // Perform site ids check only if organization component is active
+ if (Utils.isTenantComponentActive(this.tenant, TenantComponents.ORGANIZATION)) {
+ // Init user site IDs
+ let userSiteIds = [];
+ if (!Utils.isEmptyArray(siteIDs)) {
+ userSiteIds = siteIDs;
+ // Check if filter is provided
+ if (Utils.objectHasProperty(extraFilters, 'SiteID') &&
+ !Utils.isNullOrUndefined(extraFilters['SiteID'])) {
+ const filteredSiteIDs: string[] = extraFilters['SiteID'].split('|');
+ // Override
+ userSiteIds = filteredSiteIDs.filter(
+ (siteID) => userSiteIds.includes(siteID));
+ }
+ }
+ if (!Utils.isEmptyArray(userSiteIds)) {
+ authorizationFilters.authorized = true;
+ }
+ }
+ // Check user filter
+ if (!authorizationFilters.authorized) {
+ let siteAdminUserId = [userID];
+ if (userID) {
+ // Check if filter is provided
+ if (Utils.objectHasProperty(extraFilters, 'UserID') &&
+ !Utils.isNullOrUndefined(extraFilters['UserID'])) {
+ const filteredUserIDs: string[] = extraFilters['UserID'].split('|');
+ // Override
+ siteAdminUserId = filteredUserIDs.filter(
+ (user) => siteAdminUserId.includes(user));
+ }
+ }
+ if (!Utils.isEmptyArray(siteAdminUserId)) {
+ authorizationFilters.authorized = true;
+ }
+ }
+ // Remove sensible data if not authorized and filter is provided
+ if (!authorizationFilters.authorized) {
+ Utils.removeSensibeDataFromEntity(extraFilters, entityData);
+ }
+ }
+
+ public getApplicableEntities(): Entity[] {
+ return [
+ Entity.CHARGING_STATION,
+ Entity.CONNECTOR
+ ];
+ }
+
+ public getApplicableDataSources(): DynamicAuthorizationDataSourceName[] {
+ return [
+ DynamicAuthorizationDataSourceName.SITES_ADMIN_USERS
+ ];
+ }
+}

MODIFY
src/server/rest/v1/service/AuthorizationService.ts
src/server/rest/v1/service/AuthorizationService.ts
@@ -780,9 +780,18 @@ export default class AuthorizationService {
ChargePointStatus.FAULTED,
ChargePointStatus.SUSPENDED_EVSE,
].includes(connector.status);
+ // Remove sensible data
+ await AuthorizationService.canPerformAuthorizationAction(
+ tenant, userToken, Entity.CONNECTOR, Action.VIEW_USER_DATA, authorizationFilter,
+ { chargingStationID: chargingStation.id, UserID: connector.user?.id, SiteID: chargingStation.siteID, UserData: true, TagData: true }, connector);
+ // Remove properties
Utils.removeCanPropertiesWithFalseValue(connector);
}
- // Optimize data over the net
+ // Remove sensible data
+ await AuthorizationService.canPerformAuthorizationAction(
+ tenant, userToken, Entity.CHARGING_STATION, Action.VIEW_USER_DATA, authorizationFilter,
+ { chargingStationID: chargingStation.id, UserID: userToken.user.id, SiteID: chargingStation.siteID, UserData: true, TagData: true }, chargingStation);
+ // Remove properties
Utils.removeCanPropertiesWithFalseValue(chargingStation);
}

MODIFY
src/types/Authorization.ts
src/types/Authorization.ts
@@ -174,6 +174,7 @@ export enum Action {
GET_OCPP_PARAMS = 'GetOCPPParams',
UPDATE_CHARGING_PROFILE = 'UpdateChargingProfile',
GET_CONNECTOR_QR_CODE = 'GetConnectorQRCode',
+ VIEW_USER_DATA = 'ViewUserData',
}
export interface AuthorizationContext {
@@ -296,6 +297,7 @@ export interface BillingTransferAuthorizationActions extends AuthorizationAction
export enum DynamicAuthorizationFilterName {
ASSIGNED_SITES_COMPANIES = 'AssignedSitesCompanies',
SITES_ADMIN = 'SitesAdmin',
+ SITES_ADMIN_USERS = 'SitesAdminUsers',
SITES_OWNER = 'SitesOwner',
ASSIGNED_SITES = 'AssignedSites',
OWN_USER = 'OwnUser',
@@ -314,6 +316,7 @@ export enum DynamicAuthorizationAssertName {
export enum DynamicAuthorizationDataSourceName {
ASSIGNED_SITES_COMPANIES = 'AssignedSitesCompanies',
SITES_ADMIN = 'SitesAdmin',
+ SITES_ADMIN_USERS = 'SitesAdminUsers',
SITES_OWNER = 'SitesOwner',
ASSIGNED_SITES = 'AssignedSites',
OWN_USER = 'OwnUser',
@@ -330,6 +333,10 @@ export interface SitesAdminDynamicAuthorizationDataSourceData extends DynamicAut
siteIDs?: string[];
}
+export interface SitesAdminUsersDynamicAuthorizationDataSourceData extends DynamicAuthorizationDataSourceData {
+ siteIDs?: string[];
+ userID?: string;
+}
export interface SitesOwnerDynamicAuthorizationDataSourceData extends DynamicAuthorizationDataSourceData {
siteIDs?: string[];
}

MODIFY
src/utils/Utils.ts
src/utils/Utils.ts
@@ -1741,11 +1741,24 @@ export default class Utils {
}
}
- public static deleteUserPropertiesFromEntity(entityData?: EntityData): void {
+ public static removeSensibeDataFromEntity(extraFilters: Record<string, any>, entityData?: EntityData): void {
+ // User data
+ if (Utils.objectHasProperty(extraFilters, 'UserData') &&
+ !Utils.isNullOrUndefined(extraFilters['UserData']) && extraFilters['UserData']) {
+ Utils.deleteUserPropertiesFromEntity(entityData);
+ }
+ // Tag data
+ if (Utils.objectHasProperty(extraFilters, 'TagData') &&
+ !Utils.isNullOrUndefined(extraFilters['TagData']) && extraFilters['TagData']) {
+ Utils.deleteTagPropertiesFromEntity(entityData);
+ }
+ }
+
+ private static deleteUserPropertiesFromEntity(entityData?: EntityData): void {
Utils.deletePropertiesFromEntity(entityData, ['user']);
}
- public static deleteTagPropertiesFromEntity(entityData?: EntityData): void {
+ private static deleteTagPropertiesFromEntity(entityData?: EntityData): void {
Utils.deletePropertiesFromEntity(entityData, ['tag', 'currentTagID']);
}

Custom site admin filter (#3698)

- SA can only see his data or user data where he is SA
- Refactor remove entity properties: require filters
- The removal of properties should be done at the end of auth checks
- Common utils function to remove properties