handler.ts 20.9 KB
Newer Older
Diego Molteni's avatar
Diego Molteni committed
1
// ============================================================================
2
// Copyright 2017-2021, Schlumberger
Diego Molteni's avatar
Diego Molteni committed
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ============================================================================

import { Request as expRequest, Response as expResponse } from 'express';
18
import { Auth, AuthGroups, AuthRoles, UserRoles } from '../../auth';
Diego Molteni's avatar
Diego Molteni committed
19
20
import { Config } from '../../cloud';
import { JournalFactoryTenantClient } from '../../cloud/journal';
21
import { SeistoreFactory } from '../../cloud/seistore';
Diego Molteni's avatar
Diego Molteni committed
22
import { Error, Feature, FeatureFlags, Response, Utils } from '../../shared';
23
import { ISDPathModel } from '../../shared/sdpath';
24
import { DatasetDAO, DatasetModel } from '../dataset';
25
import { SubProjectDAO, SubprojectGroups, SubProjectModel } from '../subproject';
26
import { ISubProjectModel } from '../subproject/model';
27
import { TenantDAO, TenantGroups, TenantModel } from '../tenant';
28
import { ITenantModel } from '../tenant/model';
Diego Molteni's avatar
Diego Molteni committed
29
30
31
import { UserOP } from './optype';
import { UserParser } from './parser';

32

Diego Molteni's avatar
Diego Molteni committed
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
export class UserHandler {

    // handler for the [ /user ] endpoints
    public static async handler(req: expRequest, res: expResponse, op: UserOP) {

        try {

            // subproject endpoints are not available with impersonation token
            if (Auth.isImpersonationToken(req.headers.authorization)) {
                throw (Error.make(Error.Status.PERMISSION_DENIED,
                    'user endpoints not available with an impersonation token as Auth credentials.'));
            }

            if (op === UserOP.Add) {
                Response.writeOK(res, await this.addUser(req));
            } else if (op === UserOP.Remove) {
                Response.writeOK(res, await this.removeUser(req));
            } else if (op === UserOP.List) {
                Response.writeOK(res, await this.listUsers(req));
            } else if (op === UserOP.Roles) {
                Response.writeOK(res, await this.rolesUser(req));
            } else { throw (Error.make(Error.Status.UNKNOWN, 'Internal Server Error')); }

        } catch (error) { Response.writeError(res, error); }

    }

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
    private static async addUserToGroups(groups: string[], tenantEsd: string,
        userEmail: string, req: expRequest, role: UserRoles) {

        await Promise.all(groups.map(async group => {
            try {
                await AuthGroups.addUserToGroup(
                    req.headers.authorization, group, userEmail, tenantEsd,
                    req[Config.DE_FORWARD_APPKEY], role);
                return;
            } catch (e) {
                // If the error code is 400, retry adding the user as a member.
                // This would aid in adding a group email to the admin group.
                // Entitlements svc currently only allows one group to be added inside another
                // if the role is member
                if (e.error && e.error.code === 400) {
                    await AuthGroups.addUserToGroup(req.headers.authorization,
                        group, userEmail, tenantEsd, req[Config.DE_FORWARD_APPKEY], UserRoles.Member);
77
78
                } else if (e.error && e.error.code === 409) {
                    return; // If the user already exist -> return 200 (making the call idempotent)
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
                } else {
                    throw e;
                }
            }

        }));
    }


    private static async addUserAsAdmin(adminGroups: string[], viewerGroups: string[],
        tenant: TenantModel, req: expRequest, userEmail: string) {

        await this.addUserToGroups(adminGroups.concat(viewerGroups), tenant.esd, userEmail, req, UserRoles.Owner);

    }

    private static async addUserAsViewer(viewerGroups: string[], tenant: TenantModel,
        req: expRequest, userEmail: string) {

        await this.addUserToGroups(viewerGroups, tenant.esd, userEmail, req, UserRoles.Member);

    }

102
    // Add a user to a tenant or a subproject or a dataset
Diego Molteni's avatar
Diego Molteni committed
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
    private static async addUser(req: expRequest) {

        if (!FeatureFlags.isEnabled(Feature.AUTHORIZATION)) return {};

        // parse user request
        const userInput = UserParser.addUser(req);
        const sdPath = userInput.sdPath;
        let userEmail = userInput.email;
        const userGroupRole = userInput.groupRole;

        // This method is temporary required by slb during the migration of sauth from v1 to v2
        // The method replace slb.com domain name with delfiserviceaccount.com.t
        // Temporary hardcoded can be removed on 01/22 when sauth v1 will be dismissed.
        // Others service domain won't be affected by this call
        userEmail = Utils.checkSauthV1EmailDomainName(userEmail);

119
        // retrieve the tenant information
Diego Molteni's avatar
Diego Molteni committed
120
121
        const tenant = await TenantDAO.get(sdPath.tenant);

122
123
124
125
126
        if (!sdPath.subproject) {
            throw (Error.make(Error.Status.BAD_REQUEST,
                `The specified SDMS URI is not a subproject or a dataset.
                    Users cannot be managed at the tenant level`));
        }
127

128
129
        const journalClient = JournalFactoryTenantClient.get(tenant);
        const subproject = await SubProjectDAO.get(journalClient, tenant.name, sdPath.subproject);
130

131
        if (sdPath.dataset) {
132
            await UserHandler.addUserToDatasetGroups(subproject, sdPath, userGroupRole, req, tenant, userEmail);
133

134
135
136
137
138
        } else if (sdPath.subproject) {
            await UserHandler.addUserToSubprojectGroups(tenant, subproject, userGroupRole, req, userEmail);
        }

    }
139

140
141
142
143
    // ACLs at the level of dataset
    private static async addUserToDatasetGroups(subproject: ISubProjectModel,
        sdPath: ISDPathModel,
        userGroupRole: string, req, tenant: ITenantModel, userEmail: string) {
144
145


146
147
148
149
150
151
152
153
154
155
        if (subproject.access_policy !== Config.DATASET_ACCESS_POLICY) {
            throw Error.make(Error.Status.BAD_REQUEST, 'User cannot be added to the dataset ACLs as the subproject access policy is not set to dataset ');
        }

        const datasetModel: DatasetModel = {
            name: sdPath.dataset,
            subproject: sdPath.subproject,
            tenant: sdPath.tenant,
            path: sdPath.path
        } as DatasetModel;
156

157
158
159
160
161
162
163
164
165
166
167
168
169
        const journalClient = JournalFactoryTenantClient.get(tenant);

        const datasetOUT = subproject.enforce_key ?
            await DatasetDAO.getByKey(journalClient, datasetModel) :
            (await DatasetDAO.get(journalClient, datasetModel))[0];


        if (userGroupRole === AuthRoles.admin || userGroupRole === AuthRoles.editor) {
            if (datasetOUT.acls && 'admins' in datasetOUT.acls) {
                await this.addUserAsAdmin(datasetOUT.acls.admins, datasetOUT.acls.viewers,
                    tenant, req, userEmail);
            } else {
                throw Error.make(Error.Status.BAD_REQUEST, 'Dataset has no ACLs so the user cannot be added.');
170
            }
171
172
173
        } else if (userGroupRole === AuthRoles.viewer) {
            if (datasetOUT.acls && 'viewers' in datasetOUT.acls) {
                await this.addUserAsViewer(datasetOUT.acls.viewers, tenant, req, userEmail);
174

175
176
177
            } else {
                throw Error.make(Error.Status.BAD_REQUEST, 'Dataset has no ACLs so the user cannot be added.');
            }
178

179
        }
180
181
    }

182
183

    // ACLs at the level of subproject
184
185
186
187
188
189
190
191
192
193
194
195
196
197
    private static async addUserToSubprojectGroups(tenant: ITenantModel, subproject: ISubProjectModel,
        userGroupRole: string, req, userEmail: string, skipPolicyCheck = false) {

        const serviceGroupRegex = SubprojectGroups.serviceGroupNameRegExp(tenant.name, subproject.name);
        const subprojectAdminServiceGroups = subproject.acls.admins
            .filter((group) => group.match(serviceGroupRegex));
        const subprojectViewerServiceGroups = subproject.acls.viewers
            .filter((group) => group.match(serviceGroupRegex));

        const dataGroupRegex = SubprojectGroups.dataGroupNameRegExp(tenant.name, subproject.name);
        const adminSubprojectDataGroups = subproject.acls.admins.filter((group) => group.match(dataGroupRegex));
        const viewerSuprojectDataGroups = subproject.acls.viewers.filter(group => group.match(dataGroupRegex));


198
199
        const adminGroups = subprojectAdminServiceGroups.concat(adminSubprojectDataGroups);
        const viewerGroups = subprojectViewerServiceGroups.concat(viewerSuprojectDataGroups);
200

201
202


203
204
        if (userGroupRole === AuthRoles.admin || userGroupRole === AuthRoles.editor) {
            await this.addUserAsAdmin(adminGroups, viewerGroups, tenant, req, userEmail);
205
        }
206

207
208
        if (userGroupRole === AuthRoles.viewer) {
            await this.addUserAsViewer(viewerGroups, tenant, req, userEmail);
Diego Molteni's avatar
Diego Molteni committed
209
        }
210

Diego Molteni's avatar
Diego Molteni committed
211
212
    }

213
    // Remove a user from a tenant or a subproject or a dataset
Diego Molteni's avatar
Diego Molteni committed
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
    private static async removeUser(req: expRequest) {

        if (!FeatureFlags.isEnabled(Feature.AUTHORIZATION)) return {};

        // parse user request
        const userInput = UserParser.removeUser(req);
        const sdPath = userInput.sdPath;
        let userEmail = userInput.email;

        // This method is temporary required by slb during the migration of sauth from v1 to v2
        // The method replace slb.com domain name with delfiserviceaccount.com.t
        // Temporary hardcoded can be removed on 01/22 when sauth v1 will be dismissed.
        // Others service domain won't be affected
        userEmail = Utils.checkSauthV1EmailDomainName(userEmail);

        // user cannot remove himself
        // DE allows this operation, why do we disallow?
231
232
        if ((await SeistoreFactory.build(
            Config.CLOUDPROVIDER).getEmailFromTokenPayload(req.headers.authorization, true)) === userEmail) {
Diego Molteni's avatar
Diego Molteni committed
233
234
            throw (Error.make(Error.Status.BAD_REQUEST, 'A user cannot remove himself.'));
        }
235
        // retrieve the tenant information
Diego Molteni's avatar
Diego Molteni committed
236
237
        const tenant = await TenantDAO.get(sdPath.tenant);

238
239
        const journalClient = JournalFactoryTenantClient.get(tenant);
        const subproject = await SubProjectDAO.get(journalClient, tenant.name, sdPath.subproject);
Diego Molteni's avatar
Diego Molteni committed
240

Varunkumar Manohar's avatar
Varunkumar Manohar committed
241

242
243
244
245
246
247
248
        if (sdPath.dataset) {
            const datasetModel: DatasetModel = {
                name: sdPath.dataset,
                subproject: sdPath.subproject,
                tenant: sdPath.tenant,
                path: sdPath.path
            } as DatasetModel;
Varunkumar Manohar's avatar
Varunkumar Manohar committed
249

250
251
252
            const datasetOUT = subproject.enforce_key ?
                await DatasetDAO.getByKey(journalClient, datasetModel) :
                (await DatasetDAO.get(journalClient, datasetModel))[0];
Varunkumar Manohar's avatar
Varunkumar Manohar committed
253

254
            if (datasetOUT.acls) {
255
256
257
258
259

                const result = await UserHandler.listUsersInAuthGroups(datasetOUT.acls.admins, datasetOUT.acls.viewers,
                    req, tenant);

                await UserHandler.findAndRemoveUser(result, userEmail, datasetOUT, tenant, req);
Varunkumar Manohar's avatar
Varunkumar Manohar committed
260
261
            }

262

263
264
        } else if (sdPath.subproject) {

265
266
            const result = await UserHandler.listUsersInAuthGroups(subproject.acls.admins, subproject.acls.viewers,
                req, tenant);
Varunkumar Manohar's avatar
Varunkumar Manohar committed
267

268
            await UserHandler.findAndRemoveUser(result, userEmail, subproject, tenant, req);
Diego Molteni's avatar
Diego Molteni committed
269
270
271
272
273
274
275
276

        } else {
            throw (Error.make(Error.Status.BAD_REQUEST,
                'Please use Delfi portal to remove users from ' + tenant.name + ' tenant'));
        }

    }

277
278
279
280
281
    private static async findAndRemoveUser(userListInAuthGroups: any[], userEmail: string,
        datastoreEntity: DatasetModel | SubProjectModel, tenant: TenantModel, req) {
        const admins = new Set();
        const viewers = new Set();

282
283
284
285
286
287
288
289
        const regexForSvcAccount = /^[a-z0-9]*@delfiserviceaccount.com$/g;
        const svcAccountMatch = regexForSvcAccount.test(userEmail);

        if (svcAccountMatch) {
            const splitList = userEmail.split('@');
            userEmail = [splitList[0], '.slbclient.com', '@delfiserviceaccount.com'].join('');
        }

290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
        userListInAuthGroups.map(lst => {
            for (const ele of lst) {
                if (ele === 'admin' || ele === 'editor') {
                    admins.add(lst[0]);
                } else {
                    viewers.add(lst[0]);
                }

            }

        });

        if (admins.has(userEmail)) {
            await UserHandler.removeUserFromAuthGroups(datastoreEntity.acls.admins,
                datastoreEntity.acls.viewers, tenant, req, userEmail);

        } else if (viewers.has(userEmail)) {
            await UserHandler.removeUserFromAuthGroups([],
                datastoreEntity.acls.viewers, tenant, req, userEmail);

        }
        return;
    }

314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
    private static async removeUserFromAuthGroups(adminGroups: string[], viewerGroups: string[],
        tenant: ITenantModel, req, userEmail: string,
    ) {
        for (const group of adminGroups) {
            await this.doNotThrowIfNotMember(
                AuthGroups.removeUserFromGroup(req.headers.authorization, group, userEmail,
                    tenant.esd, req[Config.DE_FORWARD_APPKEY]));
        }

        for (const group of viewerGroups) {
            await this.doNotThrowIfNotMember(
                AuthGroups.removeUserFromGroup(req.headers.authorization, group, userEmail,
                    tenant.esd, req[Config.DE_FORWARD_APPKEY]));
        }
    }

Diego Molteni's avatar
Diego Molteni committed
330
331
332
333
334
335
336
337
    // list users and their roles in a subproject
    private static async listUsers(req: expRequest): Promise<string[][]> {

        if (!FeatureFlags.isEnabled(Feature.AUTHORIZATION)) return [];

        // parse user request
        const sdPath = UserParser.listUsers(req);

338
        // retrieve the tenant information
Diego Molteni's avatar
Diego Molteni committed
339
340
        const tenant = await TenantDAO.get(sdPath.tenant);

341
        const journalClient = JournalFactoryTenantClient.get(tenant);
Diego Molteni's avatar
Diego Molteni committed
342

343
        const subproject = await SubProjectDAO.get(journalClient, tenant.name, sdPath.subproject);
344
345
346



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
        if (sdPath.dataset) {

            const datasetModel: DatasetModel = {
                name: sdPath.dataset,
                subproject: sdPath.subproject,
                tenant: sdPath.tenant,
                path: sdPath.path
            } as DatasetModel;

            const datasetOUT = subproject.enforce_key ?
                await DatasetDAO.getByKey(journalClient, datasetModel) :
                (await DatasetDAO.get(journalClient, datasetModel))[0];

            if (datasetOUT.acls) {
                return await UserHandler.listUsersInAuthGroups(datasetOUT.acls.admins, datasetOUT.acls.viewers,
                    req, tenant);
363
            }
364
365
366
367
368
369


        } else if (sdPath.subproject) {
            return await UserHandler.listUsersInAuthGroups(subproject.acls.admins,
                subproject.acls.viewers, req, tenant);

370
        }
371
        return;
372
    }
373

374
    private static async listUsersInAuthGroups(admins: string[], viewers: string[], req, tenant: ITenantModel,) {
375

376
377
378
379
380
381
382
383
384
385
386
387
388
389
        let users = [];


        for (const adminGroup of admins) {
            const result = (await AuthGroups.listUsersInGroup(req.headers.authorization, adminGroup, tenant.esd,
                req[Config.DE_FORWARD_APPKEY]));
            users = users.concat(result.map((el) => [el.email, 'admin']));
        }


        for (const viewerGroup of viewers) {
            const result = (await AuthGroups.listUsersInGroup(req.headers.authorization, viewerGroup, tenant.esd,
                req[Config.DE_FORWARD_APPKEY]));
            users = users.concat(result.map((el) => [el.email, 'viewer']));
390
391
        }

392
        return users;
Diego Molteni's avatar
Diego Molteni committed
393
394
395
396
397
398
399
400
401
402
    }

    // retrieve the roles of a user
    private static async rolesUser(req: expRequest) {

        if (!FeatureFlags.isEnabled(Feature.AUTHORIZATION)) return {};

        // parse user request
        const sdPath = UserParser.rolesUser(req);

403
        // retrieve the tenant information
Diego Molteni's avatar
Diego Molteni committed
404
405
        const tenant = await TenantDAO.get(sdPath.tenant);

406
407
408
        // Check if user has read access
        await Auth.isUserRegistered(req.headers.authorization, tenant.esd, req[Config.DE_FORWARD_APPKEY]);

Diego Molteni's avatar
Diego Molteni committed
409
410
411
412
        // get the groups of the user
        const groups = await AuthGroups.getUserGroups(req.headers.authorization,
            tenant.esd, req[Config.DE_FORWARD_APPKEY]);

413
414
415
        // List of all group emails in which the user is member or a owner
        const groupEmailsOfUser = groups.map(group => group.email);

Diego Molteni's avatar
Diego Molteni committed
416
        const prefix = sdPath.subproject ?
Diego Molteni's avatar
Diego Molteni committed
417
418
            SubprojectGroups.serviceGroupPrefix(sdPath.tenant, sdPath.subproject) :
            TenantGroups.serviceGroupPrefix(sdPath.tenant);
Diego Molteni's avatar
Diego Molteni committed
419
420
421
422


        const journalClient = JournalFactoryTenantClient.get(tenant);

423
        const registeredSubprojects = (await SubProjectDAO.list(journalClient, sdPath.tenant));
Diego Molteni's avatar
Diego Molteni committed
424

425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453

        if (sdPath.dataset) {

            const subproject = await SubProjectDAO.get(journalClient, sdPath.tenant, sdPath.subproject);

            const datasetModel = {
                name: sdPath.dataset,
                tenant: sdPath.tenant,
                subproject: sdPath.subproject,
                path: sdPath.path
            } as DatasetModel;

            const datasetOUT = subproject.enforce_key ?
                await DatasetDAO.getByKey(journalClient, datasetModel) :
                (await DatasetDAO.get(journalClient, datasetModel))[0];

            const roles = [];

            if (datasetOUT.acls) {
                const validAdminGroupsForUser = datasetOUT.acls.admins.filter(grp => groupEmailsOfUser.includes(grp));
                const validViewerGroupsForUser = datasetOUT.acls.viewers.filter(grp => groupEmailsOfUser.includes(grp));

                if (validAdminGroupsForUser.length > 0) {
                    roles.push('/' + sdPath.dataset, 'admin');
                }

                if (validViewerGroupsForUser.length > 0) {
                    roles.push('/' + sdPath.dataset, 'viewer');
                }
454
            }
455
456
457
            return {
                'roles': roles
            };
458

459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
        } else if (sdPath.tenant) {

            // Concatenate all valid subproject admin groups
            const registeredSubprojectAdminGroups = registeredSubprojects.map(subproject =>
                subproject.acls.admins).flat(1);
            const registeredSubprojectViewerGroups = registeredSubprojects.map(
                subproject => subproject.acls.viewers).flat(1);

            // Find intersection of admin groups of all registered subprojects and the user group emails
            const validAdminGroupsForUser = registeredSubprojectAdminGroups.filter(grp =>
                groupEmailsOfUser.includes(grp));
            const validViewerGroupsForUser = registeredSubprojectViewerGroups.filter(
                grp => groupEmailsOfUser.includes(grp));

            let roles = [];
            for (const validAdminGroup of validAdminGroupsForUser) {
                if (validAdminGroup.startsWith('service')) {
                    roles.push(['/' + validAdminGroup.split('.')[4], 'admin']);
                    roles.push(['/' + validAdminGroup.split('.')[4], 'editor']);
                }
                else if (validAdminGroup.startsWith('data')) {
                    roles.push(['/' + validAdminGroup.split('.')[3], 'admin']);
                    roles.push(['/' + validAdminGroup.split('.')[3], 'editor']);
                }
483
            }
484
485
486
487
488
489
490
491

            for (const validViewerGroup of validViewerGroupsForUser) {
                if (validViewerGroup.startsWith('service')) {
                    roles.push(['/' + validViewerGroup.split('.')[4], 'viewer']);
                }
                else if (validViewerGroup.startsWith('data')) {
                    roles.push(['/' + validViewerGroup.split('.')[3], 'viewer']);
                }
492
493
            }

494
495
496
497
498
499
500
501
502
503
504
            // Remove duplicates from roles array where each element is array by itself
            const stringRolesArray = roles.map(role => JSON.stringify(role));
            const uniqueRolesStringArray = new Set(stringRolesArray);
            roles = Array.from(uniqueRolesStringArray, (ele) => JSON.parse(ele));

            if (sdPath.subproject) {
                const subprojectRoles = roles.filter((role) => role[0] === '/' + sdPath.subproject);
                return {
                    'roles': subprojectRoles
                };
            }
505
506

            return {
507
                'roles': roles
508
509
            };

510
        }
511

Diego Molteni's avatar
Diego Molteni committed
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
    }

    // do not throw if a user is not a member (fast remove users if not exist than check if exist and than remove it)
    private static async doNotThrowIfNotMember(methodToCall: any) {
        try {
            await methodToCall;
        } catch (error) {
            if (!(typeof error === 'object' &&
                typeof error.error === 'object' &&
                'message' in error.error &&
                (error.error.message as string).indexOf('Member not found'))) {
                throw (error);
            }
        }
    }

}