1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-11 05:43:26 +00:00

sync logic

This commit is contained in:
Kyle Spearrin
2018-05-02 22:00:44 -04:00
parent 3981cb95fc
commit 95d98181fc
8 changed files with 114 additions and 59 deletions

View File

@@ -71,7 +71,7 @@ const containerService = new ContainerService(cryptoService, platformUtilsServic
const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService, const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
i18nService, platformUtilsService, messagingService, false); i18nService, platformUtilsService, messagingService, false);
const configurationService = new ConfigurationService(storageService, secureStorageService); const configurationService = new ConfigurationService(storageService, secureStorageService);
const syncSevrice = new SyncService(configurationService, logService); const syncSevrice = new SyncService(configurationService, logService, cryptoFunctionService, apiService);
const analytics = new Analytics(window, () => true, platformUtilsService, storageService, appIdService); const analytics = new Analytics(window, () => true, platformUtilsService, storageService, appIdService);
containerService.attachToWindow(window); containerService.attachToWindow(window);

View File

@@ -28,7 +28,7 @@ export class DashboardComponent {
constructor(private i18nService: I18nService, private syncService: SyncService) { } constructor(private i18nService: I18nService, private syncService: SyncService) { }
async sync() { async sync() {
this.syncPromise = this.syncService.sync(false, true); this.syncPromise = this.syncService.sync(false, false);
await this.syncPromise; await this.syncPromise;
} }
@@ -41,7 +41,7 @@ export class DashboardComponent {
this.simPromise = new Promise(async (resolve, reject) => { this.simPromise = new Promise(async (resolve, reject) => {
try { try {
const result = await this.syncService.sync(!this.simSinceLast, false); const result = await this.syncService.sync(!this.simSinceLast, true);
this.simUsers = result[1]; this.simUsers = result[1];
this.simGroups = result[0]; this.simGroups = result[0];
} catch (e) { } catch (e) {
@@ -51,7 +51,7 @@ export class DashboardComponent {
const userMap = new Map<string, UserEntry>(); const userMap = new Map<string, UserEntry>();
if (this.simUsers != null) { if (this.simUsers != null) {
this.sort(this.simUsers); this.sort(this.simUsers);
this.simUsers.forEach((u) => { for (const u of this.simUsers) {
userMap.set(u.externalId, u); userMap.set(u.externalId, u);
if (u.deleted) { if (u.deleted) {
this.simDeletedUsers.push(u); this.simDeletedUsers.push(u);
@@ -60,29 +60,29 @@ export class DashboardComponent {
} else { } else {
this.simEnabledUsers.push(u); this.simEnabledUsers.push(u);
} }
}); }
} }
if (userMap.size > 0 && this.simGroups != null) { if (userMap.size > 0 && this.simGroups != null) {
this.sort(this.simGroups); this.sort(this.simGroups);
this.simGroups.forEach((g) => { for (const g of this.simGroups) {
if (g.userMemberExternalIds == null) { if (g.userMemberExternalIds == null) {
return; return;
} }
g.userMemberExternalIds.forEach((uid) => { for (const uid of g.userMemberExternalIds) {
if (userMap.has(uid)) { if (userMap.has(uid)) {
if ((g as any).users == null) { if ((g as any).users == null) {
(g as any).users = []; (g as any).users = [];
} }
(g as any).users.push(userMap.get(uid)); (g as any).users.push(userMap.get(uid));
} }
}); }
if ((g as any).users != null) { if ((g as any).users != null) {
this.sort((g as any).users); this.sort((g as any).users);
} }
}); }
} }
resolve(); resolve();
}); });

View File

@@ -28,7 +28,7 @@ export class AzureDirectoryService implements DirectoryService {
this.init(); this.init();
} }
async getEntries(force = false): Promise<[GroupEntry[], UserEntry[]]> { async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType(); const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.AzureActiveDirectory) { if (type !== DirectoryType.AzureActiveDirectory) {
return; return;
@@ -47,7 +47,7 @@ export class AzureDirectoryService implements DirectoryService {
let users: UserEntry[]; let users: UserEntry[];
if (this.syncConfig.users) { if (this.syncConfig.users) {
users = await this.getUsers(force); users = await this.getUsers(force, !test);
} }
let groups: GroupEntry[]; let groups: GroupEntry[];
@@ -57,7 +57,7 @@ export class AzureDirectoryService implements DirectoryService {
const groupForce = force || const groupForce = force ||
(users != null && users.filter((u) => !u.deleted && !u.disabled).length > 0); (users != null && users.filter((u) => !u.deleted && !u.disabled).length > 0);
groups = await this.getGroups(groupForce, setFilter); groups = await this.getGroups(groupForce, !test, setFilter);
if (setFilter != null && users != null) { if (setFilter != null && users != null) {
users = users.filter((u) => { users = users.filter((u) => {
if (u.disabled || u.deleted) { if (u.disabled || u.deleted) {
@@ -72,7 +72,7 @@ export class AzureDirectoryService implements DirectoryService {
return [groups, users]; return [groups, users];
} }
private async getUsers(force: boolean): Promise<UserEntry[]> { private async getUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
const entries: UserEntry[] = []; const entries: UserEntry[] = [];
let res: any = null; let res: any = null;
@@ -95,7 +95,7 @@ export class AzureDirectoryService implements DirectoryService {
while (true) { while (true) {
const users: graphType.User[] = res.value; const users: graphType.User[] = res.value;
if (users != null) { if (users != null) {
users.forEach((user) => { for (const user of users) {
const entry = this.buildUser(user); const entry = this.buildUser(user);
if (this.filterOutResult(filter, entry.email)) { if (this.filterOutResult(filter, entry.email)) {
return; return;
@@ -107,11 +107,11 @@ export class AzureDirectoryService implements DirectoryService {
} }
entries.push(entry); entries.push(entry);
}); }
} }
if (res[NextLink] == null) { if (res[NextLink] == null) {
if (res[DeltaLink] != null) { if (res[DeltaLink] != null && saveDelta) {
await this.configurationService.saveUserDeltaToken(res[DeltaLink]); await this.configurationService.saveUserDeltaToken(res[DeltaLink]);
} }
break; break;
@@ -138,7 +138,8 @@ export class AzureDirectoryService implements DirectoryService {
return entry; return entry;
} }
private async getGroups(force: boolean, setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> { private async getGroups(force: boolean, saveDelta: boolean,
setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
const entries: GroupEntry[] = []; const entries: GroupEntry[] = [];
const changedGroupIds: string[] = []; const changedGroupIds: string[] = [];
const token = await this.configurationService.getGroupDeltaToken(); const token = await this.configurationService.getGroupDeltaToken();
@@ -179,7 +180,7 @@ export class AzureDirectoryService implements DirectoryService {
} }
if (res[NextLink] == null) { if (res[NextLink] == null) {
if (res[DeltaLink] != null) { if (res[DeltaLink] != null && saveDelta) {
await this.configurationService.saveGroupDeltaToken(res[DeltaLink]); await this.configurationService.saveGroupDeltaToken(res[DeltaLink]);
} }
break; break;
@@ -232,13 +233,13 @@ export class AzureDirectoryService implements DirectoryService {
const memRes = await memReq.get(); const memRes = await memReq.get();
const members: any = memRes.value; const members: any = memRes.value;
if (members != null) { if (members != null) {
members.forEach((member: any) => { for (const member of members) {
if (member[ObjectType] === '#microsoft.graph.group') { if (member[ObjectType] === '#microsoft.graph.group') {
entry.groupMemberReferenceIds.add((member as graphType.Group).id); entry.groupMemberReferenceIds.add((member as graphType.Group).id);
} else if (member[ObjectType] === '#microsoft.graph.user') { } else if (member[ObjectType] === '#microsoft.graph.user') {
entry.userMemberExternalIds.add((member as graphType.User).id); entry.userMemberExternalIds.add((member as graphType.User).id);
} }
}); }
} }
return entry; return entry;
@@ -266,9 +267,9 @@ export class AzureDirectoryService implements DirectoryService {
const set = new Set<string>(); const set = new Set<string>();
const pieces = parts[1].split(','); const pieces = parts[1].split(',');
pieces.forEach((p) => { for (const p of pieces) {
set.add(p.trim().toLowerCase()); set.add(p.trim().toLowerCase());
}); }
return [exclude, set]; return [exclude, set];
} }

View File

@@ -18,6 +18,8 @@ const Keys = {
groupDelta: 'groupDeltaToken', groupDelta: 'groupDeltaToken',
lastUserSync: 'lastUserSync', lastUserSync: 'lastUserSync',
lastGroupSync: 'lastGroupSync', lastGroupSync: 'lastGroupSync',
lastSyncHash: 'lastSyncHash',
organizationId: 'organizationId',
}; };
export class ConfigurationService { export class ConfigurationService {
@@ -145,4 +147,28 @@ export class ConfigurationService {
return this.storageService.save(Keys.lastGroupSync, date); return this.storageService.save(Keys.lastGroupSync, date);
} }
} }
getLastSyncHash(): Promise<string> {
return this.storageService.get<string>(Keys.lastSyncHash);
}
saveLastSyncHash(hash: string) {
if (hash == null) {
return this.storageService.remove(Keys.lastSyncHash);
} else {
return this.storageService.save(Keys.lastSyncHash, hash);
}
}
getOrganizationId(): Promise<string> {
return this.storageService.get<string>(Keys.organizationId);
}
saveOrganizationId(id: string) {
if (id == null) {
return this.storageService.remove(Keys.organizationId);
} else {
return this.storageService.save(Keys.organizationId, id);
}
}
} }

View File

@@ -2,5 +2,5 @@ import { GroupEntry } from '../models/groupEntry';
import { UserEntry } from '../models/userEntry'; import { UserEntry } from '../models/userEntry';
export interface DirectoryService { export interface DirectoryService {
getEntries(force?: boolean): Promise<[GroupEntry[], UserEntry[]]>; getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
} }

View File

@@ -32,7 +32,7 @@ export class GSuiteDirectoryService implements DirectoryService {
this.service = google.admin<Admin>('directory_v1'); this.service = google.admin<Admin>('directory_v1');
} }
async getEntries(force = false): Promise<[GroupEntry[], UserEntry[]]> { async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType(); const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.GSuite) { if (type !== DirectoryType.GSuite) {
return; return;
@@ -76,7 +76,7 @@ export class GSuiteDirectoryService implements DirectoryService {
const filter = this.createSet(this.syncConfig.userFilter); const filter = this.createSet(this.syncConfig.userFilter);
if (res.data.users != null) { if (res.data.users != null) {
res.data.users.forEach((user) => { for (const user of res.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) { if (this.filterOutResult(filter, user.primaryEmail)) {
return; return;
} }
@@ -85,7 +85,7 @@ export class GSuiteDirectoryService implements DirectoryService {
if (entry != null) { if (entry != null) {
entries.push(entry); entries.push(entry);
} }
}); }
} }
this.logService.info('Querying deleted users.'); this.logService.info('Querying deleted users.');
@@ -96,7 +96,7 @@ export class GSuiteDirectoryService implements DirectoryService {
} }
if (delRes.data.users != null) { if (delRes.data.users != null) {
delRes.data.users.forEach((user) => { for (const user of delRes.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) { if (this.filterOutResult(filter, user.primaryEmail)) {
return; return;
} }
@@ -105,7 +105,7 @@ export class GSuiteDirectoryService implements DirectoryService {
if (entry != null) { if (entry != null) {
entries.push(entry); entries.push(entry);
} }
}); }
} }
return entries; return entries;
@@ -164,7 +164,7 @@ export class GSuiteDirectoryService implements DirectoryService {
} }
if (memRes.data.members != null) { if (memRes.data.members != null) {
memRes.data.members.forEach((member) => { for (const member of memRes.data.members) {
if (member.role.toLowerCase() !== 'member') { if (member.role.toLowerCase() !== 'member') {
return; return;
} }
@@ -177,7 +177,7 @@ export class GSuiteDirectoryService implements DirectoryService {
} else if (member.type.toLowerCase() === 'group') { } else if (member.type.toLowerCase() === 'group') {
entry.groupMemberReferenceIds.add(member.id); entry.groupMemberReferenceIds.add(member.id);
} }
}); }
} }
return entry; return entry;
@@ -223,9 +223,9 @@ export class GSuiteDirectoryService implements DirectoryService {
const set = new Set<string>(); const set = new Set<string>();
const pieces = parts[1].split(','); const pieces = parts[1].split(',');
pieces.forEach((p) => { for (const p of pieces) {
set.add(p.trim().toLowerCase()); set.add(p.trim().toLowerCase());
}); }
return [exclude, set]; return [exclude, set];
} }

View File

@@ -22,7 +22,7 @@ export class LdapDirectoryService implements DirectoryService {
constructor(private configurationService: ConfigurationService, private logService: LogService) { } constructor(private configurationService: ConfigurationService, private logService: LogService) { }
async getEntries(force = false): Promise<[GroupEntry[], UserEntry[]]> { async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType(); const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.Ldap) { if (type !== DirectoryType.Ldap) {
return; return;
@@ -61,7 +61,6 @@ export class LdapDirectoryService implements DirectoryService {
private async getUsers(force: boolean): Promise<UserEntry[]> { private async getUsers(force: boolean): Promise<UserEntry[]> {
const lastSync = await this.configurationService.getLastUserSyncDate(); const lastSync = await this.configurationService.getLastUserSyncDate();
let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter); let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
filter = this.buildRevisionFilter(filter, force, lastSync); filter = this.buildRevisionFilter(filter, force, lastSync);
@@ -121,7 +120,6 @@ export class LdapDirectoryService implements DirectoryService {
const entries: GroupEntry[] = []; const entries: GroupEntry[] = [];
const lastSync = await this.configurationService.getLastUserSyncDate(); const lastSync = await this.configurationService.getLastUserSyncDate();
const originalFilter = this.buildBaseFilter(this.syncConfig.groupObjectClass, this.syncConfig.groupFilter); const originalFilter = this.buildBaseFilter(this.syncConfig.groupObjectClass, this.syncConfig.groupFilter);
let filter = originalFilter; let filter = originalFilter;
const revisionFilter = this.buildRevisionFilter(filter, force, lastSync); const revisionFilter = this.buildRevisionFilter(filter, force, lastSync);
@@ -151,12 +149,12 @@ export class LdapDirectoryService implements DirectoryService {
return se; return se;
}); });
groupSearchEntries.forEach((se) => { for (const se of groupSearchEntries) {
const group = this.buildGroup(se, userIdMap); const group = this.buildGroup(se, userIdMap);
if (group != null) { if (group != null) {
entries.push(group); entries.push(group);
} }
}); }
return entries; return entries;
} }
@@ -181,13 +179,13 @@ export class LdapDirectoryService implements DirectoryService {
const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute); const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute);
if (members != null) { if (members != null) {
members.forEach((memDn) => { for (const memDn of members) {
if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) { if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) {
group.userMemberExternalIds.add(userMap.get(memDn)); group.userMemberExternalIds.add(userMap.get(memDn));
} else if (!group.groupMemberReferenceIds.has(memDn)) { } else if (!group.groupMemberReferenceIds.has(memDn)) {
group.groupMemberReferenceIds.add(memDn); group.groupMemberReferenceIds.add(memDn);
} }
}); }
} }
return group; return group;
@@ -326,9 +324,14 @@ export class LdapDirectoryService implements DirectoryService {
const pass = this.dirConfig.password == null || this.dirConfig.password.trim() === '' ? null : const pass = this.dirConfig.password == null || this.dirConfig.password.trim() === '' ? null :
this.dirConfig.password; this.dirConfig.password;
if (user == null || pass == null) {
reject('Username and/or password re not configured.');
return;
}
this.client.bind(user, pass, (err) => { this.client.bind(user, pass, (err) => {
if (err != null) { if (err != null) {
reject(err); reject('Error authenticating: ' + err.message);
} else { } else {
resolve(); resolve();
} }

View File

@@ -7,6 +7,8 @@ import { ImportDirectoryRequest } from 'jslib/models/request/importDirectoryRequ
import { ImportDirectoryRequestGroup } from 'jslib/models/request/importDirectoryRequestGroup'; import { ImportDirectoryRequestGroup } from 'jslib/models/request/importDirectoryRequestGroup';
import { ImportDirectoryRequestUser } from 'jslib/models/request/importDirectoryRequestUser'; import { ImportDirectoryRequestUser } from 'jslib/models/request/importDirectoryRequestUser';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { LogService } from 'jslib/abstractions/log.service'; import { LogService } from 'jslib/abstractions/log.service';
import { StorageService } from 'jslib/abstractions/storage.service'; import { StorageService } from 'jslib/abstractions/storage.service';
@@ -15,6 +17,7 @@ import { ConfigurationService } from './configuration.service';
import { DirectoryService } from './directory.service'; import { DirectoryService } from './directory.service';
import { GSuiteDirectoryService } from './gsuite-directory.service'; import { GSuiteDirectoryService } from './gsuite-directory.service';
import { LdapDirectoryService } from './ldap-directory.service'; import { LdapDirectoryService } from './ldap-directory.service';
import { Utils } from 'jslib/misc/utils';
const Keys = { const Keys = {
}; };
@@ -22,9 +25,10 @@ const Keys = {
export class SyncService { export class SyncService {
private dirType: DirectoryType; private dirType: DirectoryType;
constructor(private configurationService: ConfigurationService, private logService: LogService) { } constructor(private configurationService: ConfigurationService, private logService: LogService,
private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService) { }
async sync(force = true, sendToServer = true): Promise<[GroupEntry[], UserEntry[]]> { async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
this.dirType = await this.configurationService.getDirectoryType(); this.dirType = await this.configurationService.getDirectoryType();
if (this.dirType == null) { if (this.dirType == null) {
throw new Error('No directory configured.'); throw new Error('No directory configured.');
@@ -41,7 +45,7 @@ export class SyncService {
const now = new Date(); const now = new Date();
try { try {
const entries = await directoryService.getEntries(force); const entries = await directoryService.getEntries(force, test);
const groups = entries[0]; const groups = entries[0];
const users = entries[1]; const users = entries[1];
@@ -49,27 +53,48 @@ export class SyncService {
this.flattenUsersToGroups(groups, null, groups); this.flattenUsersToGroups(groups, null, groups);
} }
console.log(groups); if (test || groups == null || groups.length === 0 || users == null || users.length === 0) {
console.log(users);
if (!sendToServer) {
// TODO: restore deltas
}
if (!sendToServer || groups == null || groups.length === 0 || users == null || users.length === 0) {
return [groups, users]; return [groups, users];
} }
const req = this.buildRequest(groups, users, syncConfig.removeDisabled); const req = this.buildRequest(groups, users, syncConfig.removeDisabled);
const reqJson = JSON.stringify(req);
let hash: string = null;
const hashBuf = await this.cryptoFunctionService.hash(this.apiService.baseUrl + reqJson, 'sha256');
if (hashBuf != null) {
hash = Utils.fromBufferToB64(hashBuf);
}
const lastHash = await this.configurationService.getLastSyncHash();
if (lastHash == null || hash !== lastHash) {
const orgId = await this.configurationService.getOrganizationId();
if (orgId == null) {
throw new Error('Organization not set.');
}
const res = await this.apiService.postImportDirectory(orgId, req);
await this.configurationService.saveLastSyncHash(hash);
if (syncConfig.groups) {
await this.configurationService.saveLastGroupSyncDate(now);
}
if (syncConfig.users) {
await this.configurationService.saveLastUserSyncDate(now);
}
}
return [groups, users];
} catch (e) { } catch (e) {
// TODO: restore deltas if (!test) {
// failed sync result await this.configurationService.saveGroupDeltaToken(startingGroupDelta);
await this.configurationService.saveUserDeltaToken(startingUserDelta);
}
throw e; throw e;
} }
} }
private flattenUsersToGroups(currentGroups: GroupEntry[], currentGroupsUsers: string[], allGroups: GroupEntry[]) { private flattenUsersToGroups(currentGroups: GroupEntry[], currentGroupsUsers: string[], allGroups: GroupEntry[]) {
currentGroups.forEach((group) => { for (const group of currentGroups) {
const groupsInThisGroup = allGroups.filter((g) => group.groupMemberReferenceIds.has(g.referenceId)); const groupsInThisGroup = allGroups.filter((g) => group.groupMemberReferenceIds.has(g.referenceId));
let usersInThisGroup = Array.from(group.userMemberExternalIds); let usersInThisGroup = Array.from(group.userMemberExternalIds);
@@ -79,7 +104,7 @@ export class SyncService {
} }
this.flattenUsersToGroups(groupsInThisGroup, usersInThisGroup, allGroups); this.flattenUsersToGroups(groupsInThisGroup, usersInThisGroup, allGroups);
}); }
} }
private getDirectoryService(): DirectoryService { private getDirectoryService(): DirectoryService {
@@ -99,23 +124,23 @@ export class SyncService {
const model = new ImportDirectoryRequest(); const model = new ImportDirectoryRequest();
if (groups != null) { if (groups != null) {
groups.forEach((g) => { for (const g of groups) {
const ig = new ImportDirectoryRequestGroup(); const ig = new ImportDirectoryRequestGroup();
ig.name = g.name; ig.name = g.name;
ig.externalId = g.externalId; ig.externalId = g.externalId;
ig.users = Array.from(g.userMemberExternalIds); ig.users = Array.from(g.userMemberExternalIds);
model.groups.push(ig); model.groups.push(ig);
}); }
} }
if (users != null) { if (users != null) {
users.forEach((u) => { for (const u of users) {
const iu = new ImportDirectoryRequestUser(); const iu = new ImportDirectoryRequestUser();
iu.email = u.email; iu.email = u.email;
iu.externalId = u.externalId; iu.externalId = u.externalId;
iu.deleted = u.deleted || (removeDisabled && u.disabled); iu.deleted = u.deleted || (removeDisabled && u.disabled);
model.users.push(iu); model.users.push(iu);
}); }
} }
return model; return model;