File size: 4,994 Bytes
b39afbe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Copyright (c) 2023 MERCENARIES.AI PTE. LTD.
 * All rights reserved.
 */

import { type PureAbility, type Subject, defineAbility } from '@casl/ability';
import { Group, type User, EObjectAction, EObjectName } from 'omni-shared';
import { OMNITOOL_DOCUMENT_TYPES, type DBService } from '../services/DBService.js';
import { performance } from 'perf_hooks';
import assert from 'node:assert';

async function getGroupByMemberId(db: DBService, userId: string): Promise<Group[]> {
  const start = performance.now();
  const query = {
    _id: {
      $gte: `${Group.modelName}:`, // i.e. _id.startswith(userId + ':')
      $lt: `${Group.modelName}:\u10FFFF`
    },
    members: {
      $elemMatch: {
        id: userId
      }
    }
  };

  try {
    const result = (await db.find(query)) || [];
    const end = performance.now();
    omnilog.trace(`getGroupByMemberId(${userId}) took ${(end - start).toFixed()} ms`);
    return result;
  } catch (err) {
    const end = performance.now();
    omnilog.info(`getGroupByMemberId(${userId}) returned an error in ${(end - start).toFixed()} ms`);
    db.error(err);
    return [];
  }
}

async function setAcceptedTOS(db: DBService, user: User): Promise<number> {
  try {
    user.tosAccepted = Date.now();
    assert(user._id != null, 'User ID is null');
    //await db.put(JSON.parse(JSON.stringify(user)));
    const dbuserobj = await db.getDocumentById(OMNITOOL_DOCUMENT_TYPES.USER, user.id, [], false);
    // @ts-ignore
    dbuserobj.tosAccepted = user.tosAccepted;
    await db.putDocumentById(OMNITOOL_DOCUMENT_TYPES.USER, user.id, dbuserobj);
    return user.tosAccepted;
  } catch (err) {
    db.error(err);
    return 0;
  }
}

const loadUserPermission = async function (db: DBService, user: User) {
  const start = performance.now();
  const groups = await getGroupByMemberId(db, user.id);
  const groupIds = groups.map((group) => group.id);
  const permissions = groups.map((group) => group.permission).flat();

  // Add sharing permissions
  // @ts-ignore
  permissions.push(
    // User can edit their own details
    {
      action: [EObjectAction.READ, EObjectAction.UPDATE],
      subject: EObjectName.USER,
      conditions: { id: user.id }
    }
  );
  permissions.push(
    // Allow user to read workflows that are shared with them
    {
      action: [EObjectAction.READ],
      subject: EObjectName.WORKFLOW,
      conditions: { sharedWith: { $elemMatch: { id: user.id } } }
    }
  );

  permissions.push(
    // Allow user to read workflows that are shared with their team
    {
      action: [EObjectAction.READ],
      subject: EObjectName.WORKFLOW,
      conditions: { sharedWith: { $elemMatch: { id: { $in: groupIds.map((id) => id) } } } }
    }
  );
  // Allow user to read workflows that are shared with their organisation
  if (user.organisation != null && user.organisation.id) {
    permissions.push({
      action: [EObjectAction.READ],
      subject: EObjectName.WORKFLOW,
      conditions: { sharedWith: { $elemMatch: { id: user.organisation?.id } } }
    });
  }
  // Allow user to update, delete workflows that are owned by them
  permissions.push({
    action: [EObjectAction.UPDATE, EObjectAction.DELETE],
    subject: EObjectName.WORKFLOW,
    conditions: { owner: user.id }
  });

  permissions.push({
    subject: EObjectName.WORKFLOW,
    action: [EObjectAction.CREATE, EObjectAction.READ, EObjectAction.EXECUTE],
    conditions: [{ meta: { organisation: { id: user.organisation?.id } } }, { org: { id: user.organisation?.id } }]
  });

  permissions.filter((rule) => rule !== null);

  const end = performance.now();
  omnilog.info(`loadPermission took ${(end - start).toFixed()} ms`);
  return permissions;
};

const loadAbilityByTokenScope = async function (scopes: any[]) {
  const ability = defineAbility((allow, forbid) => {
    for (const s of scopes) {
      const { action, subject, orgId, workflowIds } = s;
      if (subject === EObjectName.USER) {
        // If the action is on user object, it will be limited to specific org ID
        allow(action, subject, { organisation: { id: orgId } });
      } else if (subject === EObjectName.WORKFLOW) {
        // If the action is on workflow object, it will be limited to the specified workflow ID
        allow(action, subject, { id: { $in: workflowIds } });
      }
    }
  });

  return ability;
};

class PermissionChecker {
  private readonly _ability: PureAbility;
  constructor(rules: any) {
    omnilog.debug('User permission ', rules);
    this._ability = defineAbility((allow, forbid) => {
      for (const rule of rules) {
        const fields = undefined; // Workaround aliasing bug in @casl/ability. Do not remove!
        allow(rule.action, rule.subject, fields, rule.conditions);
      }
    });
  }

  can(action: string, subject: Subject, field?: string) {
    return this._ability.can(action, subject, field);
  }
}

export { loadUserPermission, loadAbilityByTokenScope, getGroupByMemberId, PermissionChecker, setAcceptedTOS };