Use organization api key for auth (#382)

* Create UserService for Api Keys

* Limit scope request for organization keys

* Expose necessary services for org key-based auth service

* Linter fixes

* Add public import models

Since public import is tied tightly to the private api, constructors are
provided to maintain coupling in case of changes

* Do not parallelize file access

This storage is sometims backed by lowdb files. Parallel writes can
cause issues.

* Match file name to class

* Serialize storageService promises

* Prefer multiple awaits to .then chains

* Linter fixes
This commit is contained in:
Matt Gibson 2021-05-19 14:12:08 -05:00 committed by GitHub
parent 73ec484b17
commit 79e6d012c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 171 additions and 45 deletions

View File

@ -31,6 +31,7 @@ import { ImportOrganizationCiphersRequest } from '../models/request/importOrgani
import { KdfRequest } from '../models/request/kdfRequest';
import { KeysRequest } from '../models/request/keysRequest';
import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest';
import { OrganizationImportRequest } from '../models/request/organizationImportRequest';
import { OrganizationKeysRequest } from '../models/request/organizationKeysRequest';
import { OrganizationTaxInfoUpdateRequest } from '../models/request/organizationTaxInfoUpdateRequest';
import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest';
@ -296,6 +297,7 @@ export abstract class ApiService {
getSync: () => Promise<SyncResponse>;
postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise<any>;
postPublicImportDirectory: (request: OrganizationImportRequest) => Promise<any>;
getSettingsDomains: () => Promise<DomainsResponse>;
putSettingsDomains: (request: UpdateDomainsRequest) => Promise<DomainsResponse>;

View File

@ -0,0 +1,7 @@
export abstract class ApiKeyService {
setInformation: (clientId: string) => Promise<any>;
clear: () => Promise<any>;
getEntityType: () => Promise<string>;
getEntityId: () => Promise<string>;
isAuthenticated: () => Promise<boolean>;
}

View File

@ -0,0 +1,19 @@
import { ImportDirectoryRequestGroup } from './importDirectoryRequestGroup';
export class OrganizationImportGroupRequest {
name: string;
externalId: string;
memberExternalIds: string[];
constructor(model: Required<OrganizationImportGroupRequest> | ImportDirectoryRequestGroup) {
this.name = model.name;
this.externalId = model.externalId;
if (model instanceof ImportDirectoryRequestGroup) {
this.memberExternalIds = model.users;
}
else {
this.memberExternalIds = model.memberExternalIds;
}
}
}

View File

@ -0,0 +1,13 @@
import { ImportDirectoryRequestUser } from './importDirectoryRequestUser';
export class OrganizationImportMemberRequest {
email: string;
externalId: string;
deleted: boolean;
constructor(model: Required<OrganizationImportMemberRequest> | ImportDirectoryRequestUser) {
this.email = model.email;
this.externalId = model.externalId;
this.deleted = model.deleted;
}
}

View File

@ -0,0 +1,25 @@
import { ImportDirectoryRequest } from './importDirectoryRequest';
import { OrganizationImportGroupRequest } from './organizationImportGroupRequest';
import { OrganizationImportMemberRequest } from './organizationImportMemberRequest';
export class OrganizationImportRequest {
groups: OrganizationImportGroupRequest[] = [];
members: OrganizationImportMemberRequest[] = [];
overwriteExisting: boolean = false;
largeImport: boolean = false;
constructor(model: {
groups: Required<OrganizationImportGroupRequest>[],
users: Required<OrganizationImportMemberRequest>[], overwriteExisting: boolean, largeImport: boolean;
} | ImportDirectoryRequest) {
if (model instanceof ImportDirectoryRequest) {
this.groups = model.groups.map(g => new OrganizationImportGroupRequest(g));
this.members = model.users.map(u => new OrganizationImportMemberRequest(u));
} else {
this.groups = model.groups.map(g => new OrganizationImportGroupRequest(g));
this.members = model.users.map(u => new OrganizationImportMemberRequest(u));
}
this.overwriteExisting = model.overwriteExisting;
this.largeImport = model.largeImport;
}
}

View File

@ -41,7 +41,7 @@ export class TokenRequest {
};
if (this.clientSecret != null) {
obj.scope = 'api';
obj.scope = clientId.startsWith('organization') ? 'api.organization' : 'api';
obj.grant_type = 'client_credentials';
obj.client_secret = this.clientSecret;
} else if (this.masterPasswordHash != null && this.email != null) {

View File

@ -35,6 +35,7 @@ import { ImportOrganizationCiphersRequest } from '../models/request/importOrgani
import { KdfRequest } from '../models/request/kdfRequest';
import { KeysRequest } from '../models/request/keysRequest';
import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest';
import { OrganizationImportRequest } from '../models/request/organizationImportRequest';
import { OrganizationKeysRequest } from '../models/request/organizationKeysRequest';
import { OrganizationTaxInfoUpdateRequest } from '../models/request/organizationTaxInfoUpdateRequest';
import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest';
@ -868,6 +869,10 @@ export class ApiService implements ApiServiceAbstraction {
return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false);
}
async postPublicImportDirectory(request: OrganizationImportRequest): Promise<any> {
return this.send('POST', '/public/organization/import', request, true, false);
}
async getTaxRates(): Promise<ListResponse<TaxRateResponse>> {
const r = await this.send('GET', '/plans/sales-tax-rates/', null, true, true);
return new ListResponse(r, TaxRateResponse);

View File

@ -0,0 +1,67 @@
import { ApiKeyService as ApiKeyServiceAbstraction } from '../abstractions/apiKey.service';
import { StorageService } from '../abstractions/storage.service';
import { TokenService } from '../abstractions/token.service';
import { Utils } from '../misc/utils';
const Keys = {
clientId: 'clientId',
entityType: 'entityType',
entityId: 'entityId',
};
export class ApiKeyService implements ApiKeyServiceAbstraction {
private clientId: string;
private entityType: string;
private entityId: string;
constructor(private tokenService: TokenService, private storageService: StorageService) { }
async setInformation(clientId: string) {
this.clientId = clientId;
const idParts = clientId.split('.');
if (idParts.length !== 2 || !Utils.isGuid(idParts[1])) {
throw Error('Invalid clientId');
}
this.entityType = idParts[0];
this.entityId = idParts[1];
await this.storageService.save(Keys.clientId, this.clientId);
await this.storageService.save(Keys.entityId, this.entityId);
await this.storageService.save(Keys.entityType, this.entityType);
}
async getEntityType(): Promise<string> {
if (this.entityType == null) {
this.entityType = await this.storageService.get<string>(Keys.entityType);
}
return this.entityType;
}
async getEntityId(): Promise<string> {
if (this.entityId == null) {
this.entityId = await this.storageService.get<string>(Keys.entityId);
}
return this.entityId;
}
async clear(): Promise<any> {
await this.storageService.remove(Keys.clientId);
await this.storageService.remove(Keys.entityId);
await this.storageService.remove(Keys.entityType);
this.clientId = this.entityId = this.entityType = null;
}
async isAuthenticated(): Promise<boolean> {
const token = await this.tokenService.getToken();
if (token == null) {
return false;
}
const entityId = await this.getEntityId();
return entityId != null;
}
}

View File

@ -88,10 +88,10 @@ export class AuthService implements AuthServiceAbstraction {
private key: SymmetricCryptoKey;
constructor(private cryptoService: CryptoService, private apiService: ApiService,
private userService: UserService, private tokenService: TokenService,
private appIdService: AppIdService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService,
constructor(private cryptoService: CryptoService, protected apiService: ApiService,
private userService: UserService, protected tokenService: TokenService,
protected appIdService: AppIdService, private i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService, private messagingService: MessagingService,
private vaultTimeoutService: VaultTimeoutService, private logService: LogService,
private setCryptoKeys = true) {
}

View File

@ -294,15 +294,13 @@ export class CryptoService implements CryptoServiceAbstraction {
return this.storageService.remove(ConstantsService.pinProtectedKey);
}
clearKeys(): Promise<any> {
return Promise.all([
this.clearKey(),
this.clearKeyHash(),
this.clearOrgKeys(),
this.clearEncKey(),
this.clearKeyPair(),
this.clearPinProtectedKey(),
]);
async clearKeys(): Promise<any> {
await this.clearEncKey();
await this.clearKeyHash();
await this.clearOrgKeys();
await this.clearEncKey();
await this.clearKeyPair();
await this.clearPinProtectedKey();
}
async toggleKey(): Promise<any> {

View File

@ -19,11 +19,9 @@ export class TokenService implements TokenServiceAbstraction {
constructor(private storageService: StorageService) {
}
setTokens(accessToken: string, refreshToken: string): Promise<any> {
return Promise.all([
this.setToken(accessToken),
this.setRefreshToken(refreshToken),
]);
async setTokens(accessToken: string, refreshToken: string): Promise<any> {
await this.setToken(accessToken);
await this.setRefreshToken(refreshToken);
}
async setToken(token: string): Promise<any> {
@ -96,15 +94,13 @@ export class TokenService implements TokenServiceAbstraction {
return this.storageService.remove(Keys.twoFactorTokenPrefix + email);
}
clearToken(): Promise<any> {
async clearToken(): Promise<any> {
this.token = null;
this.decodedToken = null;
this.refreshToken = null;
return Promise.all([
this.storageService.remove(Keys.accessToken),
this.storageService.remove(Keys.refreshToken),
]);
await this.storageService.remove(Keys.accessToken);
await this.storageService.remove(Keys.refreshToken);
}
// jwthelper methods

View File

@ -27,18 +27,16 @@ export class UserService implements UserServiceAbstraction {
constructor(private tokenService: TokenService, private storageService: StorageService) { }
setInformation(userId: string, email: string, kdf: KdfType, kdfIterations: number): Promise<any> {
async setInformation(userId: string, email: string, kdf: KdfType, kdfIterations: number): Promise<any> {
this.email = email;
this.userId = userId;
this.kdf = kdf;
this.kdfIterations = kdfIterations;
return Promise.all([
this.storageService.save(Keys.userEmail, email),
this.storageService.save(Keys.userId, userId),
this.storageService.save(Keys.kdf, kdf),
this.storageService.save(Keys.kdfIterations, kdfIterations),
]);
await this.storageService.save(Keys.userEmail, email);
await this.storageService.save(Keys.userId, userId);
await this.storageService.save(Keys.kdf, kdf);
await this.storageService.save(Keys.kdfIterations, kdfIterations);
}
setSecurityStamp(stamp: string): Promise<any> {
@ -96,14 +94,12 @@ export class UserService implements UserServiceAbstraction {
async clear(): Promise<any> {
const userId = await this.getUserId();
await Promise.all([
this.storageService.remove(Keys.userId),
this.storageService.remove(Keys.userEmail),
this.storageService.remove(Keys.stamp),
this.storageService.remove(Keys.kdf),
this.storageService.remove(Keys.kdfIterations),
this.clearOrganizations(userId),
]);
await this.storageService.remove(Keys.userId);
await this.storageService.remove(Keys.userEmail);
await this.storageService.remove(Keys.stamp);
await this.storageService.remove(Keys.kdf);
await this.storageService.remove(Keys.kdfIterations);
await this.clearOrganizations(userId);
this.userId = this.email = this.stamp = null;
this.kdf = null;

View File

@ -113,12 +113,10 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
}
}
await Promise.all([
this.cryptoService.clearKey(),
this.cryptoService.clearOrgKeys(true),
this.cryptoService.clearKeyPair(true),
this.cryptoService.clearEncKey(true),
]);
await this.cryptoService.clearKey();
await this.cryptoService.clearOrgKeys(true);
await this.cryptoService.clearKeyPair(true);
await this.cryptoService.clearEncKey(true);
this.folderService.clearCache();
this.cipherService.clearCache();