1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-15 07:43:27 +00:00

Compare commits

..

24 Commits

Author SHA1 Message Date
Kyle Spearrin
3335659c8d bump version 2019-03-26 12:24:16 -04:00
Kyle Spearrin
f0282b33f0 add paging to members list api 2019-03-26 09:06:42 -04:00
Kyle Spearrin
f69d461463 move scope of filters for delUsers query 2019-03-26 09:02:35 -04:00
Elie Mélois
bcf16562fe Manage multiple pages when fetching users (#15) 2019-03-26 08:57:01 -04:00
Kyle Spearrin
d7412410f2 group paging cleanup 2019-03-26 08:02:09 -04:00
Elie Mélois
d5907477df Manage multiple pages when fetching groups (#14) 2019-03-26 07:58:05 -04:00
Kyle Spearrin
9acfaff361 remove echo 2019-03-22 22:25:37 -04:00
Kyle Spearrin
737274da59 test for ver info during install 2019-03-22 22:20:41 -04:00
Kyle Spearrin
7a0e0e6915 WIN_PKG set with ps 2019-03-22 22:16:00 -04:00
Kyle Spearrin
25847b9c83 ver info testing 2019-03-22 22:11:32 -04:00
Kyle Spearrin
07dd7bb93e bump version 2019-03-22 17:35:29 -04:00
Kyle Spearrin
bc9936bd92 update lock file 2019-03-22 17:29:38 -04:00
Kyle Spearrin
e4428e3387 update ldapjs fork 2019-03-22 17:21:18 -04:00
Kyle Spearrin
30ef0ce140 back to node 10 2019-03-22 16:55:51 -04:00
Kyle Spearrin
38c461ebe0 use ldapjs fork 2019-03-22 16:55:20 -04:00
Kyle Spearrin
ead1776f90 try node 9 2019-03-22 16:38:49 -04:00
Kyle Spearrin
0e944b2442 simplify sim promise 2019-03-22 13:12:52 -04:00
Kyle Spearrin
3bbd503308 RELEASE_NAME is just numbers 2019-03-20 23:09:54 -04:00
Kyle Spearrin
a7b13b168e release name fix 2019-03-20 23:09:12 -04:00
Kyle Spearrin
1cf8ae1bdf bump version 2019-03-20 22:16:31 -04:00
Kyle Spearrin
7da3c8e252 Merge branch 'master' of github.com:bitwarden/directory-connector 2019-03-20 17:33:13 -04:00
Kyle Spearrin
2edf3fb68d data-file and last-sync commands 2019-03-20 17:33:11 -04:00
Kyle Spearrin
1e698ba81b Update README.md 2019-03-20 16:44:41 -04:00
Kyle Spearrin
e9034ea6fe Update README.md 2019-03-20 16:43:48 -04:00
12 changed files with 240 additions and 118 deletions

View File

@@ -18,7 +18,32 @@ The application is written using Electron with Angular and installs on Windows,
![Directory Connector](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/directory-connector-macos.png "Dashboard")
# Build/Run
## Command-line Interface
A command-line interface tool is also available for the Bitwarden Directory Connector. The Directory Connector CLI (`bwdc`) is written with TypeScript and Node.js and can also be run on Windows, macOS, and Linux distributions.
## CLI Documentation
The Bitwarden Directory Connector CLI is self-documented with `--help` content and examples for every command. You should start exploring the CLI by using the global `--help` option:
```bash
bwdc --help
```
This option will list all available commands that you can use with the Directory Connector CLI.
Additionally, you can run the `--help` option on a specific command to learn more about it:
```
bwdc test --help
bwdc config --help
```
**Detailed Documentation**
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://help.bitwarden.com/article/directory-sync/#command-line-interface.
## Build/Run
**Requirements**
@@ -48,7 +73,7 @@ You can then run commands from the `./build-cli` folder:
node ./build-cli/bwdc.js --help
```
# Contribute
## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.

View File

@@ -24,21 +24,23 @@ init:
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-Product node 10
$env:PATH = "C:\Program Files (x86)\Resource Hacker;${env:PATH}"
if(Test-Path -Path $env:WIN_PKG) {
$env:VER_INFO = "true"
}
}
if($env:APPVEYOR_REPO_TAG -eq "true") {
$env:RELEASE_NAME = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
}
install:
- ps: |
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\src\package.json | ConvertFrom-Json).version
$env:RELEASE_NAME = "Version ${tagName}"
$env:PROD_DEPLOY = "false"
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
$env:PROD_DEPLOY = "true"
echo "This is a production deployment."
}
if($isWindows) {
if(Test-Path -Path $env:WIN_PKG) {
$env:VER_INFO = "true"
}
choco install reshack --no-progress
choco install cloc --no-progress
choco install checksum --no-progress
@@ -136,17 +138,17 @@ for:
only:
- image: Visual Studio 2017
cache:
- '%LOCALAPPDATA%\electron -> appveyor.yml'
- '%LOCALAPPDATA%\electron-builder -> appveyor.yml'
- 'C:\Users\appveyor\.pkg-cache\ -> package.json'
- '%LOCALAPPDATA%\electron'
- '%LOCALAPPDATA%\electron-builder'
- 'C:\Users\appveyor\.pkg-cache\'
-
matrix:
only:
- image: Ubuntu1804
cache:
- '/home/appveyor/.cache/electron -> appveyor.yml'
- '/home/appveyor/.cache/electron-builder -> appveyor.yml'
- '/home/appveyor/.cache/electron'
- '/home/appveyor/.cache/electron-builder'
deploy:
tag: $(APPVEYOR_REPO_TAG_NAME)

2
jslib

Submodule jslib updated: 50e6f24679...a884f77938

19
package-lock.json generated
View File

@@ -3865,14 +3865,6 @@
"optional": true,
"requires": {
"nan": "^2.10.0"
},
"dependencies": {
"nan": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"optional": true
}
}
},
"duo_web_sdk": {
@@ -8800,9 +8792,8 @@
}
},
"ldapjs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
"version": "git+https://git@github.com/kspearrin/node-ldapjs.git#12c07b17e0cf505e7da817599f877932812d3f8f",
"from": "git+https://git@github.com/kspearrin/node-ldapjs.git",
"requires": {
"asn1": "0.2.3",
"assert-plus": "^1.0.0",
@@ -9700,9 +9691,9 @@
}
},
"moment": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=",
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
"optional": true
},
"move-concurrently": {

View File

@@ -207,7 +207,7 @@
"googleapis": "33.0.0",
"inquirer": "6.2.0",
"keytar": "4.4.1",
"ldapjs": "1.0.2",
"ldapjs": "git+https://git@github.com/kspearrin/node-ldapjs.git",
"lowdb": "1.0.0",
"lunr": "2.3.3",
"node-fetch": "2.2.0",

View File

@@ -14,7 +14,8 @@
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong>
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong>
</p>
<button #startBtn (click)="start()" [appApiAction]="startPromise" class="btn btn-primary" [disabled]="startBtn.loading">
<button #startBtn (click)="start()" [appApiAction]="startPromise" class="btn btn-primary"
[disabled]="startBtn.loading">
<i class="fa fa-play fa-fw" [hidden]="startBtn.loading"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startBtn.loading"></i>
{{'startSync' | i18n}}
@@ -23,7 +24,8 @@
<i class="fa fa-stop fa-fw"></i>
{{'stopSync' | i18n}}
</button>
<button #syncBtn (click)="sync()" [appApiAction]="syncPromise" class="btn btn-primary" [disabled]="syncBtn.loading">
<button #syncBtn (click)="sync()" [appApiAction]="syncPromise" class="btn btn-primary"
[disabled]="syncBtn.loading">
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncBtn.loading}"></i>
{{'syncNow' | i18n}}
</button>
@@ -33,7 +35,8 @@
<h3 class="card-header">{{'testing' | i18n}}</h3>
<div class="card-body">
<p>{{'testingDesc' | i18n}}</p>
<button #simBtn (click)="simulate()" [appApiAction]="simPromise" class="btn btn-primary" [disabled]="simBtn.loading">
<button #simBtn (click)="simulate()" [appApiAction]="simPromise" class="btn btn-primary"
[disabled]="simBtn.loading">
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simBtn.loading"></i>
<i class="fa fa-bug fa-fw" [hidden]="simBtn.loading"></i>
{{'testNow' | i18n}}

View File

@@ -15,6 +15,7 @@ import { StateService } from 'jslib/abstractions/state.service';
import { SyncService } from '../../services/sync.service';
import { GroupEntry } from '../../models/groupEntry';
import { SimResult } from '../../models/simResult';
import { UserEntry } from '../../models/userEntry';
import { ConfigurationService } from '../../services/configuration.service';
@@ -34,7 +35,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
simEnabledUsers: UserEntry[] = [];
simDisabledUsers: UserEntry[] = [];
simDeletedUsers: UserEntry[] = [];
simPromise: Promise<any>;
simPromise: Promise<SimResult>;
simSinceLast: boolean = false;
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
startPromise: Promise<any>;
@@ -101,22 +102,18 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.simDisabledUsers = [];
this.simDeletedUsers = [];
this.simPromise = new Promise(async (resolve, reject) => {
try {
const result = await ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast);
this.simGroups = result.groups;
this.simUsers = result.users;
this.simEnabledUsers = result.enabledUsers;
this.simDisabledUsers = result.disabledUsers;
this.simDeletedUsers = result.deletedUsers;
} catch (e) {
this.simGroups = null;
this.simUsers = null;
reject(e);
return;
}
resolve();
});
try {
this.simPromise = ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast);
const result = await this.simPromise;
this.simGroups = result.groups;
this.simUsers = result.users;
this.simEnabledUsers = result.enabledUsers;
this.simDisabledUsers = result.disabledUsers;
this.simDeletedUsers = result.deletedUsers;
} catch (e) {
this.simGroups = null;
this.simUsers = null;
}
}
private async updateLastSync() {

View File

@@ -31,6 +31,7 @@ import { Program } from './program';
const packageJson = require('./package.json');
export class Main {
dataFilePath: string;
logService: ConsoleLogService;
messagingService: NoopMessagingService;
storageService: LowdbStorageService;
@@ -53,21 +54,20 @@ export class Main {
constructor() {
const applicationName = 'Bitwarden Directory Connector';
let p = null;
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
p = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
} else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) {
p = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
} else if (fs.existsSync(path.join(__dirname, 'bitwarden-connector-appdata'))) {
p = path.join(__dirname, 'bitwarden-connector-appdata');
this.dataFilePath = path.join(__dirname, 'bitwarden-connector-appdata');
} else if (process.platform === 'darwin') {
p = path.join(process.env.HOME, 'Library/Application Support/' + applicationName);
this.dataFilePath = path.join(process.env.HOME, 'Library/Application Support/' + applicationName);
} else if (process.platform === 'win32') {
p = path.join(process.env.APPDATA, applicationName);
this.dataFilePath = path.join(process.env.APPDATA, applicationName);
} else if (process.env.XDG_CONFIG_HOME) {
p = path.join(process.env.XDG_CONFIG_HOME, applicationName);
this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName);
} else {
p = path.join(process.env.HOME, '.config/' + applicationName);
this.dataFilePath = path.join(process.env.HOME, '.config/' + applicationName);
}
this.i18nService = new I18nService('en', './locales');
@@ -75,7 +75,7 @@ export class Main {
this.logService = new ConsoleLogService(this.platformUtilsService.isDev(),
(level) => process.env.BWCLI_DEBUG !== 'true' && level <= LogLevelType.Info);
this.cryptoFunctionService = new NodeCryptoFunctionService();
this.storageService = new LowdbStorageService(null, p, true);
this.storageService = new LowdbStorageService(null, this.dataFilePath, true);
this.secureStorageService = new KeytarSecureStorageService(applicationName);
this.cryptoService = new CryptoService(this.storageService, this.secureStorageService,
this.cryptoFunctionService);

View File

@@ -0,0 +1,29 @@
import * as program from 'commander';
import { ConfigurationService } from '../services/configuration.service';
import { Response } from 'jslib/cli/models/response';
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
export class LastSyncCommand {
constructor(private configurationService: ConfigurationService) { }
async run(object: string, cmd: program.Command): Promise<Response> {
try {
switch (object.toLowerCase()) {
case 'groups':
const groupsDate = await this.configurationService.getLastGroupSyncDate();
const groupsRes = new StringResponse(groupsDate == null ? null : groupsDate.toISOString());
return Response.success(groupsRes);
case 'users':
const usersDate = await this.configurationService.getLastUserSyncDate();
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
return Response.success(usersRes);
default:
return Response.badRequest('Unknown object.');
}
} catch (e) {
return Response.error(e);
}
}
}

View File

@@ -2,7 +2,7 @@
"name": "bitwarden-directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2.5.0",
"version": "2.5.3",
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",

View File

@@ -1,10 +1,12 @@
import * as chk from 'chalk';
import * as program from 'commander';
import * as path from 'path';
import { Main } from './bwdc';
import { ClearCacheCommand } from './commands/clearCache.command';
import { ConfigCommand } from './commands/config.command';
import { LastSyncCommand } from './commands/lastSync.command';
import { SyncCommand } from './commands/sync.command';
import { TestCommand } from './commands/test.command';
@@ -14,6 +16,9 @@ import { UpdateCommand } from 'jslib/cli/commands/update.command';
import { BaseProgram } from 'jslib/cli/baseProgram';
import { Response } from 'jslib/cli/models/response';
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
const chalk = chk.default;
const writeLn = (s: string, finalLine: boolean = false) => {
if (finalLine && process.platform === 'win32') {
@@ -61,8 +66,10 @@ export class Program extends BaseProgram {
program.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc login');
writeLn(' bwdc test');
writeLn(' bwdc sync');
writeLn(' bwdc last-sync');
writeLn(' bwdc config server https://bw.company.com');
writeLn(' bwdc update');
writeLn('', true);
@@ -143,6 +150,27 @@ export class Program extends BaseProgram {
this.processResponse(response);
});
program
.command('last-sync <object>')
.description('Get the last successful sync date.')
.on('--help', () => {
writeLn('\n Notes:');
writeLn('');
writeLn(' Returns empty response if no sync has been performed for the given object.');
writeLn('');
writeLn(' Examples:');
writeLn('');
writeLn(' bwdc last-sync groups');
writeLn(' bwdc last-sync users');
writeLn('', true);
})
.action(async (object: string, cmd: program.Command) => {
await this.exitIfNotAuthed();
const command = new LastSyncCommand(this.main.configurationService);
const response = await command.run(object, cmd);
this.processResponse(response);
});
program
.command('config <setting> <value>')
.description('Configure settings.')
@@ -174,6 +202,20 @@ export class Program extends BaseProgram {
this.processResponse(response);
});
program
.command('data-file')
.description('Path to data.json database file.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc data-file');
writeLn('', true);
})
.action(() => {
this.processResponse(
Response.success(new StringResponse(path.join(this.main.dataFilePath, 'data.json'))));
});
program
.command('clear-cache')
.description('Clear the sync cache.')

View File

@@ -67,45 +67,59 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
private async getUsers(): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
this.logService.info('Querying users.');
let p = Object.assign({ query: query }, this.authParams);
const res = await this.service.users.list(p);
if (res.status !== 200) {
throw new Error('User list API failed: ' + res.statusText);
}
let nextPageToken: string = null;
const filter = this.createCustomSet(this.syncConfig.userFilter);
if (res.data.users != null) {
for (const user of res.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
}
while (true) {
this.logService.info('Querying users - nextPageToken:' + nextPageToken);
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
const res = await this.service.users.list(p);
if (res.status !== 200) {
throw new Error('User list API failed: ' + res.statusText);
}
const entry = this.buildUser(user, false);
if (entry != null) {
entries.push(entry);
nextPageToken = res.data.nextPageToken;
if (res.data.users != null) {
for (const user of res.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
}
const entry = this.buildUser(user, false);
if (entry != null) {
entries.push(entry);
}
}
}
if (nextPageToken == null) {
break;
}
}
this.logService.info('Querying deleted users.');
p = Object.assign({ showDeleted: true, query: query }, this.authParams);
const delRes = await this.service.users.list(p);
if (delRes.status !== 200) {
throw new Error('Deleted user list API failed: ' + delRes.statusText);
}
nextPageToken = null;
while (true) {
this.logService.info('Querying deleted users - nextPageToken:' + nextPageToken);
const p = Object.assign({ showDeleted: true, query: query, pageToken: nextPageToken }, this.authParams);
const delRes = await this.service.users.list(p);
if (delRes.status !== 200) {
throw new Error('Deleted user list API failed: ' + delRes.statusText);
}
if (delRes.data.users != null) {
for (const user of delRes.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
nextPageToken = delRes.data.nextPageToken;
if (delRes.data.users != null) {
for (const user of delRes.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
}
const entry = this.buildUser(user, true);
if (entry != null) {
entries.push(entry);
}
}
}
const entry = this.buildUser(user, true);
if (entry != null) {
entries.push(entry);
}
if (nextPageToken == null) {
break;
}
}
@@ -128,56 +142,75 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
let nextPageToken: string = null;
this.logService.info('Querying groups.');
const res = await this.service.groups.list(this.authParams);
if (res.status !== 200) {
throw new Error('Group list API failed: ' + res.statusText);
}
if (res.data.groups != null) {
for (const group of res.data.groups) {
if (!this.filterOutResult(setFilter, group.name)) {
const entry = await this.buildGroup(group);
entries.push(entry);
while (true) {
this.logService.info('Querying groups - nextPageToken:' + nextPageToken);
const p = Object.assign({ pageToken: nextPageToken }, this.authParams);
const res = await this.service.groups.list(p);
if (res.status !== 200) {
throw new Error('Group list API failed: ' + res.statusText);
}
nextPageToken = res.data.nextPageToken;
if (res.data.groups != null) {
for (const group of res.data.groups) {
if (!this.filterOutResult(setFilter, group.name)) {
const entry = await this.buildGroup(group);
entries.push(entry);
}
}
}
if (nextPageToken == null) {
break;
}
}
return entries;
}
private async buildGroup(group: admin_directory_v1.Schema$Group) {
let nextPageToken: string = null;
const entry = new GroupEntry();
entry.referenceId = group.id;
entry.externalId = group.id;
entry.name = group.name;
const p = Object.assign({ groupKey: group.id }, this.authParams);
const memRes = await this.service.members.list(p);
if (memRes.status !== 200) {
this.logService.warning('Group member list API failed: ' + memRes.statusText);
return entry;
}
while (true) {
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
const memRes = await this.service.members.list(p);
if (memRes.status !== 200) {
this.logService.warning('Group member list API failed: ' + memRes.statusText);
return entry;
}
if (memRes.data.members != null) {
for (const member of memRes.data.members) {
if (member.type == null) {
continue;
}
if (member.role == null || member.role.toLowerCase() !== 'member') {
continue;
}
if (member.status == null || member.status.toLowerCase() !== 'active') {
continue;
}
nextPageToken = memRes.data.nextPageToken;
if (memRes.data.members != null) {
for (const member of memRes.data.members) {
if (member.type == null) {
continue;
}
if (member.role == null || member.role.toLowerCase() !== 'member') {
continue;
}
if (member.status == null || member.status.toLowerCase() !== 'active') {
continue;
}
const type = member.type.toLowerCase();
if (type === 'user') {
entry.userMemberExternalIds.add(member.id);
} else if (type === 'group') {
entry.groupMemberReferenceIds.add(member.id);
const type = member.type.toLowerCase();
if (type === 'user') {
entry.userMemberExternalIds.add(member.id);
} else if (type === 'group') {
entry.groupMemberReferenceIds.add(member.id);
}
}
}
if (nextPageToken == null) {
break;
}
}
return entry;