• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alkem-io / server / #8050

16 Aug 2024 11:21AM UTC coverage: 13.92%. First build
#8050

Pull #4411

travis-ci

Pull Request #4411: Type added to authorization policy entity

80 of 4158 branches covered (1.92%)

Branch coverage included in aggregate %.

61 of 116 new or added lines in 50 files covered. (52.59%)

1945 of 10389 relevant lines covered (18.72%)

3.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.33
/src/domain/storage/storage-bucket/storage.bucket.service.ts
1
import { AuthorizationPrivilege } from '@common/enums/authorization.privilege';
43✔
2
import { LogContext } from '@common/enums/logging.context';
43✔
3
import { EntityNotFoundException } from '@common/exceptions/entity.not.found.exception';
43✔
4
import { limitAndShuffle } from '@common/utils/limitAndShuffle';
43✔
5
import { AgentInfo } from '@core/authentication.agent.info/agent.info';
6
import { AuthorizationService } from '@core/authorization/authorization.service';
43✔
7
import { AuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.entity';
43✔
8
import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service';
43✔
9
import { Inject, Injectable, LoggerService } from '@nestjs/common';
43✔
10
import { InjectRepository } from '@nestjs/typeorm';
43✔
11
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
43✔
12
import { FindOneOptions, Repository } from 'typeorm';
43✔
13
import { IDocument } from '../document/document.interface';
14
import { Document } from '../document/document.entity';
43✔
15
import { DocumentService } from '../document/document.service';
43✔
16
import { StorageBucket } from './storage.bucket.entity';
43✔
17
import { IStorageBucket } from './storage.bucket.interface';
18
import { StorageBucketArgsDocuments } from './dto/storage.bucket.args.documents';
19
import {
43✔
20
  DEFAULT_ALLOWED_MIME_TYPES,
21
  MimeFileType,
22
} from '@common/enums/mime.file.type';
23
import { CreateDocumentInput } from '../document/dto/document.dto.create';
24
import { Readable } from 'stream';
25
import { ValidationException } from '@common/exceptions';
43✔
26
import { streamToBuffer } from '@common/utils';
43✔
27
import { CreateStorageBucketInput } from './dto/storage.bucket.dto.create';
28
import { Profile } from '@domain/common/profile/profile.entity';
43✔
29
import { IStorageBucketParent } from './dto/storage.bucket.dto.parent';
30
import { UrlGeneratorService } from '@services/infrastructure/url-generator/url.generator.service';
43✔
31
import { ProfileType } from '@common/enums';
32
import { StorageUploadFailedException } from '@common/exceptions/storage/storage.upload.failed.exception';
43✔
33
import { MimeTypeVisual } from '@common/enums/mime.file.type.visual';
43✔
34
import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type';
35
import { AvatarCreatorService } from '@services/external/avatar-creator/avatar.creator.service';
36
import { VisualType } from '@common/enums/visual.type';
43✔
37
@Injectable()
×
38
export class StorageBucketService {
39
  DEFAULT_MAX_ALLOWED_FILE_SIZE = 15728640;
40

×
41
  constructor(
×
42
    private documentService: DocumentService,
×
43
    private avatarCreatorService: AvatarCreatorService,
×
44
    private authorizationPolicyService: AuthorizationPolicyService,
45
    private authorizationService: AuthorizationService,
×
46
    private urlGeneratorService: UrlGeneratorService,
47
    @InjectRepository(StorageBucket)
×
48
    private storageBucketRepository: Repository<StorageBucket>,
49
    @InjectRepository(Document)
×
50
    private documentRepository: Repository<Document>,
51
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
×
52
    private readonly logger: LoggerService,
53
    @InjectRepository(Profile)
54
    private profileRepository: Repository<Profile>
55
  ) {}
56

57
  public createStorageBucket(
×
NEW
58
    storageBucketData: CreateStorageBucketInput
×
59
  ): IStorageBucket {
60
    const storage: IStorageBucket = new StorageBucket();
61
    storage.authorization = new AuthorizationPolicy(
×
62
      AuthorizationPolicyType.STORAGE_BUCKET
×
63
    );
×
64
    storage.documents = [];
×
65
    storage.allowedMimeTypes =
×
66
      storageBucketData?.allowedMimeTypes || DEFAULT_ALLOWED_MIME_TYPES;
×
67
    storage.maxFileSize =
68
      storageBucketData?.maxFileSize || this.DEFAULT_MAX_ALLOWED_FILE_SIZE;
×
69
    storage.storageAggregator = storageBucketData.storageAggregator;
70

71
    return storage;
72
  }
×
73

74
  async deleteStorageBucket(storageID: string): Promise<IStorageBucket> {
75
    const storage = await this.getStorageBucketOrFail(storageID, {
76
      relations: { documents: true },
×
77
    });
×
78

79
    if (storage.authorization)
×
80
      await this.authorizationPolicyService.delete(storage.authorization);
×
81

×
82
    if (storage.documents) {
83
      for (const document of storage.documents) {
84
        await this.documentService.deleteDocument({
85
          ID: document.id,
86
        });
87
      }
×
88
    }
89

90
    const result = await this.storageBucketRepository.remove(
×
91
      storage as StorageBucket
×
92
    );
93
    result.id = storageID;
94
    return result;
95
  }
96

97
  public async save(storage: IStorageBucket): Promise<IStorageBucket> {
98
    return this.storageBucketRepository.save(storage);
×
99
  }
×
100

101
  async getStorageBucketOrFail(
102
    storageBucketID: string,
103
    options?: FindOneOptions<StorageBucket>
104
  ): Promise<IStorageBucket> {
×
105
    if (!storageBucketID) {
106
      throw new EntityNotFoundException(
107
        `StorageBucket not found: ${storageBucketID}`,
108
        LogContext.STORAGE_BUCKET
×
109
      );
×
110
    }
111
    const storageBucket = await this.storageBucketRepository.findOneOrFail({
112
      where: { id: storageBucketID },
113
      ...options,
×
114
    });
115
    if (!storageBucket)
116
      throw new EntityNotFoundException(
117
        `StorageBucket not found: ${storageBucketID}`,
118
        LogContext.STORAGE_BUCKET
119
      );
×
120
    return storageBucket;
121
  }
122

×
123
  public async getDocuments(
×
124
    storageInput: IStorageBucket
×
125
  ): Promise<IDocument[]> {
126
    const storage = await this.getStorageBucketOrFail(storageInput.id, {
127
      relations: { documents: true },
128
    });
129
    const documents = storage.documents;
×
130
    if (!documents)
131
      throw new EntityNotFoundException(
132
        `Undefined storage documents found: ${storage.id}`,
133
        LogContext.STORAGE_BUCKET
134
      );
135

136
    return documents;
137
  }
138
  public async uploadFileAsDocument(
×
139
    storageBucketId: string,
140
    readStream: Readable,
×
141
    filename: string,
142
    mimeType: string,
143
    userID: string,
144
    temporaryDocument = false
145
  ): Promise<IDocument> {
146
    const buffer = await streamToBuffer(readStream);
147

148
    return await this.uploadFileAsDocumentFromBuffer(
149
      storageBucketId,
150
      buffer,
151
      filename,
152
      mimeType,
153
      userID,
154
      temporaryDocument
155
    );
×
156
  }
157

×
158
  public async uploadFileAsDocumentFromBuffer(
159
    storageBucketId: string,
160
    buffer: Buffer,
161
    filename: string,
×
162
    mimeType: string,
163
    userID: string,
164
    temporaryLocation = false
×
165
  ): Promise<IDocument> {
×
166
    const storage = await this.getStorageBucketOrFail(storageBucketId, {
×
167
      relations: {},
168
    });
×
169

170
    this.validateMimeTypes(storage, mimeType);
171

172
    // Upload the document
173
    const size = buffer.length;
174
    this.validateSize(storage, size);
175
    const externalID = await this.documentService.uploadFile(buffer, filename);
176

177
    const createDocumentInput: CreateDocumentInput = {
×
178
      mimeType: mimeType as MimeFileType,
179
      externalID: externalID,
×
180
      displayName: filename,
181
      size: size,
182
      createdBy: userID,
183
      temporaryLocation: temporaryLocation,
184
    };
185

186
    try {
×
187
      const docByExternalId =
×
188
        await this.documentService.getDocumentByExternalIdOrFail(externalID, {
189
          where: {
190
            storageBucket: {
191
              id: storageBucketId,
192
            },
193
          },
194
        });
×
195
      if (docByExternalId) {
×
196
        return docByExternalId;
197
      }
×
198
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
199
    } catch (e) {
200
      /* just consume */
201
    }
×
202

203
    const document =
204
      await this.documentService.createDocument(createDocumentInput);
205
    document.storageBucket = storage;
206

207
    this.logger.verbose?.(
208
      `Uploaded document '${document.externalID}' on storage bucket: ${storage.id}`,
209
      LogContext.STORAGE_BUCKET
210
    );
211
    return await this.documentService.save(document);
212
  }
213

×
214
  async uploadFileFromURI(
×
215
    uri: string,
216
    entityId: string,
217
    storageBucket: IStorageBucket,
218
    readStream: Readable,
219
    filename: string,
220
    mimetype: string,
×
221
    userID: string
222
  ): Promise<IDocument> {
×
223
    if (!readStream)
×
224
      throw new ValidationException(
225
        'Readstream should be defined!',
226
        LogContext.DOCUMENT
227
      );
228

229
    const documentForReference =
230
      await this.documentService.getDocumentFromURL(uri);
231

×
232
    try {
×
233
      const newDocument = await this.uploadFileAsDocument(
234
        storageBucket.id,
235
        readStream,
×
236
        filename,
237
        mimetype,
238
        userID
239
      );
×
240
      // Delete the old document, if any. Do not delete the same doc.
241
      if (
×
242
        documentForReference &&
243
        newDocument.externalID != documentForReference.externalID
244
      ) {
245
        await this.documentService.deleteDocument({
246
          ID: documentForReference.id,
247
        });
248
      }
249
      return newDocument;
250
    } catch (error: any) {
251
      throw new StorageUploadFailedException(
252
        'Upload on reference or link failed!',
253
        LogContext.STORAGE_BUCKET,
254
        {
255
          message: error.message,
256
          fileName: filename,
257
          referenceID: entityId,
258
          originalException: error,
×
259
        }
260
      );
×
261
    }
×
262
  }
×
263

264
  /**
×
265
   * @throws {Error}
266
   */
267
  public async addDocumentToStorageBucketOrFail(
268
    storageBucket: IStorageBucket,
×
269
    document: IDocument
270
  ): Promise<IDocument> {
271
    this.validateMimeTypes(storageBucket, document.mimeType);
272
    this.validateSize(storageBucket, document.size);
×
273
    document.storageBucket = storageBucket;
274
    if (!storageBucket.documents.includes(document)) {
275
      storageBucket.documents.push(document);
276
    }
277
    this.logger.verbose?.(
278
      `Added document '${document.externalID}' on storage bucket: ${storageBucket.id}`,
279
      LogContext.STORAGE_BUCKET
280
    );
×
281
    return document;
282
  }
283

284
  public async addDocumentToStorageBucketByIdOrFail(
285
    storageBucketId: string,
286
    document: IDocument
287
  ): Promise<IDocument> {
288
    const storageBucket = await this.getStorageBucketOrFail(storageBucketId);
×
289
    return this.addDocumentToStorageBucketOrFail(storageBucket, document);
290
  }
291

×
292
  public async size(storage: IStorageBucket): Promise<number> {
×
293
    const documentsSize = await this.documentRepository
×
294
      .createQueryBuilder('document')
295
      .where('document.storageBucketId = :storageBucketId', {
296
        storageBucketId: storage.id,
297
      })
298
      .select('SUM(size)', 'totalSize')
299
      .getRawOne<{ totalSize: number }>();
×
300

×
301
    return documentsSize?.totalSize ?? 0;
302
  }
303

304
  public async getFilteredDocuments(
×
305
    storage: IStorageBucket,
×
306
    args: StorageBucketArgsDocuments,
×
307
    agentInfo: AgentInfo
×
308
  ): Promise<IDocument[]> {
309
    const storageLoaded = await this.getStorageBucketOrFail(storage.id, {
×
310
      relations: { documents: true },
×
311
    });
312
    const allDocuments = storageLoaded.documents;
313
    if (!allDocuments)
314
      throw new EntityNotFoundException(
×
315
        `Storage not initialised, no documents: ${storage.id}`,
316
        LogContext.STORAGE_BUCKET
×
317
      );
318

319
    // First filter the documents the current user has READ privilege to
320
    const readableDocuments = allDocuments.filter(document =>
×
321
      this.hasAgentAccessToDocument(document, agentInfo)
×
322
    );
323

324
    // (a) by IDs, results in order specified by IDs
×
325
    if (args.IDs) {
326
      const results: IDocument[] = [];
327
      for (const documentID of args.IDs) {
328
        const document = readableDocuments.find(e => e.id === documentID);
329

330
        if (!document)
331
          throw new EntityNotFoundException(
×
332
            `Document with requested ID (${documentID}) not located within current StorageBucket: ${storage.id}`,
333
            LogContext.STORAGE_BUCKET
334
          );
335
        results.push(document);
336
      }
337
      return results;
338
    }
339

340
    // (b) limit number of results
341
    if (args.limit) {
342
      return limitAndShuffle(readableDocuments, args.limit, false);
×
343
    }
344

345
    return readableDocuments;
×
346
  }
×
347

348
  private hasAgentAccessToDocument(
349
    document: IDocument,
350
    agentInfo: AgentInfo
351
  ): boolean {
352
    return this.authorizationService.isAccessGranted(
353
      agentInfo,
354
      document.authorization,
355
      AuthorizationPrivilege.READ
356
    );
357
  }
×
358

×
359
  private validateMimeTypes(
360
    storageBucket: IStorageBucket,
361
    mimeType: string
362
  ): void {
363
    const result = Object.values(storageBucket.allowedMimeTypes).includes(
364
      mimeType as MimeFileType
365
    );
366
    if (!result) {
367
      throw new ValidationException(
368
        `Invalid Mime Type specified for storage bucket '${mimeType}'- allowed types: ${storageBucket.allowedMimeTypes}.`,
×
369
        LogContext.STORAGE_BUCKET
370
      );
371
    }
372
  }
373

374
  private validateSize(storageBucket: IStorageBucket, size: number): void {
375
    if (size > storageBucket.maxFileSize) {
376
      throw new ValidationException(
377
        `File size (${size}) exceeds maximum allowed file size for storage bucket: ${storageBucket.maxFileSize}`,
378
        LogContext.STORAGE_BUCKET
379
      );
380
    }
×
381
  }
382

383
  public async getStorageBucketsForAggregator(
384
    storageAggregatorID: string
385
  ): Promise<IStorageBucket[]> {
386
    return this.storageBucketRepository.find({
387
      where: {
×
388
        storageAggregator: {
×
389
          id: storageAggregatorID,
390
        },
391
      },
392
    });
393
  }
394

395
  public async getStorageBucketParent(
396
    storageBucket: IStorageBucket
×
397
  ): Promise<IStorageBucketParent | null> {
398
    const profile = await this.profileRepository.findOne({
399
      where: {
400
        storageBucket: {
401
          id: storageBucket.id,
402
        },
403
      },
404
    });
405
    if (profile) {
406
      return {
407
        id: profile.id,
408
        type: profile.type as ProfileType,
409
        displayName: profile.displayName,
410
        url: await this.urlGeneratorService.generateUrlForProfile(profile),
411
      };
412
    }
413

414
    return null;
415
  }
416

417
  public async ensureAvatarUrlIsDocument(
418
    avatarURL: string,
419
    storageBucketId: string,
420
    userId: string
421
  ): Promise<IDocument> {
422
    if (this.documentService.isAlkemioDocumentURL(avatarURL)) {
423
      const document = await this.documentService.getDocumentFromURL(avatarURL);
424
      if (!document) {
425
        throw new EntityNotFoundException(
426
          `Document not found: ${avatarURL}`,
427
          LogContext.STORAGE_BUCKET
428
        );
429
      }
430
      return document;
431
    }
432

433
    // Not stored on Alkemio, download + store
434
    const imageBuffer = await this.avatarCreatorService.urlToBuffer(avatarURL);
435
    let fileType = await this.avatarCreatorService.getFileType(imageBuffer);
436
    if (!fileType) {
437
      fileType = MimeTypeVisual.PNG;
438
    }
439

440
    const document = await this.uploadFileAsDocumentFromBuffer(
441
      storageBucketId,
442
      imageBuffer,
443
      VisualType.AVATAR,
444
      fileType,
445
      userId
446
    );
447

448
    const storageBucket = await this.getStorageBucketOrFail(storageBucketId);
449
    document.storageBucket = storageBucket;
450

451
    return await this.documentService.saveDocument(document);
452
  }
453
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc