@guardian/permissions-client 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +8 -0
- package/README.md +30 -0
- package/package.json +30 -26
- package/src/index.test.ts +181 -0
- package/src/index.ts +21 -27
- package/src/model.ts +26 -6
- package/src/normalise.ts +20 -0
- package/tsconfig.emit.json +4 -0
- package/tsconfig.json +6 -2
- package/dist/declaration/index.d.ts +0 -16
- package/dist/declaration/model.d.ts +0 -16
- package/dist/index.js +0 -73
- package/dist/model.js +0 -2
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
This software is licensed under the Apache 2 license, quoted below.
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with
|
|
4
|
+
the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
|
|
6
|
+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
7
|
+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
|
|
8
|
+
language governing permissions and limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# `@guardian/permissions-client`
|
|
2
|
+
|
|
3
|
+
Client for reading Guardian user permissions data in Node.js backends
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @guardian/permissions-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
or
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @guardian/permissions-client
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
(`@aws-sdk/client-s3` and `@aws-sdk/credential-providers` must also be installed)
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { init } from "@guardian/permissions-client";
|
|
23
|
+
|
|
24
|
+
const { hasPermission } = init({ isRunningLocally: true });
|
|
25
|
+
|
|
26
|
+
const doesLiamHaveGridAccess = await hasPermission(
|
|
27
|
+
"grid_access",
|
|
28
|
+
"liam.duffy@guardian.co.uk",
|
|
29
|
+
);
|
|
30
|
+
```
|
package/package.json
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
2
|
+
"name": "@guardian/permissions-client",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Client for Guardian's permissions service",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/guardian/permissions",
|
|
7
|
+
"directory": "node-client"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/declaration/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@aws-sdk/client-s3": "3.1090.0",
|
|
18
|
+
"@aws-sdk/credential-providers": "^3.1090.0"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@aws-sdk/client-s3": "^3.1090.0",
|
|
22
|
+
"@aws-sdk/credential-providers": "^3.1090.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc --project tsconfig.emit.json",
|
|
26
|
+
"test": "node --test --experimental-test-module-mocks src/**/*.test.ts",
|
|
27
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
28
|
+
"format:ci": "prettier --check \"src/**/*.ts\"",
|
|
29
|
+
"lint": "eslint src/** --ext .ts"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mock, test } from "node:test";
|
|
4
|
+
import type * as ClientModule from "./index.ts";
|
|
5
|
+
import type { PermissionsCache } from "./model.ts";
|
|
6
|
+
|
|
7
|
+
const mockPermissionsCache: PermissionsCache = [
|
|
8
|
+
{
|
|
9
|
+
permission: {
|
|
10
|
+
name: "permission1",
|
|
11
|
+
app: "app1",
|
|
12
|
+
defaultValue: false,
|
|
13
|
+
},
|
|
14
|
+
overrides: [
|
|
15
|
+
{
|
|
16
|
+
userId: "user1",
|
|
17
|
+
active: true,
|
|
18
|
+
isCasualNotOnShift: false,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
userId: "user2",
|
|
22
|
+
active: false,
|
|
23
|
+
isCasualNotOnShift: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
userId: "user3",
|
|
27
|
+
active: true,
|
|
28
|
+
isCasualNotOnShift: true,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
permission: {
|
|
34
|
+
name: "permission2",
|
|
35
|
+
app: "app1",
|
|
36
|
+
defaultValue: false,
|
|
37
|
+
},
|
|
38
|
+
overrides: [
|
|
39
|
+
{
|
|
40
|
+
userId: "user1",
|
|
41
|
+
active: true,
|
|
42
|
+
isCasualNotOnShift: false,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
userId: "user2",
|
|
46
|
+
active: true,
|
|
47
|
+
isCasualNotOnShift: false,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
userId: "user3",
|
|
51
|
+
active: false,
|
|
52
|
+
isCasualNotOnShift: false,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
let currentMockPermissionsCache = mockPermissionsCache;
|
|
59
|
+
|
|
60
|
+
const originalCredentialProviders =
|
|
61
|
+
await import("@aws-sdk/credential-providers");
|
|
62
|
+
|
|
63
|
+
mock.module("@aws-sdk/credential-providers", {
|
|
64
|
+
exports: {
|
|
65
|
+
...originalCredentialProviders,
|
|
66
|
+
fromNodeProviderChain: () => () => Promise.resolve({}),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const originalClientS3 = await import("@aws-sdk/client-s3");
|
|
71
|
+
|
|
72
|
+
mock.module("@aws-sdk/client-s3", {
|
|
73
|
+
exports: {
|
|
74
|
+
...originalClientS3,
|
|
75
|
+
S3: class {
|
|
76
|
+
getObject() {
|
|
77
|
+
return Promise.resolve({
|
|
78
|
+
Body: {
|
|
79
|
+
transformToString: () =>
|
|
80
|
+
Promise.resolve(JSON.stringify(currentMockPermissionsCache)),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// This is required to get a fresh instance of the module for each test, with a new cache state.
|
|
89
|
+
const importIndex = () =>
|
|
90
|
+
import(`./index.ts?cache-bust=${randomUUID()}`) as Promise<
|
|
91
|
+
typeof ClientModule
|
|
92
|
+
>;
|
|
93
|
+
|
|
94
|
+
test("hasPermission() returns true for a user with active: true in overrides", async () => {
|
|
95
|
+
const { init } = await importIndex();
|
|
96
|
+
const client = init();
|
|
97
|
+
const result = await client.hasPermission("permission1", "user1");
|
|
98
|
+
|
|
99
|
+
assert.equal(result, true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("hasPermission() returns false for a user with active: false in overrides", async () => {
|
|
103
|
+
const { init } = await importIndex();
|
|
104
|
+
const client = init();
|
|
105
|
+
const result = await client.hasPermission("permission1", "user2");
|
|
106
|
+
|
|
107
|
+
assert.equal(result, false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("hasPermission() returns false for a user who is not present in overrides", async () => {
|
|
111
|
+
const { init } = await importIndex();
|
|
112
|
+
const client = init();
|
|
113
|
+
const result = await client.hasPermission("permission1", "user9001");
|
|
114
|
+
|
|
115
|
+
assert.equal(result, false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("hasPermission() returns false for a user with active: true but isCasualNotOnShift: true in overrides", async () => {
|
|
119
|
+
const { init } = await importIndex();
|
|
120
|
+
const client = init();
|
|
121
|
+
const result = await client.hasPermission("permission1", "user3");
|
|
122
|
+
|
|
123
|
+
assert.equal(result, false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("hasPermission() returns the latest permission information when the cache expires", async () => {
|
|
127
|
+
mock.timers.enable();
|
|
128
|
+
|
|
129
|
+
const { init } = await importIndex();
|
|
130
|
+
const client = init();
|
|
131
|
+
const result = await client.hasPermission("permission1", "user1");
|
|
132
|
+
assert.equal(result, true);
|
|
133
|
+
|
|
134
|
+
const updatedPermissionsCache: PermissionsCache = [
|
|
135
|
+
{
|
|
136
|
+
permission: {
|
|
137
|
+
name: "permission1",
|
|
138
|
+
app: "app1",
|
|
139
|
+
defaultValue: false,
|
|
140
|
+
},
|
|
141
|
+
overrides: [
|
|
142
|
+
{
|
|
143
|
+
userId: "user1",
|
|
144
|
+
active: false,
|
|
145
|
+
isCasualNotOnShift: false,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
currentMockPermissionsCache = updatedPermissionsCache;
|
|
152
|
+
|
|
153
|
+
mock.timers.tick(30_000);
|
|
154
|
+
|
|
155
|
+
const resultAfter30Seconds = await client.hasPermission(
|
|
156
|
+
"permission1",
|
|
157
|
+
"user1",
|
|
158
|
+
);
|
|
159
|
+
assert.equal(resultAfter30Seconds, true);
|
|
160
|
+
|
|
161
|
+
mock.timers.tick(30_001);
|
|
162
|
+
const resultAfter60Seconds = await client.hasPermission(
|
|
163
|
+
"permission1",
|
|
164
|
+
"user1",
|
|
165
|
+
);
|
|
166
|
+
assert.equal(resultAfter60Seconds, false);
|
|
167
|
+
|
|
168
|
+
// Reset to original state
|
|
169
|
+
currentMockPermissionsCache = mockPermissionsCache;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("listPermissions() returns list of valid permissions for user", async () => {
|
|
173
|
+
const { init } = await importIndex();
|
|
174
|
+
const client = init();
|
|
175
|
+
const user1Result = await client.listPermissions("user1");
|
|
176
|
+
assert.deepEqual(user1Result, ["permission1", "permission2"]);
|
|
177
|
+
const user2Result = await client.listPermissions("user2");
|
|
178
|
+
assert.deepEqual(user2Result, ["permission2"]);
|
|
179
|
+
const user3Result = await client.listPermissions("user3");
|
|
180
|
+
assert.deepEqual(user3Result, []);
|
|
181
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { S3 } from "@aws-sdk/client-s3";
|
|
2
2
|
import { fromIni, fromNodeProviderChain } from "@aws-sdk/credential-providers";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
NormalisedPermissionsCache,
|
|
5
|
+
PermissionsCache,
|
|
6
|
+
UserEntry,
|
|
7
|
+
} from "./model.ts";
|
|
8
|
+
import { normalisePermissionsCache } from "./normalise.ts";
|
|
4
9
|
|
|
5
10
|
interface ClientConfig {
|
|
6
11
|
stage?: string;
|
|
@@ -13,25 +18,19 @@ interface ClientConfig {
|
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
type PermissionsResult = {
|
|
16
|
-
allPermissions:
|
|
21
|
+
allPermissions: NormalisedPermissionsCache;
|
|
17
22
|
loadTime: number | null;
|
|
18
23
|
};
|
|
19
24
|
|
|
20
25
|
let permissionsResult: PermissionsResult = {
|
|
21
|
-
allPermissions:
|
|
26
|
+
allPermissions: {},
|
|
22
27
|
loadTime: null,
|
|
23
28
|
};
|
|
24
29
|
let permissionsRequest: Promise<PermissionsResult> =
|
|
25
30
|
Promise.resolve(permissionsResult);
|
|
26
31
|
|
|
27
|
-
const permissionValidForUser = (
|
|
28
|
-
|
|
29
|
-
userId: string,
|
|
30
|
-
) => {
|
|
31
|
-
const userOverride = permission.overrides.find((o) => o.userId === userId);
|
|
32
|
-
|
|
33
|
-
return !!(userOverride?.active && !userOverride.isCasualNotOnShift);
|
|
34
|
-
};
|
|
32
|
+
const permissionValidForUser = (userEntry: UserEntry) =>
|
|
33
|
+
!!(userEntry.active && !userEntry.isCasualNotOnShift);
|
|
35
34
|
|
|
36
35
|
export function init({
|
|
37
36
|
stage = "CODE",
|
|
@@ -62,16 +61,18 @@ export function init({
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
const transformedBody = await Body.transformToString();
|
|
65
|
-
const
|
|
64
|
+
const permissionsCache = JSON.parse(transformedBody) as PermissionsCache;
|
|
65
|
+
const allPermissions = normalisePermissionsCache(permissionsCache);
|
|
66
66
|
|
|
67
67
|
return { allPermissions, loadTime: Date.now() };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
async function getOrRefresh() {
|
|
71
71
|
const latestResult = await permissionsRequest;
|
|
72
|
-
const cacheExpired =
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
const cacheExpired =
|
|
73
|
+
latestResult.loadTime !== null
|
|
74
|
+
? Date.now() > latestResult.loadTime + cacheIntervalMs
|
|
75
|
+
: true;
|
|
75
76
|
|
|
76
77
|
if (cacheExpired) {
|
|
77
78
|
try {
|
|
@@ -90,23 +91,16 @@ export function init({
|
|
|
90
91
|
|
|
91
92
|
async function hasPermission(permissionName: string, userId: string) {
|
|
92
93
|
const permissionsCache = await getOrRefresh();
|
|
93
|
-
const
|
|
94
|
-
(p) => p.permission.name === permissionName,
|
|
95
|
-
);
|
|
94
|
+
const userEntry = permissionsCache[userId]?.[permissionName];
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
throw Error(`Permission data for "${permissionName}" not found`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return permissionValidForUser(permissionData, userId);
|
|
96
|
+
return userEntry ? permissionValidForUser(userEntry) : false;
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
async function listPermissions(userId: string) {
|
|
105
100
|
const permissionsCache = await getOrRefresh();
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
.map((p) => p.permission.name);
|
|
101
|
+
return Object.entries(permissionsCache[userId] ?? {})
|
|
102
|
+
.filter(([, userEntry]) => permissionValidForUser(userEntry))
|
|
103
|
+
.flatMap(([permissionName]) => permissionName);
|
|
110
104
|
}
|
|
111
105
|
|
|
112
106
|
function storeIsEmpty() {
|
package/src/model.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
interface UserSetting {
|
|
1
|
+
// Types from S3
|
|
2
|
+
interface UserOverride {
|
|
4
3
|
userId: string;
|
|
5
4
|
active: boolean;
|
|
6
5
|
isCasualNotOnShift: boolean;
|
|
@@ -12,9 +11,30 @@ interface PermissionDefinition {
|
|
|
12
11
|
defaultValue: boolean;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
interface PermissionWithOverrides {
|
|
16
15
|
permission: PermissionDefinition;
|
|
17
|
-
overrides:
|
|
16
|
+
overrides: UserOverride[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type PermissionsCache = PermissionWithOverrides[];
|
|
20
|
+
|
|
21
|
+
// Normalised types
|
|
22
|
+
export interface UserEntry {
|
|
23
|
+
permission: PermissionDefinition;
|
|
24
|
+
active: boolean;
|
|
25
|
+
isCasualNotOnShift: boolean;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
/**
|
|
29
|
+
* I would find an interface more readable here than a record, but @typescript-eslint/consistent-indexed-object-style says no
|
|
30
|
+
* Here's what it would look like as an interface, with named types for the keys
|
|
31
|
+
* interface NormalisedPermissionsCache {
|
|
32
|
+
* [userId: string]: {
|
|
33
|
+
* [permissionName: string]: UserEntry;
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
export type NormalisedPermissionsCache = Record<
|
|
38
|
+
string,
|
|
39
|
+
Record<string, UserEntry>
|
|
40
|
+
>;
|
package/src/normalise.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NormalisedPermissionsCache, PermissionsCache } from "./model.ts";
|
|
2
|
+
|
|
3
|
+
// Invert shape of permissions cache, so we can look up permissions data without looping
|
|
4
|
+
export const normalisePermissionsCache = (
|
|
5
|
+
permissionsCache: PermissionsCache,
|
|
6
|
+
): NormalisedPermissionsCache => {
|
|
7
|
+
return permissionsCache.reduce<NormalisedPermissionsCache>((acc, p) => {
|
|
8
|
+
const { permission, overrides } = p;
|
|
9
|
+
overrides.forEach(({ userId, active, isCasualNotOnShift }) => {
|
|
10
|
+
acc[userId] ??= {};
|
|
11
|
+
acc[userId][permission.name] = {
|
|
12
|
+
permission,
|
|
13
|
+
active,
|
|
14
|
+
isCasualNotOnShift,
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return acc;
|
|
19
|
+
}, {});
|
|
20
|
+
};
|
package/tsconfig.json
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
"extends": "@guardian/tsconfig/tsconfig.json",
|
|
3
3
|
"allowImportingTsExtensions": true,
|
|
4
4
|
"compilerOptions": {
|
|
5
|
-
"
|
|
5
|
+
"target": "esnext",
|
|
6
|
+
"module": "nodenext",
|
|
7
|
+
"moduleResolution": "nodenext",
|
|
6
8
|
"types": [
|
|
7
9
|
"vitest/globals",
|
|
8
10
|
"node"
|
|
9
11
|
],
|
|
10
|
-
"declaration": true,
|
|
11
12
|
"rootDir": "./src",
|
|
12
13
|
"outDir": "dist",
|
|
14
|
+
"declaration": true,
|
|
13
15
|
"declarationDir": "dist/declaration",
|
|
16
|
+
"rewriteRelativeImportExtensions": true,
|
|
17
|
+
"erasableSyntaxOnly": true,
|
|
14
18
|
},
|
|
15
19
|
"include": ["src/**/*"],
|
|
16
20
|
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
interface ClientConfig {
|
|
2
|
-
stage?: string;
|
|
3
|
-
region?: string;
|
|
4
|
-
bucket?: string;
|
|
5
|
-
file?: string;
|
|
6
|
-
cacheIntervalMs?: number;
|
|
7
|
-
isRunningLocally?: boolean;
|
|
8
|
-
localProfile?: string;
|
|
9
|
-
}
|
|
10
|
-
export declare function init({ stage, region, bucket, file, cacheIntervalMs, // 1 minute,
|
|
11
|
-
isRunningLocally, localProfile, }?: ClientConfig): {
|
|
12
|
-
hasPermission: (permissionName: string, userId: string) => Promise<boolean>;
|
|
13
|
-
listPermissions: (userId: string) => Promise<string[]>;
|
|
14
|
-
storeIsEmpty: () => boolean;
|
|
15
|
-
};
|
|
16
|
-
export {};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
interface UserSetting {
|
|
2
|
-
userId: string;
|
|
3
|
-
active: boolean;
|
|
4
|
-
isCasualNotOnShift: boolean;
|
|
5
|
-
}
|
|
6
|
-
interface PermissionDefinition {
|
|
7
|
-
name: string;
|
|
8
|
-
app: string;
|
|
9
|
-
defaultValue: boolean;
|
|
10
|
-
}
|
|
11
|
-
export interface PermissionWithUsers {
|
|
12
|
-
permission: PermissionDefinition;
|
|
13
|
-
overrides: UserSetting[];
|
|
14
|
-
}
|
|
15
|
-
export type PermissionsCache = PermissionWithUsers[];
|
|
16
|
-
export {};
|
package/dist/index.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { S3 } from "@aws-sdk/client-s3";
|
|
2
|
-
import { fromIni, fromNodeProviderChain } from "@aws-sdk/credential-providers";
|
|
3
|
-
let permissionsResult = {
|
|
4
|
-
allPermissions: [],
|
|
5
|
-
loadTime: null,
|
|
6
|
-
};
|
|
7
|
-
let permissionsRequest = Promise.resolve(permissionsResult);
|
|
8
|
-
const permissionValidForUser = (permission, userId) => {
|
|
9
|
-
const userOverride = permission.overrides.find((o) => o.userId === userId);
|
|
10
|
-
return !!(userOverride?.active && !userOverride.isCasualNotOnShift);
|
|
11
|
-
};
|
|
12
|
-
export function init({ stage = "CODE", region = "eu-west-1", bucket = "permissions-cache", file = "permissions.json", cacheIntervalMs = 60_000, // 1 minute,
|
|
13
|
-
isRunningLocally = false, localProfile = "workflow", } = {}) {
|
|
14
|
-
const awsConfig = {
|
|
15
|
-
region,
|
|
16
|
-
credentials: isRunningLocally
|
|
17
|
-
? fromIni({ profile: localProfile })
|
|
18
|
-
: fromNodeProviderChain(),
|
|
19
|
-
};
|
|
20
|
-
const s3 = new S3(awsConfig);
|
|
21
|
-
async function loadPermissions() {
|
|
22
|
-
const { Body } = await s3.getObject({
|
|
23
|
-
Bucket: bucket,
|
|
24
|
-
Key: `${stage}/${file}`,
|
|
25
|
-
});
|
|
26
|
-
if (!Body) {
|
|
27
|
-
throw Error("Could not fetch permissions");
|
|
28
|
-
}
|
|
29
|
-
const transformedBody = await Body.transformToString();
|
|
30
|
-
const allPermissions = JSON.parse(transformedBody);
|
|
31
|
-
return { allPermissions, loadTime: Date.now() };
|
|
32
|
-
}
|
|
33
|
-
async function getOrRefresh() {
|
|
34
|
-
const latestResult = await permissionsRequest;
|
|
35
|
-
const cacheExpired = latestResult.loadTime
|
|
36
|
-
? Date.now() > latestResult.loadTime + cacheIntervalMs
|
|
37
|
-
: true;
|
|
38
|
-
if (cacheExpired) {
|
|
39
|
-
try {
|
|
40
|
-
permissionsRequest = loadPermissions();
|
|
41
|
-
permissionsResult = await permissionsRequest;
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
console.error("Refreshing permissions cache failed with error, so keeping stale cache...", error);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return permissionsResult.allPermissions;
|
|
48
|
-
}
|
|
49
|
-
async function hasPermission(permissionName, userId) {
|
|
50
|
-
const permissionsCache = await getOrRefresh();
|
|
51
|
-
const permissionData = permissionsCache.find((p) => p.permission.name === permissionName);
|
|
52
|
-
if (!permissionData) {
|
|
53
|
-
throw Error(`Permission data for "${permissionName}" not found`);
|
|
54
|
-
}
|
|
55
|
-
return permissionValidForUser(permissionData, userId);
|
|
56
|
-
}
|
|
57
|
-
async function listPermissions(userId) {
|
|
58
|
-
const permissionsCache = await getOrRefresh();
|
|
59
|
-
return permissionsCache
|
|
60
|
-
.filter((p) => permissionValidForUser(p, userId))
|
|
61
|
-
.map((p) => p.permission.name);
|
|
62
|
-
}
|
|
63
|
-
function storeIsEmpty() {
|
|
64
|
-
return permissionsResult.loadTime === null;
|
|
65
|
-
}
|
|
66
|
-
// Run getOrRefresh() immediately after init so that the cache is populated and ready for use
|
|
67
|
-
void getOrRefresh();
|
|
68
|
-
return {
|
|
69
|
-
hasPermission,
|
|
70
|
-
listPermissions,
|
|
71
|
-
storeIsEmpty,
|
|
72
|
-
};
|
|
73
|
-
}
|
package/dist/model.js
DELETED