Passed
Push — trunk ( 6af328...aca8a0 )
by Christian
16:06 queued 15s
created

PrivilegesService.isPrivilegeMapping   B

Complexity

Conditions 6

Size

Total Lines 28
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 23
dl 0
loc 28
rs 8.3946
c 0
b 0
f 0
1
/**
2
 * @package admin
3
 */
4
5
import { reactive } from 'vue3';
6
7
const { warn, error } = Shopware.Utils.debug;
8
const { object } = Shopware.Utils;
9
10
type GetPrivilegesWithDependenciesSignature = () => string[];
11
12
type PrivilegeRole = {
13
    dependencies: Array<string>,
14
    privileges: Array<string|GetPrivilegesWithDependenciesSignature>,
15
}
16
17
type PrivilegeMapping = {
18
    category: 'permissions'|'additional_permissions',
19
    key: null|string,
20
    parent: string,
21
    roles: {
22
        [key: string]: PrivilegeRole,
23
    }
24
}
25
26
type PrivilegesState = {
27
    privilegesMappings: PrivilegeMapping[],
28
}
29
30
/**
31
 * @deprecated tag:v6.6.0 - Will be private
32
 */
33
export default class PrivilegesService {
34
    /**
35
     * @deprecated tag: v6.6.0 - Will be private
36
     */
37
    public alreadyImportedAdminPrivileges: string[] = [];
38
39
    /**
40
     * @deprecated tag: v6.6.0 - Will be private. Use getPrivilegesMappings instead of direct access
41
     */
42
    public state = reactive<PrivilegesState>({
43
        privilegesMappings: [],
44
    });
45
46
    /**
47
     * @deprecated tag: v6.6.0 - Will be private. Use getRequiredMappings instead of direct access
48
     */
49
    public requiredPrivileges = [
50
        'language:read', // for entityInit and languageSwitch
51
        'locale:read', // for localeToLanguage service
52
        'message_queue_stats:read', // for message queue
53
        'log_entry:create', // for sw-error-boundary
54
    ];
55
56
    /**
57
     * Removes all keys from the given array which are not an admin role.
58
     *
59
     * Example:
60
     * product.viewer => Valid
61
     * product:read => Invalid
62
     */
63
    public filterPrivilegesRoles(privileges: string[]) {
64
        const onlyRoles = privileges.filter(privilegeKey => this.existsPrivilege(privilegeKey));
65
66
        return onlyRoles.filter((role, index) => onlyRoles.indexOf(role) === index);
67
    }
68
69
    public existsPrivilege(privilegeKey: string) {
70
        const [key, role] = privilegeKey.split('.');
71
72
        return this.state.privilegesMappings.some(privilegeMapping => {
73
            return privilegeMapping.key === key && role in privilegeMapping.roles;
74
        });
75
    }
76
77
    private _getPrivilege(privilegeKey: string): PrivilegeMapping|undefined {
78
        const [key, role] = privilegeKey.split('.');
79
80
        return this.state.privilegesMappings.find(privilegeMapping => {
81
            return privilegeMapping.key === key && role in privilegeMapping.roles;
82
        });
83
    }
84
85
    public getPrivilegeRole(privilegeKey: string): PrivilegeRole|undefined {
86
        const role = privilegeKey.split('.')[1];
87
88
        const privilege = this._getPrivilege(privilegeKey);
89
90
        return privilege?.roles[role] ?? undefined;
91
    }
92
93
    /**
94
     *
95
     * @example
96
     * // returns [
97
     *      'promotion:read',
98
     *      'promotion:update',
99
     *      'promotion:create',
100
     *      'rule:read',
101
     *      'rule:update',
102
     *      'rule:create'
103
     *    ]
104
     * _getPrivilegesWithDependencies('promotion.creator', false);
105
     *
106
     * @example
107
     * // returns [
108
     *      'promotion:read',
109
     *      'promotion:update',
110
     *      'promotion:create',
111
     *      'rule:read',
112
     *      'rule:update',
113
     *      'rule:create',
114
     *      'promotion.viewer',
115
     *      'promotion.editor',
116
     *      'promotion.creator'
117
     *    ]
118
     * _getPrivilegesWithDependencies('promotion.creator', true);
119
     */
120
    private _getPrivilegesWithDependencies(adminPrivilegeKey: string, shouldAddAdminPrivilege = true): string[] {
121
        // check for duplicated calls to prevent infinite loop
122
        if (this.alreadyImportedAdminPrivileges.includes(adminPrivilegeKey)) {
123
            return [];
124
        }
125
        this.alreadyImportedAdminPrivileges.push(adminPrivilegeKey);
126
127
        const privilegeRole = this.getPrivilegeRole(adminPrivilegeKey);
128
129
        if (!privilegeRole) {
130
            return [];
131
        }
132
133
        /**
134
         * Get all privileges (['product:read', fn(), 'product:update', ...])
135
         * and dependencies (['product.viewer', ...])
136
         */
137
        const { privileges, dependencies } = privilegeRole;
138
139
        /**
140
         * Resolve all privileges for dependencies
141
         */
142
        const dependenciesPrivileges = dependencies.reduce((acc: string[], dependencyKey) => {
143
            return [
144
                ...acc,
145
                ...this._getPrivilegesWithDependencies(dependencyKey, shouldAddAdminPrivilege),
146
            ];
147
        }, []);
148
149
        /**
150
         * Look in privileges for the getPrivileges() method. If found then it
151
         * will be recursively resolved and the returned privileges are added
152
         */
153
        const resolvedPrivileges = privileges.reduce((acc: string[], privilege) => {
154
            if (typeof privilege === 'function') {
155
                return [...acc, ...privilege()];
156
            }
157
158
            return [...acc, privilege];
159
        }, []);
160
161
        /**
162
         * Combine privileges and privileges of dependencies
163
         */
164
        const collectedPrivileges = [
165
            ...resolvedPrivileges,
166
            ...dependenciesPrivileges,
167
        ];
168
169
        /**
170
         * Only add adminPrivilege if wanted
171
         */
172
        if (shouldAddAdminPrivilege) {
173
            collectedPrivileges.push(adminPrivilegeKey);
174
        }
175
176
        return collectedPrivileges;
177
    }
178
179
    /**
180
     *
181
     * Use this method directly in privilegeMappingEntries. Then it will
182
     * automatically get all privileges dynamically from the other adminRole.
183
     *
184
     * @usage
185
     * Shopware.Service('privileges').addPrivilegeMappingEntry({
186
     *     category: 'permissions',
187
     *     parent: null,
188
     *
189
     *     key: 'product',
190
     *     roles: {
191
     *         viewer: {
192
     *             privileges: [
193
     *                 'product.read',
194
     *                 Shopware.Service('privileges').getPrivileges('rule.viewer')
195
     *             ],
196
     *             dependencies: []
197
     *         }
198
     *     }
199
     * })
200
     *
201
     * @example
202
     * // returns "() => this._getPrivilegesWithDependencies('rule.creator', false)"
203
     * getPrivileges('rule.editor')
204
     */
205
    getPrivileges(privilegeKey: string) {
206
        return () => this._getPrivilegesWithDependencies(privilegeKey, false);
207
    }
208
209
    /**
210
     * This method gets all privileges for the given admin identifier
211
     *
212
     * @example
213
     * // return [
214
     *      'product.viewer',
215
     *      'product.editor',
216
     *      'product.creator',
217
     *      'product:read',
218
     *      'product:update',
219
     *      'promotion.viewer',
220
     *      'promotion:read',
221
     *      'rule:read'
222
     *      ...
223
     *    ]
224
     * getPrivilegesForAdminPrivilegeKeys(['promotion.viewer', 'product.creator'])
225
     */
226
    getPrivilegesForAdminPrivilegeKeys(adminPrivileges: string[]) {
227
        // reset the global state
228
        this.alreadyImportedAdminPrivileges = [];
229
230
        const allPrivileges = adminPrivileges.reduce((acc: string[], adminPrivilegeKey) => {
231
            const isAdminPrivilege = adminPrivilegeKey.match(/.+\..+/);
232
233
            if (!isAdminPrivilege) {
234
                return acc;
235
            }
236
237
            const privileges = this._getPrivilegesWithDependencies(adminPrivilegeKey);
238
239
            return [...acc, adminPrivilegeKey, ...privileges];
240
        }, []);
241
242
        return [
243
            // convert to Set and back to Array to remove duplicates
244
            ...new Set([...allPrivileges, ...this.getRequiredPrivileges()]),
245
        ].sort();
246
    }
247
248
    public addPrivilegeMappingEntry(privilegeMapping: unknown) {
249
        if (!this.isPrivilegeMapping(privilegeMapping)) {
250
            return this;
251
        }
252
253
        const existingCategoryKeyCombination = this.state.privilegesMappings.find(mapping => {
254
            return mapping.category === privilegeMapping.category &&
255
                mapping.key === privilegeMapping.key;
256
        });
257
258
        if (!existingCategoryKeyCombination) {
259
            this.state.privilegesMappings.push(privilegeMapping);
260
261
            return this;
262
        }
263
264
        Object.entries(privilegeMapping.roles).forEach(([role, entry]) => {
265
            if (existingCategoryKeyCombination.roles.hasOwnProperty(role) === true) {
266
                existingCategoryKeyCombination.roles[role] =
267
                    object.deepMergeObject(existingCategoryKeyCombination.roles[role], entry);
268
            } else {
269
                existingCategoryKeyCombination.roles[role] = entry;
270
            }
271
        });
272
273
        return this;
274
    }
275
276
    private isPrivilegeMapping(privilegeMapping: unknown): privilegeMapping is PrivilegeMapping {
277
        if (typeof privilegeMapping !== 'object') {
278
            warn('addPrivilegeMappingEntry', 'The privilegeMapping has to be an object.');
279
            return false;
280
        }
281
282
        if (privilegeMapping === null) {
283
            warn('addPrivilegeMappingEntry', 'The privilegeMapping must not be null.');
284
            return false;
285
        }
286
287
        if (!('category' in privilegeMapping)) {
288
            warn('addPrivilegeMappingEntry', 'The privilegeMapping need the property "category".');
289
            return false;
290
        }
291
292
        if (!('parent' in privilegeMapping)) {
293
            warn('addPrivilegeMappingEntry', 'The privilegeMapping need the property "parent".');
294
            return false;
295
        }
296
297
        if (!('key' in privilegeMapping)) {
298
            warn('addPrivilegeMappingEntry', 'The privilegeMapping need the property "key".');
299
            return false;
300
        }
301
302
        return true;
303
    }
304
305
    /**
306
     *
307
     * @returns {PrivilegesService}
308
     * @param privilegeMappings {Object[]}
309
     */
310
    public addPrivilegeMappingEntries(privilegeMappings: unknown) {
311
        if (!Array.isArray(privilegeMappings)) {
312
            error('addPrivilegeMappingEntries', 'The privilegeMappings must be an array.');
313
            return this;
314
        }
315
316
        privilegeMappings.forEach((privilegeMapping) => {
317
            this.addPrivilegeMappingEntry(privilegeMapping);
318
        });
319
320
        return this;
321
    }
322
323
    public getPrivilegesMappings() {
324
        return this.state.privilegesMappings;
325
    }
326
327
    public getRequiredPrivileges() {
328
        return this.requiredPrivileges;
329
    }
330
}
331