-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathenforcer.ts
More file actions
225 lines (202 loc) · 8.07 KB
/
Copy pathenforcer.ts
File metadata and controls
225 lines (202 loc) · 8.07 KB
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import axios, { AxiosInstance } from 'axios';
import { Logger } from 'winston';
import { IPermitConfig } from '../config';
import { CheckConfig, Context, ContextStore } from '../utils/context';
import { AxiosLoggingInterceptor } from '../utils/http-logger';
import { IAction, IResource, IUser, OpaDecisionResult, PolicyDecision } from './interfaces';
const RESOURCE_DELIMITER = ':';
function isString(x: any): x is string {
return typeof x === 'string';
}
export class PermitError extends Error {
constructor(message: string) {
super(message);
this.name = 'PermitError';
}
}
export class PermitConnectionError extends PermitError {
constructor(message: string) {
super(message);
this.name = 'PermitConnectionError';
}
}
export class PermitPDPStatusError extends PermitError {
constructor(message: string) {
super(message);
this.name = 'PermitPDPStatusError';
}
}
export interface IEnforcer {
/**
* Checks if a `user` is authorized to perform an `action` on a `resource` within the specified context.
*
* @param user - The user object representing the user.
* @param action - The action to be performed on the resource.
* @param resource - The resource object representing the resource.
* @param context - The context object representing the context in which the action is performed.
* @returns `true` if the user is authorized, `false` otherwise.
* @throws {@link PermitConnectionError} if an error occurs while sending the authorization request to the PDP.
* @throws {@link PermitPDPStatusError} if received a response with unexpected status code from the PDP.
*/
check(
user: IUser | string,
action: IAction,
resource: IResource | string,
context?: Context,
config?: CheckConfig,
): Promise<boolean>;
}
/**
* The {@link Enforcer} class is responsible for performing permission checks against the PDP.
* It implements the {@link IEnforcer} interface.
*/
export class Enforcer implements IEnforcer {
public contextStore: ContextStore; // cross-query context (global context)
private client: AxiosInstance;
/**
* Creates an instance of the Enforcer class.
* @param config - The configuration object for the Permit SDK.
* @param logger - The logger instance for logging.
*/
constructor(private config: IPermitConfig, private logger: Logger) {
const version = process.env.npm_package_version ?? 'unknown';
this.client = axios.create({
baseURL: `${this.config.pdp}/`,
headers: {
'X-Permit-SDK-Version': `node:${version}`,
},
});
this.logger = logger;
AxiosLoggingInterceptor.setupInterceptor(this.client, this.logger);
this.contextStore = new ContextStore();
}
/**
* Checks if a `user` is authorized to perform an `action` on a `resource` within the specified context.
*
* @param user - The user object representing the user.
* @param action - The action to be performed on the resource.
* @param resource - The resource object representing the resource.
* @param context - The context object representing the context in which the action is performed.
* @returns `true` if the user is authorized, `false` otherwise.
* @throws {@link PermitConnectionError} if an error occurs while sending the authorization request to the PDP.
* @throws {@link PermitPDPStatusError} if received a response with unexpected status code from the PDP.
*/
public async check(
user: IUser | string,
action: IAction,
resource: IResource | string,
context: Context = {}, // context provided specifically for this query
config: CheckConfig = {},
): Promise<boolean> {
return await this.checkWithExceptions(user, action, resource, context, config).catch((err) => {
const shouldThrow =
config.throwOnError === undefined ? this.config.throwOnError : config.throwOnError;
if (shouldThrow) {
throw err;
} else {
this.logger.error(err);
return false;
}
});
}
private async checkWithExceptions(
user: IUser | string,
action: IAction,
resource: IResource | string,
context: Context = {}, // context provided specifically for this query
config: CheckConfig = {},
): Promise<boolean> {
const normalizedUser: IUser = isString(user) ? { key: user } : user;
const checkTimeout = config.timeout || this.config.timeout;
const resourceObj = isString(resource) ? Enforcer.resourceFromString(resource) : resource;
const normalizedResource: IResource = this.normalizeResource(resourceObj);
const tenant = resourceObj.tenant || 'default';
const queryContext = this.contextStore.getDerivedContext(context);
const input = {
user: normalizedUser,
action: action,
resource: normalizedResource,
context: queryContext,
};
return await this.client
.post<PolicyDecision | OpaDecisionResult>('allowed', input, {
headers: {
Authorization: `Bearer ${this.config.token}`,
'X-Tenant-ID': tenant,
},
timeout: checkTimeout,
})
.then((response) => {
if (response.status !== 200) {
throw new PermitPDPStatusError(`Permit.check() got an unexpected status code: ${response.status}, please check your SDK init and make sure the PDP sidecar is configured correctly. \n\
Read more about setting up the PDP at https://docs.permit.io`);
}
const decision =
('allow' in response.data ? response.data.allow : response.data.result.allow) || false;
this.logger.info(
`permit.check(${Enforcer.userRepr(normalizedUser)}, ${action}, ${Enforcer.resourceRepr(
resourceObj,
)}) = ${decision}`,
);
return decision;
})
.catch((error) => {
const errorMessage = `Error in permit.check(${Enforcer.userRepr(
normalizedUser,
)}, ${action}, ${Enforcer.resourceRepr(resourceObj)})`;
if (axios.isAxiosError(error)) {
const errorStatusCode: string = error.response?.status.toString() || '';
const errorDetails: string = error?.response?.data
? JSON.stringify(error.response.data)
: error.message;
this.logger.error(`[${errorStatusCode}] ${errorMessage}, err: ${errorDetails}`);
} else {
this.logger.error(`${errorMessage}\n${error}`);
}
throw new PermitConnectionError(`Permit SDK got error: \n ${error.message} \n
and cannot connect to the PDP, please check your configuration and make sure the PDP is running at ${this.config.pdp} and accepting requests. \n
Read more about setting up the PDP at https://docs.permit.io`);
});
}
// TODO: remove this eventually, once we decide on finalized structure of AuthzQuery
private normalizeResource(resource: IResource): IResource {
const normalizedResource: IResource = Object.assign({}, resource);
// if tenant is empty, we might auto-set the default tenant according to config
if (!normalizedResource.tenant && this.config.multiTenancy.useDefaultTenantIfEmpty) {
normalizedResource.tenant = this.config.multiTenancy.defaultTenant;
}
return normalizedResource;
}
private static userRepr(user: IUser): string {
if (user.attributes || user.email) {
return JSON.stringify(user);
}
return user.key;
}
private static resourceRepr(resource: IResource): string {
if (resource.attributes && resource.attributes.length > 0) {
return JSON.stringify(resource);
}
let resourceRepr = '';
if (resource.tenant) {
resourceRepr += `${resource.tenant}/`;
}
resourceRepr += `${resource.type}:${resource.key ?? '*'}`;
return resourceRepr;
}
private static resourceFromString(resource: string): IResource {
const parts = resource.split(RESOURCE_DELIMITER);
if (parts.length < 1 || parts.length > 2) {
throw Error(`permit.check() got invalid resource string: '${resource}'`);
}
return {
type: parts[0],
key: parts.length > 1 ? parts[1] : undefined,
};
}
public getMethods(): IEnforcer {
return {
check: this.check.bind(this),
};
}
}