Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

Commit

Permalink
Adding AuthScopes value object
Browse files Browse the repository at this point in the history
  • Loading branch information
paulomarg committed Feb 12, 2021
1 parent 34bdfba commit 81895e4
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 10 deletions.
2 changes: 1 addition & 1 deletion src/auth/oauth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const ShopifyOAuth = {
/* eslint-disable @typescript-eslint/naming-convention */
const query = {
client_id: Context.API_KEY,
scope: Context.SCOPES.join(', '),
scope: Context.SCOPES.toString(),
redirect_uri: `https://${Context.HOST_NAME}${redirectPath}`,
state,
'grant_options[]': isOnline ? 'per-user' : '',
Expand Down
8 changes: 4 additions & 4 deletions src/auth/oauth/test/oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe('beginAuth', () => {
/* eslint-disable @typescript-eslint/naming-convention */
const query = {
client_id: Context.API_KEY,
scope: Context.SCOPES,
scope: Context.SCOPES.toString(),
redirect_uri: `https://${Context.HOST_NAME}/some-callback`,
state: session ? session.state : '',
'grant_options[]': '',
Expand All @@ -111,7 +111,7 @@ describe('beginAuth', () => {
/* eslint-disable @typescript-eslint/naming-convention */
const query = {
client_id: Context.API_KEY,
scope: Context.SCOPES,
scope: Context.SCOPES.toString(),
redirect_uri: `https://${Context.HOST_NAME}/some-callback`,
state: session ? session.state : '',
'grant_options[]': 'per-user',
Expand Down Expand Up @@ -221,7 +221,7 @@ describe('validateAuthCallback', () => {
/* eslint-disable @typescript-eslint/naming-convention */
const successResponse = {
access_token: 'some access token string',
scope: Context.SCOPES.join(','),
scope: Context.SCOPES.toString(),
};
/* eslint-enable @typescript-eslint/naming-convention */

Expand Down Expand Up @@ -255,7 +255,7 @@ describe('validateAuthCallback', () => {
/* eslint-disable @typescript-eslint/naming-convention */
const successResponse = {
access_token: 'some access token string',
scope: Context.SCOPES.join(','),
scope: Context.SCOPES.toString(),
};
/* eslint-enable @typescript-eslint/naming-convention */

Expand Down
73 changes: 73 additions & 0 deletions src/auth/scopes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
class AuthScopes {
public static SCOPE_DELIMITER = ',';

private compressedScopes: Set<string>;
private expandedScopes: Set<string>;

constructor(scopes: string | string[]) {
let scopesArray: string[] = [];
if (typeof scopes === 'string') {
scopesArray = scopes.split(new RegExp(`${AuthScopes.SCOPE_DELIMITER}\\s*`));
} else {
scopesArray = scopes;
}

scopesArray = scopesArray.map((scope) => scope.trim()).filter((scope) => scope.length);

const impliedScopes = this.getImpliedScopes(scopesArray);

const scopeSet = new Set(scopesArray);
const impliedSet = new Set(impliedScopes);

this.compressedScopes = new Set([...scopeSet].filter((x) => !impliedSet.has(x)));
this.expandedScopes = new Set([...scopeSet, ...impliedSet]);
}

public has(scope: string | string[] | AuthScopes) {
let other: AuthScopes;

if (scope instanceof AuthScopes) {
other = scope;
} else {
other = new AuthScopes(scope);
}

return other.toArray().filter((x) => !this.expandedScopes.has(x)).length === 0;
}

public equals(otherScopes: string | string[] | AuthScopes) {
let other: AuthScopes;

if (otherScopes instanceof AuthScopes) {
other = otherScopes;
} else {
other = new AuthScopes(otherScopes);
}

return (
this.compressedScopes.size === other.compressedScopes.size &&
this.toArray().filter((x) => !other.has(x)).length === 0
);
}

public toString() {
return this.toArray().join(AuthScopes.SCOPE_DELIMITER);
}

public toArray() {
return [...this.compressedScopes];
}

private getImpliedScopes(scopesArray: string[]): string[] {
return scopesArray.reduce((array: string[], current: string) => {
const matches = current.match(/^(unauthenticated_)?write_(.*)$/);
if (matches) {
array.push(`${matches[1] ? matches[1] : ''}read_${matches[2]}`);
}

return array;
}, []);
}
}

export {AuthScopes};
138 changes: 138 additions & 0 deletions src/auth/scopes/test/scopes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import '../../../test/test_helper';

import {AuthScopes} from '../index';

describe('AuthScopes', () => {
it('can parse and trim string scopes', () => {
const scopeString = ' read_products, read_orders,,write_customers ';
const scopes = new AuthScopes(scopeString);

expect(scopes.toString()).toEqual('read_products,read_orders,write_customers');
});

it('can parse and trim array scopes', () => {
const scopeString = [' read_products', 'read_orders', '', 'unauthenticated_write_customers '];
const scopes = new AuthScopes(scopeString);

expect(scopes.toString()).toEqual('read_products,read_orders,unauthenticated_write_customers');
});

it('trims implied scopes', () => {
const scopeString = 'read_customers,write_customers,read_products';
const scopes = new AuthScopes(scopeString);

expect(scopes.toString()).toEqual('write_customers,read_products');
});

it('trims implied unauthenticated scopes', () => {
const scopeString = 'unauthenticated_read_customers,unauthenticated_write_customers,unauthenticated_read_products';
const scopes = new AuthScopes(scopeString);

expect(scopes.toString()).toEqual('unauthenticated_write_customers,unauthenticated_read_products');
});
});

describe('AuthScopes.equals', () => {
it('returns true for equivalent sets', () => {
const scopes1 = new AuthScopes('write_customers,read_products');
const scopes2 = new AuthScopes(['write_customers', 'read_products']);

expect(scopes1.equals(scopes2)).toBeTruthy();
expect(scopes2.equals(scopes1)).toBeTruthy();
});

it('returns false for different sets', () => {
const scopes1 = new AuthScopes('write_customers,read_products');
const scopes2 = new AuthScopes(['write_customers', 'write_orders']);

expect(scopes1.equals(scopes2)).toBeFalsy();
expect(scopes2.equals(scopes1)).toBeFalsy();
});

it('returns true if there are implied scopes', () => {
const scopes1 = new AuthScopes('write_customers,read_products,write_products');
const scopes2 = new AuthScopes(['write_customers', 'write_products']);

expect(scopes1.equals(scopes2)).toBeTruthy();
expect(scopes2.equals(scopes1)).toBeTruthy();
});

it('returns false if current set is a subset of other', () => {
const scopes1 = new AuthScopes('write_customers,read_products,write_products');
const scopes2 = new AuthScopes(['write_customers', 'write_products', 'write_orders']);

expect(scopes1.equals(scopes2)).toBeFalsy();
expect(scopes2.equals(scopes1)).toBeFalsy();
});

it('allows comparing against strings', () => {
const scopes1 = new AuthScopes('write_customers,read_products,write_products');

expect(scopes1.equals('write_customers,read_products,write_products')).toBeTruthy();
});

it('allows comparing against string arrays', () => {
const scopes1 = new AuthScopes('write_customers,read_products,write_products');

expect(scopes1.equals(['write_customers', 'read_products', 'write_products'])).toBeTruthy();
});
});

describe('AuthScopes.has', () => {
it('returns true for subset string', () => {
const scopes1 = new AuthScopes('write_customers,read_products');

expect(scopes1.has('write_customers')).toBeTruthy();
});

it('returns true for subset string array', () => {
const scopes1 = new AuthScopes('write_customers,read_products');

expect(scopes1.has(['write_customers'])).toBeTruthy();
});

it('returns true for subset scopes object', () => {
const scopes1 = new AuthScopes('write_customers,read_products');
const scopes2 = new AuthScopes(['write_customers']);

expect(scopes1.has(scopes2)).toBeTruthy();
});

it('returns true for equal string', () => {
const scopes1 = new AuthScopes('write_customers,read_products');

expect(scopes1.has('write_customers,read_products')).toBeTruthy();
});

it('returns true for equal string array', () => {
const scopes1 = new AuthScopes('write_customers,read_products');

expect(scopes1.has(['write_customers', 'read_products'])).toBeTruthy();
});

it('returns true for equal scopes object', () => {
const scopes1 = new AuthScopes('write_customers,read_products');
const scopes2 = new AuthScopes(['write_customers', 'read_products']);

expect(scopes1.has(scopes2)).toBeTruthy();
});

it('returns false for superset string', () => {
const scopes1 = new AuthScopes('write_customers,read_products');

expect(scopes1.has('write_customers,read_products,read_orders')).toBeFalsy();
});

it('returns false for superset string array', () => {
const scopes1 = new AuthScopes('write_customers,read_products');

expect(scopes1.has(['write_customers', 'read_products', 'read_orders'])).toBeFalsy();
});

it('returns false for superset scopes object', () => {
const scopes1 = new AuthScopes('write_customers,read_products');
const scopes2 = new AuthScopes(['write_customers', 'read_products', 'read_orders']);

expect(scopes1.has(scopes2)).toBeFalsy();
});
});
3 changes: 2 additions & 1 deletion src/base_types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {SessionStorage} from './auth/session';
import {AuthScopes} from './auth/scopes';

export interface ContextParams {
API_KEY: string;
API_SECRET_KEY: string;
SCOPES: string[];
SCOPES: string[] | AuthScopes;
HOST_NAME: string;
API_VERSION: ApiVersion;
IS_EMBEDDED_APP: boolean;
Expand Down
15 changes: 12 additions & 3 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as ShopifyErrors from './error';
import {SessionStorage, MemorySessionStorage} from './auth/session';
import {ApiVersion, ContextParams} from './base_types';
import {AuthScopes} from './auth/scopes';

interface ContextInterface extends ContextParams {
SESSION_STORAGE: SessionStorage;
SCOPES: AuthScopes;

/**
* Sets up the Shopify API Library to be able to integrate with Shopify and run authenticated commands.
Expand All @@ -26,14 +28,21 @@ interface ContextInterface extends ContextParams {
const Context: ContextInterface = {
API_KEY: '',
API_SECRET_KEY: '',
SCOPES: [],
SCOPES: new AuthScopes([]),
HOST_NAME: '',
API_VERSION: ApiVersion.Unstable,
IS_EMBEDDED_APP: true,
IS_PRIVATE_APP: false,
SESSION_STORAGE: new MemorySessionStorage(),

initialize(params: ContextParams): void {
let scopes: AuthScopes;
if (params.SCOPES instanceof AuthScopes) {
scopes = params.SCOPES;
} else {
scopes = new AuthScopes(params.SCOPES);
}

// Make sure that the essential params actually have content in them
const missing: string[] = [];
if (!params.API_KEY.length) {
Expand All @@ -42,7 +51,7 @@ const Context: ContextInterface = {
if (!params.API_SECRET_KEY.length) {
missing.push('API_SECRET_KEY');
}
if (!params.SCOPES.length) {
if (!scopes.toArray().length) {
missing.push('SCOPES');
}
if (!params.HOST_NAME.length) {
Expand All @@ -57,7 +66,7 @@ const Context: ContextInterface = {

this.API_KEY = params.API_KEY;
this.API_SECRET_KEY = params.API_SECRET_KEY;
this.SCOPES = params.SCOPES;
this.SCOPES = scopes;
this.HOST_NAME = params.HOST_NAME;
this.API_VERSION = params.API_VERSION;
this.IS_EMBEDDED_APP = params.IS_EMBEDDED_APP;
Expand Down
2 changes: 1 addition & 1 deletion src/test/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('Context object', () => {

expect(Context.API_KEY).toEqual(validParams.API_KEY);
expect(Context.API_SECRET_KEY).toEqual(validParams.API_SECRET_KEY);
expect(Context.SCOPES).toEqual(validParams.SCOPES);
expect(Context.SCOPES.equals(validParams.SCOPES)).toBeTruthy();
expect(Context.HOST_NAME).toEqual(validParams.HOST_NAME);
});

Expand Down

0 comments on commit 81895e4

Please # to comment.