Skip to content

Commit

Permalink
refactor: made sync service abstract for any type
Browse files Browse the repository at this point in the history
  • Loading branch information
JustSamuel committed Nov 20, 2024
1 parent 21f277f commit 24c9f3c
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 157 deletions.
21 changes: 15 additions & 6 deletions src/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ import RoleManager from './rbac/role-manager';
import Gewis from './gewis/gewis';
import EventService from './service/event-service';
import DefaultRoles from './rbac/default-roles';
import { SyncService } from './service/sync/sync-service';
import LdapSyncService from './service/sync/ldap-sync-service';
import LdapSyncService from './service/sync/user/ldap-sync-service';
import { UserSyncService } from './service/sync/user/user-sync-service';
import UserSyncManager from './service/sync/user/user-sync-manager';

class CronApplication {
logger: Logger;
Expand Down Expand Up @@ -101,7 +102,7 @@ async function createCronTasks(): Promise<void> {
// INJECT GEWIS BINDINGS
Gewis.overwriteBindings();

const syncServices: SyncService[] = [];
const syncServices: UserSyncService[] = [];

if (process.env.ENABLE_LDAP === 'true') {
const ldapSyncService = new LdapSyncService(application.roleManager, null, AppDataSource.manager);
Expand All @@ -119,9 +120,17 @@ async function createCronTasks(): Promise<void> {
// application.tasks.push(syncGewis);
// }

// TODO sensible cron schedule
// const runSyncer = cron.schedule('*/10 * * * *', async () => {
// }
if (syncServices.length !== 0) {
const syncManager = new UserSyncManager(syncServices);

// TODO sensible cron schedule
const userSyncer = cron.schedule('*/10 * * * *', async () => {
logger.debug('Syncing users.');
await syncManager.run();
await syncManager.fetch();
});
application.tasks.push(userSyncer);
}

application.logger.info('Tasks registered');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,60 +18,58 @@
* @license
*/

import User from '../../entity/user/user';
import WithManager from '../../database/with-manager';
import { SyncResult, SyncService } from './sync-service';
import { In } from 'typeorm';
import log4js, { Logger } from 'log4js';

export default class UserSyncService extends WithManager {
export default abstract class SyncManager<T, S extends SyncService<T>> extends WithManager {

private readonly services: SyncService[];
protected readonly services: S[];

private logger: Logger = log4js.getLogger('UserSync');
protected logger: Logger = log4js.getLogger('SyncManager');

constructor(services: SyncService[]) {
constructor(services: S[]) {
super();
this.logger.level = process.env.LOG_LEVEL;
this.services = services;
}

async syncUsers() {
const userTypes = this.services.flatMap((s) => s.targets);
this.logger.trace('Syncing users of types', userTypes);
await this.pre();
abstract getTargets(): Promise<T[]>;

async run() {
this.logger.trace('Start sync job');
const entities = await this.getTargets();

const users = await this.manager.find(User, { where: { type: In(userTypes) } });
for (const user of users) {
await this.pre();
for (const entity of entities) {
try {
const result = await this.sync(user);
const result = await this.sync(entity);

if (result.skipped) {
this.logger.trace('Syncing skipped for user', user.id, user.firstName, user.type);
this.logger.trace('Syncing skipped for', entity);
continue;
}

if (result.result === false) {
this.logger.warn('Sync result: false for user', user.id);
await this.down(user);
this.logger.warn('Sync result: false for', entity);
await this.down(entity);
} else {
this.logger.trace('Sync result: true for user', user.id);
this.logger.trace('Sync result: true for', entity);
}

} catch (error) {
this.logger.error('Syncing error for user', user.id, error);
this.logger.error('Syncing error for', entities, error);
}
}

await this.post();
}

async sync(user: User): Promise<SyncResult> {
async sync(entity: T): Promise<SyncResult> {
const syncResult: SyncResult = { skipped: true, result: false };

// Aggregate results from all services
for (const service of this.services) {
const result = await service.up(user);
const result = await service.up(entity);

if (!result.skipped) syncResult.skipped = false;
if (result.result) syncResult.result = true;
Expand All @@ -80,12 +78,12 @@ export default class UserSyncService extends WithManager {
return syncResult;
}

async down(user: User): Promise<void> {
async down(entity: T): Promise<void> {
for (const service of this.services) {
try {
await service.down(user);
await service.down(entity);
} catch (error) {
this.logger.error('Could not down user', user.id);
this.logger.error('Could not down', entity);
}
}
}
Expand Down
44 changes: 17 additions & 27 deletions src/service/sync/sync-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
/**
* This is the module page of the abstract sync-service.
*
* @module internal/user-sync
* @module internal/sync-service
*/

import User, { UserType } from '../../entity/user/user';
import WithManager from '../../database/with-manager';

export interface SyncResult {
Expand All @@ -35,53 +34,44 @@ export interface SyncResult {
/**
* SyncService interface.
*
* SyncService is the abstract class which is used to sync user data.
* SyncService is the abstract class which is used to sync entity data.
* This can be used to integrate external data sources into the SudoSOS back-end.
*/
export abstract class SyncService extends WithManager {
export abstract class SyncService<T> extends WithManager {

/**
* Targets is the list of user types that this sync service is responsible for.
*
* Used to improve performance by only syncing the relevant user types.
*/
targets: UserType[];

/**
* Guard determines whether the user should be synced using this sync service.
* Guard determines whether the entity should be synced using this sync service.
*
* Not passing the guard will result in the user being skipped.
* A skipped sync does not count as a failure.
*
* @param user The user to check.
* @returns {Promise<boolean>} True if the user should be synced, false otherwise.
* @param entity The entity to check.
* @returns {Promise<boolean>} True if the entity should be synced, false otherwise.
*/
protected guard(user: User): Promise<boolean> {
return Promise.resolve(this.targets.includes(user.type));
}
abstract guard(entity: T): Promise<boolean>;

/**
* Up is a wrapper around `sync` that handles the guard.
*
* @param user
* @param entity
*
* @returns {Promise<SyncResult>} The result of the sync.
*/
async up(user: User): Promise<SyncResult> {
const guardResult = await this.guard(user);
async up(entity: T): Promise<SyncResult> {
const guardResult = await this.guard(entity);
if (!guardResult) return { skipped: true, result: false };

const result = await this.sync(user);
const result = await this.sync(entity);
return { skipped: false, result };
}

/**
* Synchronizes the user data with the external data source.
*
* @param user The user to synchronize.
* @param entity The user to synchronize.
* @returns {Promise<boolean>} True if the user was synchronized, false otherwise.
*/
protected abstract sync(user: User): Promise<boolean>;
protected abstract sync(entity: T): Promise<boolean>;

/**
* Fetches the user data from the external data source.
Expand All @@ -91,14 +81,14 @@ export abstract class SyncService extends WithManager {
abstract fetch(): Promise<void>;

/**
* Down is called when the SyncService decides that the user is no longer connected to this sync service be removed.
* This can be used to remove the user from the database or clean up entities.
* Down is called when the SyncService decides that the entity is no longer connected to this sync service be removed.
* This can be used to remove the entity from the database or clean up entities.
*
* This should be revertible and idempotent!
*
* @param user
* @param entity
*/
abstract down(user: User): Promise<void>;
abstract down(entity: T): Promise<void>;

/**
* Called before a sync batch is started.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,21 @@
/**
* This is the module page of the ldap-sync-service.
*
* @module internal/user-sync
* @module internal/ldap-sync-service
*/

import { SyncService } from './sync-service';
import User, { TermsOfServiceStatus, UserType } from '../../entity/user/user';
import User, { TermsOfServiceStatus, UserType } from '../../../entity/user/user';
import { Client } from 'ldapts';
import ADService from '../ad-service';
import LDAPAuthenticator from '../../entity/authenticator/ldap-authenticator';
import RoleManager from '../../rbac/role-manager';
import ADService from '../../ad-service';
import LDAPAuthenticator from '../../../entity/authenticator/ldap-authenticator';
import RoleManager from '../../../rbac/role-manager';
import { EntityManager } from 'typeorm';
import { getLDAPConnection, LDAPGroup, LDAPUser } from '../../helpers/ad';
import RBACService from '../rbac-service';
import { getLDAPConnection, LDAPGroup, LDAPUser } from '../../../helpers/ad';
import RBACService from '../../rbac-service';
import log4js, { Logger } from 'log4js';
import { UserSyncService } from './user-sync-service';

export default class LdapSyncService extends SyncService {
export default class LdapSyncService extends UserSyncService {

// We only sync organs, members and integrations.
targets = [UserType.ORGAN, UserType.MEMBER, UserType.INTEGRATION];
Expand Down
36 changes: 36 additions & 0 deletions src/service/sync/user/user-sync-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* SudoSOS back-end API service.
* Copyright (C) 2024 Study association GEWIS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @license
*/

import User from '../../../entity/user/user';
import { In } from 'typeorm';
import log4js, { Logger } from 'log4js';
import SyncManager from '../sync-manager';
import { UserSyncService } from './user-sync-service';

export default class UserSyncManager extends SyncManager<User, UserSyncService> {

protected logger: Logger = log4js.getLogger('UserSyncManager');

async getTargets(): Promise<User[]> {
const userTypes = this.services.flatMap((s) => s.targets);
this.logger.trace('Syncing users of types', userTypes);
return this.manager.find(User, { where: { type: In(userTypes) } });
}
}
42 changes: 42 additions & 0 deletions src/service/sync/user/user-sync-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* SudoSOS back-end API service.
* Copyright (C) 2024 Study association GEWIS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @license
*/

/**
* This is the module page of the abstract sync-service.
*
* @module internal/user-user-service
*/

import User, { UserType } from '../../../entity/user/user';
import { SyncService } from '../sync-service';

/**
* UserSyncService interface.
*
* Specific sync service for users.
*/
export abstract class UserSyncService extends SyncService<User> {

targets: UserType[];

guard(user: User): Promise<boolean> {
return Promise.resolve(this.targets.includes(user.type));
}
}
Loading

0 comments on commit 24c9f3c

Please sign in to comment.