Skip to content

[msal-extensions] Extensions #2 #1831

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jun 30, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions extensions/src/error/PersistenceError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

/**
* Error thrown when trying to write MSAL cache to persistence.
*/
export class PersistenceError extends Error {

// Short string denoting error
errorCode: string;
// Detailed description of error
errorMessage: string;

constructor(errorCode: string, errorMessage: string) {
const errorString = errorMessage ? `${errorCode}: ${errorMessage}` : errorCode;
super(errorString);
Object.setPrototypeOf(this, PersistenceError.prototype);

this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.name = "PersistenceError";
}

static createFileSystemError(errorCode: string, errorMessage: string): PersistenceError {
return new PersistenceError(errorCode, errorMessage);
}

static createLibSecretError(errorCode: string, errorMessage: string): PersistenceError {
const updatedErrorMessage = `Error accessing Gnome Keyring: ${errorCode}- ${errorMessage}`;
return new PersistenceError("GnomeKeyringError", updatedErrorMessage);
}

static createKeychainPersistenceError(errorCode: string, errorMessage: string): PersistenceError {
const updatedErrorMessage = `Error accessing Keychain: ${errorCode}- ${errorMessage}`;
return new PersistenceError("KeychainError", updatedErrorMessage);
}

static createFilePersistenceWithDPAPIError(errorCode: string,errorMessage: string): PersistenceError {
const updatedErrorMessage = `Error accessing DPAPI encrypted file: ${errorCode}- ${errorMessage}`;
return new PersistenceError("DPAPIEncryptedFileError", updatedErrorMessage);
}

static createCrossPlatformLockError(errorCode: string, errorMessage: string): PersistenceError {
const updatedErrorMessage = `Error acquiring lock: ${errorCode}- ${errorMessage}`;
return new PersistenceError("CrossPlatformLockError", updatedErrorMessage);
}
}
8 changes: 8 additions & 0 deletions extensions/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export { PersistenceCachePlugin } from "./persistence/PersistenceCachePlugin"
export { FilePersistence } from "./persistence/FilePersistence";
export { FilePersistenceWithDataProtection } from "./persistence/FilePersistenceWithDataProtection";
export { DataProtectionScope } from "./persistence/DataProtectionScope";
export { KeychainPersistence } from "./persistence/KeychainPersistence";
export { LibSecretPersistence } from "./persistence/LibSecretPersistence";
export { IPersistence } from "./persistence/IPersistence";
export { CrossPlatformLockOptions } from "./lock/CrossPlatformLockOptions";
80 changes: 80 additions & 0 deletions extensions/src/lock/CrossPlatformLock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { open, close, write, unlink } from "fs";
import { promisify } from "util";
import { pid } from "process";
import { CrossPlatformLockOptions } from "./CrossPlatformLockOptions";
import { Constants } from "../utils/Constants";
import { PersistenceError } from "../error/PersistenceError";

/**
* Cross-process lock that works on all platforms.
*/
export class CrossPlatformLock {

private readonly lockFilePath: string;
private lockFileDescriptor: number;
private readonly retryNumber: number;
private readonly retryDelay: number;

constructor(lockFilePath:string, lockOptions?: CrossPlatformLockOptions) {
this.lockFilePath = lockFilePath;
this.retryNumber = lockOptions ? lockOptions.retryNumber : 500;
this.retryDelay = lockOptions ? lockOptions.retryDelay : 100;
}

/**
* Locks cache from read or writes by creating file with same path and name as
* cache file but with .lockfile extension. If another process has already created
* the lockfile, will retry again based on configuration settings set by CrossPlatformLockOptions
*/
public async lock(): Promise<void> {
const processId = pid.toString();
for (let tryCount = 0; tryCount < this.retryNumber; tryCount++)
try {
console.log("Pid " + pid + " trying to acquire lock");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every console.log statement will be switched over to using the msal-common logger in upcoming PR

const openPromise = promisify(open);
this.lockFileDescriptor = await openPromise(this.lockFilePath, "wx+");

console.log("Pid " + pid + " acquired lock");
const writePromise = promisify(write);
await writePromise(this.lockFileDescriptor, processId);
break;
} catch (err) {
if (err.code == Constants.EEXIST_ERROR) {
console.log(err);
await this.sleep(this.retryDelay);
} else {
throw PersistenceError.createCrossPlatformLockError(err.code, err.message);
}
}
}

/**
* unlocks cache file by deleting .lockfile.
*/
public async unlock(): Promise<void> {
try {
// delete lock file
const unlinkPromise = promisify(unlink);
await unlinkPromise(this.lockFilePath);
const closePromise = promisify(close);
await closePromise(this.lockFileDescriptor);
} catch(err){
if(err.code == Constants.ENOENT_ERROR){
console.log("Lockfile does not exist");
} else {
throw PersistenceError.createCrossPlatformLockError(err.code, err.message);
}
}
}

private sleep(ms): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
}
15 changes: 15 additions & 0 deletions extensions/src/lock/CrossPlatformLockOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

/**
* Options for CrossPlatform lock.
*
* retryNumber: Numbers of times we should try to acquire a lock. Defaults to 500.
* retryDelay: Time to wait before trying to retry a lock acquisition. Defaults to 100 ms.
*/
export type CrossPlatformLockOptions = {
retryNumber: number;
retryDelay: number;
}
24 changes: 24 additions & 0 deletions extensions/src/utils/Constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

export const Constants = {

/**
* An existing file was the target of an operation that required that the target not exist
*/
EEXIST_ERROR: "EEXIST",

/**
* No such file or directory: Commonly raised by fs operations to indicate that a component
* of the specified pathname does not exist. No entity (file or directory) could be found
* by the given path
*/
ENOENT_ERROR: "ENOENT",

/**
* Default service name for using MSAL Keytar
*/
DEFAULT_SERVICE_NAME: "msal-node-extensions",
};