Skip to content

Commit bd73eff

Browse files
authored
Feat/smtp notification (#47)
* feat: smtp notification * chore: update dependencies
1 parent 5a828e1 commit bd73eff

19 files changed

+2424
-1560
lines changed

package-lock.json

Lines changed: 2009 additions & 1481 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,37 @@
77
"authtool": "./bin/run"
88
},
99
"dependencies": {
10-
"@oclif/core": "^4.0.17",
11-
"@oclif/plugin-help": "^6.2.8",
12-
"@oclif/plugin-plugins": "^5.4.4",
13-
"axios": "^1.7.4",
14-
"inversify": "^6.0.2",
10+
"@oclif/core": "^4.0.30",
11+
"@oclif/plugin-help": "^6.2.16",
12+
"@oclif/plugin-plugins": "^5.4.15",
13+
"axios": "^1.7.7",
14+
"inversify": "^6.0.3",
15+
"nodemailer": "^6.9.15",
1516
"reflect-metadata": "^0.2.2",
1617
"rxjs": "^7.8.1"
1718
},
1819
"devDependencies": {
19-
"@oclif/test": "^4.0.8",
20-
"@types/chai": "^4.3.17",
20+
"@oclif/test": "^4.1.0",
21+
"@types/chai": "file:@types/chai",
2122
"@types/ejs": "^3.1.5",
2223
"@types/eslint__js": "^8.42.3",
23-
"@types/mocha": "^10.0.7",
24-
"@types/node": "^22.4.1",
25-
"@typescript-eslint/eslint-plugin": "^8.2.0",
26-
"@typescript-eslint/parser": "^8.2.0",
27-
"chai": "^5.1.1",
28-
"eslint": "^9.9.0",
24+
"@types/mocha": "^10.0.9",
25+
"@types/node": "^22.8.0",
26+
"@types/nodemailer": "^6.4.16",
27+
"@typescript-eslint/eslint-plugin": "^8.11.0",
28+
"@typescript-eslint/parser": "^8.11.0",
29+
"chai": "^5.1.2",
30+
"eslint": "^9.13.0",
2931
"eslint-config-prettier": "^9.1.0",
3032
"eslint-plugin-prettier": "^5.2.1",
3133
"jest": "^29.7.0",
3234
"mocha": "^10.7.3",
33-
"oclif": "^4.14.22",
35+
"oclif": "^4.15.12",
3436
"shx": "^0.3.3",
3537
"ts-node": "^10.9.2",
36-
"tslib": "^2.6.3",
37-
"typescript": "^5.5.4",
38-
"typescript-eslint": "^8.2.0"
38+
"tslib": "^2.8.0",
39+
"typescript": "^5.6.3",
40+
"typescript-eslint": "^8.11.0"
3941
},
4042
"engines": {
4143
"node": ">=8.0.0"

src/commands/member-sync.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ import {
1111
brokerToken,
1212
brokerApiUrl,
1313
sourceBrokerIdp,
14+
notificationOptionFrom,
15+
notificationOptionSubject,
16+
notificationOptionTemplateHtml,
17+
notificationOptionTemplateText,
18+
notificationSmtpHost,
19+
notificationSmtpPort,
20+
notificationSmtpSecure,
1421
} from '../flags';
1522
import { TYPES } from '../inversify.types';
1623
import {
1724
bindBroker,
1825
bindConstants,
26+
bindNotification,
1927
bindTarget,
2028
vsContainer,
2129
} from '../inversify.config';
@@ -37,6 +45,13 @@ export default class MemberSync extends Command {
3745
...cssTokenUrl,
3846
...cssClientId,
3947
...cssClientSecret,
48+
...notificationSmtpHost,
49+
...notificationSmtpPort,
50+
...notificationSmtpSecure,
51+
...notificationOptionFrom,
52+
...notificationOptionSubject,
53+
...notificationOptionTemplateText,
54+
...notificationOptionTemplateHtml,
4055
...sourceBrokerIdp,
4156
};
4257

@@ -48,6 +63,19 @@ export default class MemberSync extends Command {
4863

4964
bindConstants(flags['config-path'], flags['source-broker-idp']);
5065
bindBroker(flags['broker-api-url'], flags['broker-token']);
66+
bindNotification(
67+
{
68+
host: flags['notification-smtp-host'],
69+
port: flags['notification-smtp-port'],
70+
secure: flags['notification-smtp-secure'],
71+
},
72+
{
73+
from: flags['notification-option-from'],
74+
subject: flags['notification-option-subject'],
75+
text: flags['notification-option-template-text'],
76+
html: flags['notification-option-template-html'],
77+
},
78+
);
5179
await bindTarget(
5280
flags['css-token-url'],
5381
flags['css-client-id'],

src/commands/monitor.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@ import {
88
cssClientSecret,
99
cssTokenUrl,
1010
help,
11+
notificationOptionFrom,
12+
notificationOptionSubject,
13+
notificationOptionTemplateHtml,
14+
notificationOptionTemplateText,
15+
notificationSmtpHost,
16+
notificationSmtpPort,
17+
notificationSmtpSecure,
1118
sourceBrokerIdp,
1219
} from '../flags';
1320
import {
1421
bindBroker,
1522
bindConstants,
23+
bindNotification,
1624
bindTarget,
1725
vsContainer,
1826
} from '../inversify.config';
@@ -33,6 +41,13 @@ export default class Monitor extends Command {
3341
...cssTokenUrl,
3442
...cssClientId,
3543
...cssClientSecret,
44+
...notificationSmtpHost,
45+
...notificationSmtpPort,
46+
...notificationSmtpSecure,
47+
...notificationOptionFrom,
48+
...notificationOptionSubject,
49+
...notificationOptionTemplateText,
50+
...notificationOptionTemplateHtml,
3651
...sourceBrokerIdp,
3752
};
3853

@@ -46,6 +61,19 @@ export default class Monitor extends Command {
4661

4762
bindConstants(flags['config-path'], flags['source-broker-idp']);
4863
bindBroker(flags['broker-api-url'], flags['broker-token']);
64+
bindNotification(
65+
{
66+
host: notificationSmtpHost,
67+
port: notificationSmtpPort,
68+
secure: notificationSmtpSecure,
69+
},
70+
{
71+
from: notificationOptionFrom,
72+
subject: notificationOptionSubject,
73+
text: notificationOptionTemplateText,
74+
html: notificationOptionTemplateHtml,
75+
},
76+
);
4977
await bindTarget(
5078
flags['css-token-url'],
5179
flags['css-client-id'],

src/controller/auth-member-sync.controller.ts

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { inject, injectable, multiInject } from 'inversify';
2+
import { getLogger } from '@oclif/core';
23

34
import { TYPES } from '../inversify.types';
45
import { SourceService, SourceUser } from '../services/source.service';
5-
import { IntegrationConfig, RoleConfig } from '../types';
6+
import { IntegrationConfig, RoleConfig, UserSummary } from '../types';
67
import { TargetService } from '../services/target.service';
8+
import { SmtpNotificationService } from '../notification/smtp-notification.service';
9+
import { roleFromConfig } from '../util/role.util';
710

811
type OutletMap = Map<string, Map<string, SourceUser>>;
912

@@ -12,43 +15,67 @@ type OutletMap = Map<string, Map<string, SourceUser>>;
1215
* Css sync controller
1316
*/
1417
export class AuthMemberSyncController {
18+
private readonly console = getLogger('AuthMemberSyncController');
1519
/**
1620
* Constructor
1721
*/
1822
constructor(
1923
@multiInject(TYPES.SourceService) private sourceServices: SourceService[],
2024
@inject(TYPES.TargetService) private targetService: TargetService,
25+
@inject(TYPES.SmtpNotificationService)
26+
private notificationService: SmtpNotificationService,
2127
) {}
2228

2329
public async sync(integrationConfigs: IntegrationConfig[]) {
2430
const sdate = new Date();
2531
const userMap: { [key in string]: OutletMap } = {};
2632
for (const integrationConfig of integrationConfigs) {
2733
const idp = integrationConfig.idp ?? 'idir';
28-
console.log(`>>> ${integrationConfig.name} : Get users`);
34+
this.console.info(`>>> ${integrationConfig.name} : Get users`);
2935
userMap[integrationConfig.name] = await this.integrationMemberSync(
3036
idp,
3137
integrationConfig.roles,
3238
);
3339

3440
for (const environment of integrationConfig.environments) {
3541
const sEnvDate = new Date();
36-
console.log(`>>> ${integrationConfig.name} - ${environment}: start`);
37-
await this.syncIntegrationRoleUsers(
38-
integrationConfig,
39-
environment,
40-
userMap[integrationConfig.name],
41-
idp,
42+
this.console.info(
43+
`>>> ${integrationConfig.name} - ${environment}: start`,
4244
);
45+
// const summaryMap = await this.syncIntegrationRoleUsers(
46+
// integrationConfig,
47+
// environment,
48+
// userMap[integrationConfig.name],
49+
// idp,
50+
// );
51+
const summaryMap = new Map<string, UserSummary>();
52+
summaryMap.set(
53+
'483CFF50E3E94A22BDB082B56DE564B6',
54+
new UserSummary({
55+
guid: '483CFF50E3E94A22BDB082B56DE564B6',
56+
domain: 'azureidir',
57+
email: 'matthew.bystedt@gov.bc.ca',
58+
name: 'Bystedt, Matthew WLRS:EX',
59+
}),
60+
);
61+
summaryMap
62+
.get('483CFF50E3E94A22BDB082B56DE564B6')
63+
?.addRoles.push('group_vault-user');
64+
console.log(summaryMap);
65+
66+
this.notificationService.notifyUsers(integrationConfig, [
67+
...summaryMap.values(),
68+
]);
69+
4370
const eEnvDate = new Date();
44-
console.log(
71+
this.console.info(
4572
`>>> ${integrationConfig.name} - ${environment}: done - ${eEnvDate.getTime() - sEnvDate.getTime()} ms`,
4673
);
4774
}
4875
}
4976
const edate = new Date();
5077

51-
console.log(`Done - ${edate.getTime() - sdate.getTime()} ms`);
78+
this.console.info(`Done - ${edate.getTime() - sdate.getTime()} ms`);
5279
}
5380

5481
private async syncIntegrationRoleUsers(
@@ -57,8 +84,9 @@ export class AuthMemberSyncController {
5784
userRoles: OutletMap,
5885
idp: string,
5986
) {
87+
const userSummary = new Map<string, UserSummary>();
6088
for (const [roleName, roleUserGuidMap] of userRoles.entries()) {
61-
console.log(`${integrationConfig.id} ${environment} ${roleName}`);
89+
this.console.info(`${integrationConfig.id} ${environment} ${roleName}`);
6290
const existingUserGuidMap = await this.targetService.getRoleUsers(
6391
integrationConfig.id,
6492
environment,
@@ -74,11 +102,11 @@ export class AuthMemberSyncController {
74102
.filter((guid) => !existingUserGuidMap.has(guid))
75103
.map((guid) => roleUserGuidMap.get(guid))
76104
.filter((user) => !!user);
77-
// console.log(`remove:`);
78-
// console.log(usersToRemove);
79-
// console.log(`add:`);
80-
// console.log(usersToAdd);
81-
await Promise.all([
105+
// this.console.info(`remove:`);
106+
// this.console.info(usersToRemove);
107+
// this.console.info(`add:`);
108+
// this.console.info(usersToAdd);
109+
const [finalizedAdd, finalizedDel] = await Promise.all([
82110
this.targetService.alterIntegrationRoleUser(
83111
integrationConfig,
84112
environment,
@@ -94,12 +122,25 @@ export class AuthMemberSyncController {
94122
usersToRemove,
95123
),
96124
]);
125+
for (const finalize of finalizedAdd) {
126+
if (!userSummary.has(finalize.guid)) {
127+
userSummary.set(finalize.guid, new UserSummary(finalize));
128+
}
129+
userSummary.get(finalize.guid)?.addRoles.push(roleName);
130+
}
131+
for (const finalize of finalizedDel) {
132+
if (!userSummary.has(finalize.guid)) {
133+
userSummary.set(finalize.guid, new UserSummary(finalize));
134+
}
135+
userSummary.get(finalize.guid)?.delRoles.push(roleName);
136+
}
97137
}
138+
return userSummary;
98139
}
99140

100141
private async integrationMemberSync(idp: string, roleConfigs: RoleConfig[]) {
101142
const roleConfigNames = roleConfigs.map((roleConfig) =>
102-
this.roleFromConfig(roleConfig),
143+
roleFromConfig(roleConfig),
103144
);
104145

105146
const outletMap = await this.addUserToRoleWithServices(roleConfigs);
@@ -174,19 +215,15 @@ export class AuthMemberSyncController {
174215
if (!outletMap.has(target)) {
175216
continue;
176217
}
177-
callback(
178-
this.roleFromConfig(roleConfig),
179-
outletMap,
180-
outletMap.get(target),
181-
);
218+
callback(roleFromConfig(roleConfig), outletMap, outletMap.get(target));
182219
}
183220
}
184221
}
185222

186223
private async addUserToRoleWithServices(roleConfigs: RoleConfig[]) {
187224
const outletMap = new Map<string, Map<string, SourceUser>>();
188225
for (const roleConfig of roleConfigs) {
189-
const role = this.roleFromConfig(roleConfig);
226+
const role = roleFromConfig(roleConfig);
190227
const users = await this.getUserMapFromServices(roleConfig);
191228
if (users.size > 0) {
192229
outletMap.set(role, users);
@@ -203,12 +240,4 @@ export class AuthMemberSyncController {
203240
}
204241
return userMap;
205242
}
206-
207-
private roleFromConfig(roleConfig: RoleConfig) {
208-
if (roleConfig.group) {
209-
return `${roleConfig.group}_${roleConfig.name}`;
210-
} else {
211-
return roleConfig.name;
212-
}
213-
}
214243
}

src/controller/auth-monitor.controller.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-empty-function */
22
import { inject, injectable } from 'inversify';
3+
import { getLogger } from '@oclif/core';
4+
35
import { TYPES } from '../inversify.types';
46
import { delay, exhaustMap, filter, interval, timer } from 'rxjs';
57
import { GenerateController } from './generate.contoller';
@@ -17,6 +19,7 @@ const MONITOR_CACHE_RESET_FULL_NTH = 4;
1719
* Auth monitor controller
1820
*/
1921
export class AuthMonitorController {
22+
private readonly console = getLogger('AuthMonitorController');
2023
/**
2124
* Constructor
2225
*/
@@ -38,17 +41,17 @@ export class AuthMonitorController {
3841
const resetAllCacheInterval$ = interval(
3942
MONITOR_CACHE_RESET_INTERVAL_MS * MONITOR_CACHE_RESET_FULL_NTH,
4043
);
41-
console.log(`>>> Monitor - start`);
44+
this.console.info(`>>> Monitor - start`);
4245

4346
// Skip every MONITOR_CACHE_RESET_FULL_NTH because it is a full reset
4447
resetCacheInterval$
4548
.pipe(filter((cnt) => cnt % MONITOR_CACHE_RESET_FULL_NTH === 0))
4649
.subscribe(() => {
47-
console.log(`---- Reset user cache`);
50+
this.console.info(`---- Reset user cache`);
4851
this.targetService.resetUserCache(false);
4952
});
5053
resetAllCacheInterval$.subscribe(() => {
51-
console.log(`---- Reset user cache (all)`);
54+
this.console.info(`---- Reset user cache (all)`);
5255
this.targetService.resetUserCache(true);
5356
});
5457

@@ -64,9 +67,9 @@ export class AuthMonitorController {
6467
await this.role.sync(integrationConfigs);
6568
await this.member.sync(integrationConfigs);
6669
}
67-
console.log(`---- sync end [${Date.now() - startMs}]`);
70+
this.console.info(`---- sync end [${Date.now() - startMs}]`);
6871
} catch (e) {
69-
console.log(`---- sync fail [Check authentication]`);
72+
this.console.info(`---- sync fail [Check authentication]`);
7073
}
7174
}),
7275
)

0 commit comments

Comments
 (0)