You've already forked pigallery2
mirror of
https://github.com/bpatrik/pigallery2.git
synced 2025-11-25 22:32:52 +02:00
Implement search query based sharing on the backend #1015
This commit is contained in:
@@ -4,6 +4,8 @@ Author: Junie (JetBrains autonomous programmer)
|
||||
Date: 2025-08-13
|
||||
Status: Updated after maintainer clarifications
|
||||
|
||||
NOTE: scoped working is the same as projection. THe app uses the projection terminologie instead of scoped.
|
||||
|
||||
## Overview
|
||||
This document proposes a design to support:
|
||||
- Search-based sharing: a share link represents a pre-filtered view of the gallery.
|
||||
|
||||
@@ -8,7 +8,6 @@ import {ObjectManagers} from '../model/ObjectManagers';
|
||||
import {ContentWrapper} from '../../common/entities/ConentWrapper';
|
||||
import {ProjectPath} from '../ProjectPath';
|
||||
import {Config} from '../../common/config/private/Config';
|
||||
import {UserDTOUtils} from '../../common/entities/UserDTO';
|
||||
import {MediaDTO, MediaDTOUtils} from '../../common/entities/MediaDTO';
|
||||
import {QueryParams} from '../../common/QueryParams';
|
||||
import {VideoProcessing} from '../model/fileaccess/fileprocessing/VideoProcessing';
|
||||
@@ -57,15 +56,6 @@ export class GalleryMWs {
|
||||
req.resultPipe = new ContentWrapper(null, null, true);
|
||||
return next();
|
||||
}
|
||||
if (
|
||||
req.session.context?.user.permissions &&
|
||||
req.session.context?.user.permissions.length > 0 &&
|
||||
req.session.context?.user.permissions[0] !== '/*'
|
||||
) {
|
||||
directory.directories = directory.directories.filter((d): boolean =>
|
||||
UserDTOUtils.isDirectoryAvailable(d, req.session.context.user.permissions)
|
||||
);
|
||||
}
|
||||
req.resultPipe = new ContentWrapper(directory, null);
|
||||
return next();
|
||||
} catch (err) {
|
||||
|
||||
@@ -40,7 +40,6 @@ export class RenderingMWs {
|
||||
name: req.session.context.user.name,
|
||||
role: req.session.context.user.role,
|
||||
usedSharingKey: req.session.context.user.usedSharingKey,
|
||||
permissions: req.session.context.user.permissions,
|
||||
projectionKey: req.session.context.user.projectionKey,
|
||||
} as UserDTO;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {Config} from '../../common/config/private/Config';
|
||||
import {QueryParams} from '../../common/QueryParams';
|
||||
import * as path from 'path';
|
||||
import {UserRoles} from '../../common/entities/UserDTO';
|
||||
import {SearchQueryDTO, SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../common/entities/SearchQueryDTO';
|
||||
|
||||
export class SharingMWs {
|
||||
public static async getSharing(
|
||||
@@ -97,17 +98,25 @@ export class SharingMWs {
|
||||
}
|
||||
|
||||
const directoryName = path.normalize(req.params['directory'] || '/');
|
||||
|
||||
// Prefer provided searchQuery; otherwise fallback to strict directory exact-match query for compatibility
|
||||
const searchQuery = createSharing.searchQuery || ({
|
||||
type: SearchQueryTypes.directory,
|
||||
text: directoryName,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
negate: false
|
||||
} as TextSearch);
|
||||
|
||||
const sharing: SharingDTO = {
|
||||
id: null,
|
||||
sharingKey,
|
||||
path: directoryName,
|
||||
searchQuery,
|
||||
password: createSharing.password,
|
||||
creator: req.session.context?.user,
|
||||
expires:
|
||||
createSharing.valid >= 0 // if === -1 it's forever
|
||||
? Date.now() + createSharing.valid
|
||||
: new Date(9999, 0, 1).getTime(), // never expire
|
||||
includeSubfolders: createSharing.includeSubfolders,
|
||||
timeStamp: Date.now(),
|
||||
};
|
||||
|
||||
@@ -147,9 +156,17 @@ export class SharingMWs {
|
||||
}
|
||||
const updateSharing: CreateSharingDTO = req.body.updateSharing;
|
||||
const directoryName = path.normalize(req.params['directory'] || '/');
|
||||
|
||||
const searchQuery = updateSharing.searchQuery || ({
|
||||
type: SearchQueryTypes.directory,
|
||||
text: directoryName,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
negate: false
|
||||
} as TextSearch);
|
||||
|
||||
const sharing: SharingDTO = {
|
||||
id: updateSharing.id,
|
||||
path: directoryName,
|
||||
searchQuery,
|
||||
sharingKey: '',
|
||||
password:
|
||||
updateSharing.password && updateSharing.password !== ''
|
||||
@@ -160,7 +177,6 @@ export class SharingMWs {
|
||||
updateSharing.valid >= 0 // if === -1 its forever
|
||||
? Date.now() + updateSharing.valid
|
||||
: new Date(9999, 0, 1).getTime(), // never expire
|
||||
includeSubfolders: updateSharing.includeSubfolders,
|
||||
timeStamp: Date.now(),
|
||||
};
|
||||
|
||||
@@ -249,7 +265,7 @@ export class SharingMWs {
|
||||
}
|
||||
}
|
||||
|
||||
public static async listSharingForDir(
|
||||
public static async listSharingForQuery(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
@@ -257,15 +273,16 @@ export class SharingMWs {
|
||||
if (Config.Sharing.enabled === false) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const dir = path.normalize(req.params['directory'] || '/');
|
||||
const query: SearchQueryDTO = JSON.parse(
|
||||
req.params['searchQueryDTO'] as string
|
||||
);
|
||||
try {
|
||||
if (req.session.context?.user.role >= UserRoles.Admin) {
|
||||
req.resultPipe =
|
||||
await ObjectManagers.getInstance().SharingManager.listAllForDir(dir);
|
||||
await ObjectManagers.getInstance().SharingManager.listAllForQuery(query);
|
||||
} else {
|
||||
req.resultPipe =
|
||||
await ObjectManagers.getInstance().SharingManager.listAllForDir(dir, req.session.context?.user);
|
||||
await ObjectManagers.getInstance().SharingManager.listAllForQuery(query, req.session.context?.user);
|
||||
}
|
||||
return next();
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
|
||||
import {UserDTO, UserDTOUtils, UserRoles,} from '../../../common/entities/UserDTO';
|
||||
import {UserDTO, UserRoles,} from '../../../common/entities/UserDTO';
|
||||
import {ObjectManagers} from '../../model/ObjectManagers';
|
||||
import {Config} from '../../../common/config/private/Config';
|
||||
import {PasswordHelper} from '../../model/PasswordHelper';
|
||||
@@ -10,6 +10,8 @@ import * as path from 'path';
|
||||
import {Logger} from '../../Logger';
|
||||
import {SQLConnection} from '../../model/database/SQLConnection';
|
||||
import {MediaEntity} from '../../model/database/enitites/MediaEntity';
|
||||
import {UserEntity} from '../../model/database/enitites/UserEntity';
|
||||
import {ContextUser} from '../../model/SessionContext';
|
||||
|
||||
const LOG_TAG = 'AuthenticationMWs';
|
||||
|
||||
@@ -93,43 +95,31 @@ export class AuthenticationMWs {
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy authorisePath kept for compatibility; prefer new specific middlewares below
|
||||
private static authorisePath(
|
||||
paramName: string,
|
||||
isDirectory: boolean
|
||||
): (req: Request, res: Response, next: NextFunction) => void {
|
||||
return function authorisePath(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Response | void {
|
||||
let p: string = req.params[paramName];
|
||||
if (!isDirectory) {
|
||||
p = path.dirname(p);
|
||||
}
|
||||
|
||||
if (
|
||||
!UserDTOUtils.isDirectoryPathAvailable(p, req.session.context.user.permissions)
|
||||
) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
// New middlewares
|
||||
public static authoriseDirectories(
|
||||
paramName: string
|
||||
): (req: Request, res: Response, next: NextFunction) => void {
|
||||
return AuthenticationMWs.authorisePath(paramName, true);
|
||||
}
|
||||
|
||||
public static authoriseMetaFiles(
|
||||
paramName: string
|
||||
): (req: Request, res: Response, next: NextFunction) => void {
|
||||
// For metafiles: same behavior as legacy (check directory permission only)
|
||||
return AuthenticationMWs.authorisePath(paramName, false);
|
||||
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
|
||||
return async function authoriseMetaFiles(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
try {
|
||||
const p: string = req.params[paramName];
|
||||
|
||||
if (!await ObjectManagers.getInstance().GalleryManager.authoriseMetaFile(req.session.context, p)) {
|
||||
res.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (e) {
|
||||
// On error, fail closed to be safe
|
||||
Logger.warn(LOG_TAG, 'authoriseMedia error:', e);
|
||||
res.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static authoriseMedia(
|
||||
@@ -142,12 +132,6 @@ export class AuthenticationMWs {
|
||||
): Promise<void> {
|
||||
try {
|
||||
const mediaRelPath: string = req.params[paramName];
|
||||
// First: directory permission check (same as legacy for files)
|
||||
const dirRelPath = path.dirname(mediaRelPath);
|
||||
if (!UserDTOUtils.isDirectoryPathAvailable(dirRelPath, req.session.context.user.permissions)) {
|
||||
res.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await ObjectManagers.getInstance().GalleryManager.authoriseMedia(req.session.context, mediaRelPath)) {
|
||||
res.sendStatus(403);
|
||||
@@ -217,17 +201,13 @@ export class AuthenticationMWs {
|
||||
return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND));
|
||||
}
|
||||
|
||||
let sharingPath = sharing.path;
|
||||
if (sharing.includeSubfolders === true) {
|
||||
sharingPath += '*';
|
||||
}
|
||||
|
||||
const user = {
|
||||
name: 'Guest',
|
||||
role: UserRoles.LimitedGuest,
|
||||
permissions: [sharingPath],
|
||||
usedSharingKey: sharing.sharingKey,
|
||||
} as UserDTO;
|
||||
overrideAllowBlockList: true,
|
||||
allowQuery: ObjectManagers.getInstance().buildAllowListForSharing(sharing)
|
||||
} as ContextUser;
|
||||
req.session.context = await ObjectManagers.getInstance().buildContext(user);
|
||||
return next();
|
||||
} catch (err) {
|
||||
@@ -304,7 +284,7 @@ export class AuthenticationMWs {
|
||||
return next();
|
||||
}
|
||||
|
||||
private static async getSharingUser(req: Request): Promise<UserDTO> {
|
||||
private static async getSharingUser(req: Request): Promise<ContextUser> {
|
||||
if (
|
||||
Config.Sharing.enabled === true &&
|
||||
(!!req.query[QueryParams.gallery.sharingKey_query] ||
|
||||
@@ -326,16 +306,13 @@ export class AuthenticationMWs {
|
||||
return null;
|
||||
}
|
||||
|
||||
let sharingPath = sharing.path;
|
||||
if (sharing.includeSubfolders === true) {
|
||||
sharingPath += '*';
|
||||
}
|
||||
return {
|
||||
name: 'Guest',
|
||||
role: UserRoles.LimitedGuest,
|
||||
permissions: [sharingPath],
|
||||
usedSharingKey: sharing.sharingKey,
|
||||
} as UserDTO;
|
||||
overrideAllowBlockList: true,
|
||||
allowQuery: ObjectManagers.getInstance().buildAllowListForSharing(sharing)
|
||||
} as ContextUser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ import {PersonManager} from './database/PersonManager';
|
||||
import {SharingManager} from './database/SharingManager';
|
||||
import {IObjectManager} from './database/IObjectManager';
|
||||
import {ExtensionManager} from './extension/ExtensionManager';
|
||||
import {SessionContext} from './SessionContext';
|
||||
import {ContextUser, SessionContext} from './SessionContext';
|
||||
import {UserEntity} from './database/enitites/UserEntity';
|
||||
import {ANDSearchQuery, SearchQueryDTOUtils, SearchQueryTypes} from '../../common/entities/SearchQueryDTO';
|
||||
import {ANDSearchQuery, SearchQueryDTO, SearchQueryDTOUtils, SearchQueryTypes} from '../../common/entities/SearchQueryDTO';
|
||||
import {Config} from '../../common/config/private/Config';
|
||||
import {SharingEntity} from './database/enitites/SharingEntity';
|
||||
|
||||
const LOG_TAG = '[ObjectManagers]';
|
||||
|
||||
@@ -275,30 +276,55 @@ export class ObjectManagers {
|
||||
this.managers.push(this.extensionManager as IObjectManager);
|
||||
}
|
||||
|
||||
async buildContext(user: UserEntity): Promise<SessionContext> {
|
||||
const context = new SessionContext();
|
||||
context.user = user;
|
||||
private getQueryForUser(user: ContextUser) {
|
||||
let blockQuery = user.overrideAllowBlockList ? user.blockQuery : Config.Users.blockQuery;
|
||||
const allowQuery = user.overrideAllowBlockList ? user.allowQuery : Config.Users.allowQuery;
|
||||
|
||||
if (!SearchQueryDTOUtils.isValidQuery(allowQuery) && !SearchQueryDTOUtils.isValidQuery(blockQuery)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (SearchQueryDTOUtils.isValidQuery(blockQuery)) {
|
||||
blockQuery = SearchQueryDTOUtils.negate(blockQuery);
|
||||
}
|
||||
if (SearchQueryDTOUtils.isValidQuery(allowQuery) || SearchQueryDTOUtils.isValidQuery(blockQuery)) {
|
||||
let query = allowQuery || blockQuery;
|
||||
if (allowQuery && blockQuery) {
|
||||
query = {
|
||||
type: SearchQueryTypes.AND,
|
||||
list: [
|
||||
allowQuery,
|
||||
blockQuery
|
||||
]
|
||||
} as ANDSearchQuery;
|
||||
}
|
||||
console.log(allowQuery, blockQuery, query);
|
||||
// Build the Brackets-based query
|
||||
context.projectionQuery = await ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(query);
|
||||
let query = SearchQueryDTOUtils.isValidQuery(allowQuery) ? allowQuery : blockQuery;
|
||||
if (SearchQueryDTOUtils.isValidQuery(allowQuery) && SearchQueryDTOUtils.isValidQuery(blockQuery)) {
|
||||
query = {
|
||||
type: SearchQueryTypes.AND,
|
||||
list: [
|
||||
allowQuery,
|
||||
blockQuery
|
||||
]
|
||||
} as ANDSearchQuery;
|
||||
}
|
||||
return query;
|
||||
|
||||
context.user.projectionKey = crypto.createHash('md5').update(JSON.stringify(query)).digest('hex');
|
||||
}
|
||||
|
||||
public buildAllowListForSharing(sharing:SharingEntity): SearchQueryDTO {
|
||||
const creatorQuery = this.getQueryForUser(sharing.creator);
|
||||
let finalQuery = sharing.searchQuery;
|
||||
if(creatorQuery){
|
||||
finalQuery = {
|
||||
type: SearchQueryTypes.AND,
|
||||
list: [
|
||||
creatorQuery,
|
||||
sharing.searchQuery
|
||||
]
|
||||
} as ANDSearchQuery;
|
||||
}
|
||||
return finalQuery;
|
||||
}
|
||||
|
||||
public async buildContext(user: ContextUser): Promise<SessionContext> {
|
||||
const context = new SessionContext();
|
||||
context.user = user;
|
||||
let finalQuery = this.getQueryForUser(user);
|
||||
|
||||
if (finalQuery) {
|
||||
// Build the Brackets-based query
|
||||
context.projectionQuery = await ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(finalQuery);
|
||||
context.user.projectionKey = crypto.createHash('md5').update(JSON.stringify(finalQuery)).digest('hex');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import {Brackets} from 'typeorm';
|
||||
import {UserDTO} from '../../common/entities/UserDTO';
|
||||
import {SearchQueryDTO} from '../../common/entities/SearchQueryDTO';
|
||||
|
||||
export class SessionContext {
|
||||
user: UserDTO;
|
||||
user: ContextUser;
|
||||
// New structured projection with prebuilt SQL and params
|
||||
projectionQuery?: Brackets;
|
||||
}
|
||||
|
||||
export interface ContextUser extends UserDTO {
|
||||
overrideAllowBlockList?: boolean;
|
||||
|
||||
allowQuery?: SearchQueryDTO;
|
||||
|
||||
blockQuery?: SearchQueryDTO;
|
||||
}
|
||||
|
||||
@@ -445,4 +445,31 @@ export class GalleryManager {
|
||||
|
||||
return count !== 0;
|
||||
}
|
||||
|
||||
async authoriseMetaFile(session: SessionContext, p: string) {
|
||||
// If no projection set for session, proceed
|
||||
if (!session?.projectionQuery) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Authorize metafile if its directory contains any media that matches the projectionQuery
|
||||
const dirRelPath = path.dirname(p);
|
||||
const directoryName = path.basename(dirRelPath);
|
||||
const directoryParent = path.join(path.dirname(dirRelPath), path.sep);
|
||||
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const qb = connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoin('media.directory', 'directory')
|
||||
.where('directory.name = :dname AND directory.path = :dpath', {
|
||||
dname: directoryName,
|
||||
dpath: directoryParent,
|
||||
})
|
||||
.andWhere(session.projectionQuery);
|
||||
|
||||
const count = await qb.getCount();
|
||||
|
||||
return count !== 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,23 +5,24 @@ import {Config} from '../../../common/config/private/Config';
|
||||
import {PasswordHelper} from '../PasswordHelper';
|
||||
import {DeleteResult, SelectQueryBuilder} from 'typeorm';
|
||||
import {UserDTO} from '../../../common/entities/UserDTO';
|
||||
import {SearchQueryDTO} from '../../../common/entities/SearchQueryDTO';
|
||||
|
||||
export class SharingManager {
|
||||
private static async removeExpiredLink(): Promise<DeleteResult> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
return await connection
|
||||
.getRepository(SharingEntity)
|
||||
.createQueryBuilder('share')
|
||||
.where('expires < :now', {now: Date.now()})
|
||||
.delete()
|
||||
.execute();
|
||||
.getRepository(SharingEntity)
|
||||
.createQueryBuilder('share')
|
||||
.where('expires < :now', {now: Date.now()})
|
||||
.delete()
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteSharing(sharingKey: string): Promise<void> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const sharing = await connection
|
||||
.getRepository(SharingEntity)
|
||||
.findOneBy({sharingKey});
|
||||
.getRepository(SharingEntity)
|
||||
.findOneBy({sharingKey});
|
||||
await connection.getRepository(SharingEntity).remove(sharing);
|
||||
}
|
||||
|
||||
@@ -29,35 +30,35 @@ export class SharingManager {
|
||||
await SharingManager.removeExpiredLink();
|
||||
const connection = await SQLConnection.getConnection();
|
||||
return await connection
|
||||
.getRepository(SharingEntity)
|
||||
.createQueryBuilder('share')
|
||||
.leftJoinAndSelect('share.creator', 'creator')
|
||||
.getMany();
|
||||
.getRepository(SharingEntity)
|
||||
.createQueryBuilder('share')
|
||||
.leftJoinAndSelect('share.creator', 'creator')
|
||||
.getMany();
|
||||
}
|
||||
|
||||
|
||||
async listAllForDir(dir: string, user?: UserDTO): Promise<SharingDTO[]> {
|
||||
async listAllForQuery(query: SearchQueryDTO, user?: UserDTO): Promise<SharingDTO[]> {
|
||||
await SharingManager.removeExpiredLink();
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const q: SelectQueryBuilder<SharingEntity> = connection
|
||||
.getRepository(SharingEntity)
|
||||
.createQueryBuilder('share')
|
||||
.leftJoinAndSelect('share.creator', 'creator')
|
||||
.where('path = :dir', {dir});
|
||||
.getRepository(SharingEntity)
|
||||
.createQueryBuilder('share')
|
||||
.leftJoinAndSelect('share.creator', 'creator')
|
||||
.where('share.searchQuery = :query', {query: JSON.stringify(query)});
|
||||
if (user) {
|
||||
q.andWhere('share.creator = :user', {user: user.id});
|
||||
q.where('share.creator = :user', {user: user.id});
|
||||
}
|
||||
return await q.getMany();
|
||||
}
|
||||
|
||||
async findOne(sharingKey: string): Promise<SharingDTO> {
|
||||
async findOne(sharingKey: string): Promise<SharingEntity> {
|
||||
await SharingManager.removeExpiredLink();
|
||||
const connection = await SQLConnection.getConnection();
|
||||
return await connection.getRepository(SharingEntity)
|
||||
.createQueryBuilder('share')
|
||||
.leftJoinAndSelect('share.creator', 'creator')
|
||||
.where('share.sharingKey = :sharingKey', {sharingKey})
|
||||
.getOne();
|
||||
.createQueryBuilder('share')
|
||||
.leftJoinAndSelect('share.creator', 'creator')
|
||||
.where('share.sharingKey = :sharingKey', {sharingKey})
|
||||
.getOne();
|
||||
}
|
||||
|
||||
async createSharing(sharing: SharingDTO): Promise<SharingDTO> {
|
||||
@@ -70,20 +71,19 @@ export class SharingManager {
|
||||
}
|
||||
|
||||
async updateSharing(
|
||||
inSharing: SharingDTO,
|
||||
forceUpdate: boolean
|
||||
inSharing: SharingDTO,
|
||||
forceUpdate: boolean
|
||||
): Promise<SharingDTO> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
const sharing = await connection.getRepository(SharingEntity).findOneBy({
|
||||
id: inSharing.id,
|
||||
creator: inSharing.creator.id as unknown,
|
||||
path: inSharing.path,
|
||||
});
|
||||
|
||||
if (
|
||||
sharing.timeStamp < Date.now() - Config.Sharing.updateTimeout &&
|
||||
forceUpdate !== true
|
||||
sharing.timeStamp < Date.now() - Config.Sharing.updateTimeout &&
|
||||
forceUpdate !== true
|
||||
) {
|
||||
throw new Error('Sharing is locked, can\'t update anymore');
|
||||
}
|
||||
@@ -92,7 +92,8 @@ export class SharingManager {
|
||||
} else {
|
||||
sharing.password = PasswordHelper.cryptPassword(inSharing.password);
|
||||
}
|
||||
sharing.includeSubfolders = inSharing.includeSubfolders;
|
||||
// allow updating searchQuery
|
||||
(sharing as any).searchQuery = (inSharing as any).searchQuery || (sharing as any).searchQuery;
|
||||
sharing.expires = inSharing.expires;
|
||||
|
||||
return connection.getRepository(SharingEntity).save(sharing);
|
||||
|
||||
@@ -2,6 +2,7 @@ import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from 'typeorm';
|
||||
import {SharingDTO} from '../../../../common/entities/SharingDTO';
|
||||
import {UserEntity} from './UserEntity';
|
||||
import {UserDTO} from '../../../../common/entities/UserDTO';
|
||||
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
@Entity()
|
||||
export class SharingEntity implements SharingDTO {
|
||||
@@ -11,8 +12,19 @@ export class SharingEntity implements SharingDTO {
|
||||
@Column()
|
||||
sharingKey: string;
|
||||
|
||||
@Column()
|
||||
path: string;
|
||||
@Column({
|
||||
type: 'text',
|
||||
nullable: false,
|
||||
transformer: {
|
||||
from: (val: string) => {
|
||||
return val ? JSON.parse(val) : null;
|
||||
},
|
||||
to: (val: object) => {
|
||||
return val ? JSON.stringify(val) : null;
|
||||
},
|
||||
},
|
||||
})
|
||||
searchQuery: SearchQueryDTO;
|
||||
|
||||
@Column({type: 'text', nullable: true})
|
||||
password: string;
|
||||
@@ -35,9 +47,6 @@ export class SharingEntity implements SharingDTO {
|
||||
})
|
||||
timeStamp: number;
|
||||
|
||||
@Column()
|
||||
includeSubfolders: boolean;
|
||||
|
||||
@ManyToOne(() => UserEntity, {onDelete: 'CASCADE', nullable: false})
|
||||
creator: UserDTO;
|
||||
creator: UserEntity;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {UserDTO, UserRoles} from '../../../../common/entities/UserDTO';
|
||||
import {Column, Entity, PrimaryGeneratedColumn, Unique} from 'typeorm';
|
||||
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {ContextUser} from '../../SessionContext';
|
||||
|
||||
@Entity()
|
||||
@Unique(['name'])
|
||||
export class UserEntity implements UserDTO {
|
||||
export class UserEntity implements UserDTO,ContextUser {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@@ -17,9 +18,6 @@ export class UserEntity implements UserDTO {
|
||||
@Column('smallint')
|
||||
role: UserRoles;
|
||||
|
||||
@Column('simple-array', {nullable: true})
|
||||
permissions: string[];
|
||||
|
||||
// only if this set to true, will the per-user allow/blocklist be considered
|
||||
@Column({type: 'boolean', default: false})
|
||||
overrideAllowBlockList?: boolean;
|
||||
|
||||
@@ -36,7 +36,6 @@ export class GalleryRouter {
|
||||
// common part
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.normalizePathParam('directory'),
|
||||
AuthenticationMWs.authoriseDirectories('directory'),
|
||||
VersionMWs.injectGalleryVersion,
|
||||
|
||||
// specific part
|
||||
|
||||
@@ -80,8 +80,7 @@ export class PublicRouter {
|
||||
name: req.session.context.user.name,
|
||||
role: req.session.context.user.role,
|
||||
usedSharingKey: req.session.context.user.usedSharingKey,
|
||||
permissions:req.session.context.user.permissions,
|
||||
projectionKey:req.session.context.user.projectionKey,
|
||||
projectionKey: req.session.context.user.projectionKey,
|
||||
} as UserDTO;
|
||||
|
||||
}
|
||||
|
||||
@@ -101,12 +101,10 @@ export class SharingRouter {
|
||||
|
||||
private static addListSharingForDir(app: express.Express): void {
|
||||
app.get(
|
||||
[Config.Server.apiPath + '/share/list/:directory(*)',
|
||||
Config.Server.apiPath + '/share/list//',
|
||||
Config.Server.apiPath + '/share/list'],
|
||||
[Config.Server.apiPath + '/share/list/:searchQueryDTO'],
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.User),
|
||||
SharingMWs.listSharingForDir,
|
||||
SharingMWs.listSharingForQuery,
|
||||
ServerTimingMWs.addServerTiming,
|
||||
RenderingMWs.renderSharingList
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {UserDTO} from './UserDTO';
|
||||
import {SearchQueryDTO} from './SearchQueryDTO';
|
||||
|
||||
export interface SharingDTOKey {
|
||||
sharingKey: string;
|
||||
@@ -6,18 +7,17 @@ export interface SharingDTOKey {
|
||||
|
||||
export interface SharingDTO extends SharingDTOKey {
|
||||
id: number;
|
||||
path: string;
|
||||
searchQuery: SearchQueryDTO;
|
||||
sharingKey: string;
|
||||
password: string;
|
||||
password?: string;
|
||||
expires: number;
|
||||
timeStamp: number;
|
||||
includeSubfolders: boolean;
|
||||
creator: UserDTO;
|
||||
creator?: UserDTO;
|
||||
}
|
||||
|
||||
export interface CreateSharingDTO {
|
||||
id?: number;
|
||||
password: string;
|
||||
valid: number;
|
||||
includeSubfolders: boolean;
|
||||
searchQuery: SearchQueryDTO;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import {DirectoryPathDTO} from './DirectoryDTO';
|
||||
import {Utils} from '../Utils';
|
||||
|
||||
export enum UserRoles {
|
||||
LimitedGuest = 1,
|
||||
Guest = 2,
|
||||
@@ -12,51 +9,8 @@ export enum UserRoles {
|
||||
export interface UserDTO {
|
||||
id: number;
|
||||
name: string;
|
||||
password: string;
|
||||
password?: string;
|
||||
role: UserRoles;
|
||||
usedSharingKey?: string;
|
||||
permissions: string[]; // user can only see these permissions. if ends with *, its recursive
|
||||
projectionKey?: string; // allow- and blocklist projection hash. if null, no projection
|
||||
}
|
||||
|
||||
export const UserDTOUtils = {
|
||||
isDirectoryPathAvailable: (path: string, permissions: string[]): boolean => {
|
||||
if (permissions == null) {
|
||||
return true;
|
||||
}
|
||||
permissions = permissions.map((p) => Utils.canonizePath(p));
|
||||
path = Utils.canonizePath(path);
|
||||
if (permissions.length === 0 || permissions[0] === '/*') {
|
||||
return true;
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
if (permission === '/*') {
|
||||
return true;
|
||||
}
|
||||
if (permission[permission.length - 1] === '*') {
|
||||
permission = permission.slice(0, -1);
|
||||
if (
|
||||
path.startsWith(permission) &&
|
||||
(!path[permission.length] || path[permission.length] === '/')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else if (path === permission) {
|
||||
return true;
|
||||
} else if (path === '.' && permission === '/') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isDirectoryAvailable: (
|
||||
directory: DirectoryPathDTO,
|
||||
permissions: string[]
|
||||
): boolean => {
|
||||
return UserDTOUtils.isDirectoryPathAvailable(
|
||||
Utils.concatUrls(directory.path, directory.name),
|
||||
permissions
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -183,7 +183,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
qParams[QueryParams.gallery.sharingKey_query] =
|
||||
this.shareService.getSharingKey();
|
||||
this.router
|
||||
.navigate(['/gallery', sharing.path], {queryParams: qParams})
|
||||
.navigate(['/search', sharing.searchQuery], {queryParams: qParams})
|
||||
.catch(console.error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Component, ElementRef, HostListener, ViewChild} from '@angular/core';
|
||||
import {Router, RouterLink} from '@angular/router';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {UserDTOUtils} from '../../../../../common/entities/UserDTO';
|
||||
import {UserRoles} from '../../../../../common/entities/UserDTO';
|
||||
import {AuthenticationService} from '../../../model/network/authentication.service';
|
||||
import {QueryService} from '../../../model/query.service';
|
||||
import {Utils} from '../../../../../common/Utils';
|
||||
@@ -137,7 +137,7 @@ export class GalleryNavigatorComponent {
|
||||
} else {
|
||||
arr.push({
|
||||
name: this.RootFolderName,
|
||||
route: UserDTOUtils.isDirectoryPathAvailable('/', user.permissions)
|
||||
route: user.role <= UserRoles.LimitedGuest // it's basically a sharing. they should not just navigate wherever
|
||||
? '/'
|
||||
: null,
|
||||
});
|
||||
@@ -151,7 +151,7 @@ export class GalleryNavigatorComponent {
|
||||
} else {
|
||||
arr.push({
|
||||
name,
|
||||
route: UserDTOUtils.isDirectoryPathAvailable(route, user.permissions)
|
||||
route: user.role <= UserRoles.LimitedGuest // it's basically a sharing. they should not just navigate wherever
|
||||
? route
|
||||
: null,
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {QueryParams} from '../../../../common/QueryParams';
|
||||
import {UserDTO, UserRoles} from '../../../../common/entities/UserDTO';
|
||||
import {Utils} from '../../../../common/Utils';
|
||||
import {Config} from '../../../../common/config/public/Config';
|
||||
import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@@ -80,8 +81,8 @@ export class ShareService {
|
||||
this.sharingSubject.value == null
|
||||
) {
|
||||
this.sharingKey = user.usedSharingKey || this.getSharingKey();
|
||||
if(!this.sharingKey){ //no key to fetch
|
||||
return
|
||||
if (!this.sharingKey) { //no key to fetch
|
||||
return;
|
||||
}
|
||||
await this.getSharing();
|
||||
}
|
||||
@@ -102,13 +103,11 @@ export class ShareService {
|
||||
|
||||
public createSharing(
|
||||
dir: string,
|
||||
includeSubFolders: boolean,
|
||||
password: string,
|
||||
valid: number
|
||||
): Promise<SharingDTO> {
|
||||
return this.networkService.postJson('/share/' + dir, {
|
||||
createSharing: {
|
||||
includeSubfolders: includeSubFolders,
|
||||
valid,
|
||||
...(!!password && {password: password}) // only add password if present
|
||||
} as CreateSharingDTO,
|
||||
@@ -118,14 +117,12 @@ export class ShareService {
|
||||
public updateSharing(
|
||||
dir: string,
|
||||
sharingId: number,
|
||||
includeSubFolders: boolean,
|
||||
password: string,
|
||||
valid: number
|
||||
): Promise<SharingDTO> {
|
||||
return this.networkService.putJson('/share/' + dir, {
|
||||
updateSharing: {
|
||||
id: sharingId,
|
||||
includeSubfolders: includeSubFolders,
|
||||
valid,
|
||||
password,
|
||||
} as CreateSharingDTO,
|
||||
@@ -169,7 +166,11 @@ export class ShareService {
|
||||
public async getSharingListForDir(
|
||||
dir: string
|
||||
): Promise<SharingDTO[]> {
|
||||
return this.networkService.getJson('/share/list/' + dir);
|
||||
return this.networkService.getJson('/share/list/' + JSON.stringify({
|
||||
type: SearchQueryTypes.directory,
|
||||
text: dir,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
} as TextSearch));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
|
||||
showSharingList = false;
|
||||
|
||||
input = {
|
||||
includeSubfolders: true,
|
||||
valid: {
|
||||
amount: 30,
|
||||
type: ValidityTypes.Days as ValidityTypes,
|
||||
@@ -139,7 +138,6 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
|
||||
this.sharing = await this.sharingService.updateSharing(
|
||||
this.currentDir,
|
||||
this.sharing.id,
|
||||
this.input.includeSubfolders,
|
||||
this.input.password,
|
||||
this.calcValidity()
|
||||
);
|
||||
@@ -157,7 +155,6 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
|
||||
this.url = $localize`loading..`;
|
||||
this.sharing = await this.sharingService.createSharing(
|
||||
this.currentDir,
|
||||
this.input.includeSubfolders,
|
||||
this.input.password,
|
||||
this.calcValidity()
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {TestHelper} from '../../../TestHelper';
|
||||
import {ProjectPath} from '../../../../src/backend/ProjectPath';
|
||||
import * as chai from "chai";
|
||||
import {default as chaiHttp, request} from "chai-http";
|
||||
import {DBTestHelper} from '../../DBTestHelper';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
chai.should();
|
||||
@@ -20,24 +21,21 @@ const {expect} = chai;
|
||||
chai.use(chaiHttp);
|
||||
|
||||
describe('PublicRouter', () => {
|
||||
const sqlHelper = new DBTestHelper(DatabaseType.sqlite);
|
||||
|
||||
const testUser: UserDTO = {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
password: 'test',
|
||||
role: UserRoles.User,
|
||||
permissions: null
|
||||
role: UserRoles.User
|
||||
};
|
||||
const {password: pass, ...expectedUser} = testUser;
|
||||
|
||||
let server: Server;
|
||||
const setUp = async () => {
|
||||
await fs.promises.rm(TestHelper.TMP_DIR, {recursive: true, force: true});
|
||||
await sqlHelper.initDB();
|
||||
Config.Users.authenticationRequired = true;
|
||||
Config.Sharing.enabled = true;
|
||||
Config.Database.type = DatabaseType.sqlite;
|
||||
Config.Database.dbFolder = TestHelper.TMP_DIR;
|
||||
ProjectPath.reset();
|
||||
|
||||
server = new Server(false);
|
||||
await server.onStarted.wait();
|
||||
@@ -47,8 +45,7 @@ describe('PublicRouter', () => {
|
||||
await SQLConnection.close();
|
||||
};
|
||||
const tearDown = async () => {
|
||||
await ObjectManagers.reset();
|
||||
await fs.promises.rm(TestHelper.TMP_DIR, {recursive: true, force: true});
|
||||
await sqlHelper.clearDB();
|
||||
};
|
||||
|
||||
const shouldHaveInjectedUser = (result: any, user: any) => {
|
||||
@@ -61,6 +58,17 @@ describe('PublicRouter', () => {
|
||||
|
||||
const u = JSON.parse(result.text.substring(result.text.indexOf(startToken) + startToken.length, result.text.indexOf(endToken)));
|
||||
|
||||
if (user == null) {
|
||||
expect(u).to.equal(null);
|
||||
return;
|
||||
}
|
||||
// Only public-safe subset is injected; ensure projectionKey is present but do not assert its value
|
||||
expect(u).to.be.an('object');
|
||||
expect(u).to.have.property('name', user.name);
|
||||
expect(u).to.have.property('role', user.role);
|
||||
expect(u).to.have.property('usedSharingKey', user.usedSharingKey);
|
||||
expect(u).to.have.property('projectionKey');
|
||||
expect(u.projectionKey).to.be.a('string').and.to.have.length.greaterThan(0);
|
||||
expect(u).to.deep.equal(user);
|
||||
};
|
||||
|
||||
@@ -94,13 +102,9 @@ describe('PublicRouter', () => {
|
||||
Config.Sharing.passwordRequired = false;
|
||||
const sharing = await RouteTestingHelper.createSharing(testUser);
|
||||
const res = await fistLoad(server, sharing.sharingKey);
|
||||
shouldHaveInjectedUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
|
||||
shouldHaveInjectedUser(res, RouteTestingHelper.getExpectedSharingUserForUI(sharing));
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -2,6 +2,11 @@ import {SharingDTO} from '../../../../src/common/entities/SharingDTO';
|
||||
import {ObjectManagers} from '../../../../src/backend/model/ObjectManagers';
|
||||
import {UserDTO, UserRoles} from '../../../../src/common/entities/UserDTO';
|
||||
import {Utils} from '../../../../src/common/Utils';
|
||||
import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../../src/common/entities/SearchQueryDTO';
|
||||
import * as chai from 'chai';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
const should = chai.should();
|
||||
|
||||
export class RouteTestingHelper {
|
||||
|
||||
@@ -9,25 +14,62 @@ export class RouteTestingHelper {
|
||||
static async createSharing(testUser: UserDTO, password: string = null): Promise<SharingDTO> {
|
||||
const sharing = {
|
||||
sharingKey: 'sharing_test_key_' + Date.now(),
|
||||
path: 'test',
|
||||
searchQuery: {type: SearchQueryTypes.directory, text: 'test', matchType: TextSearchQueryMatchTypes.exact_match} as TextSearch,
|
||||
expires: Date.now() + 1000,
|
||||
timeStamp: Date.now(),
|
||||
includeSubfolders: false,
|
||||
creator: testUser
|
||||
} as SharingDTO;
|
||||
} as any;
|
||||
if (password) {
|
||||
sharing.password = password;
|
||||
}
|
||||
await ObjectManagers.getInstance().SharingManager.createSharing(Utils.clone(sharing)); // do not rewrite password
|
||||
await ObjectManagers.getInstance().SharingManager.createSharing(Utils.clone(sharing)); // do not rewrite the password
|
||||
return sharing;
|
||||
}
|
||||
|
||||
public static getExpectedSharingUser(sharing: SharingDTO): UserDTO {
|
||||
return {
|
||||
public static getExpectedSharingUserForUI(sharing: SharingDTO): UserDTO {
|
||||
const u = {
|
||||
name: 'Guest',
|
||||
role: UserRoles.LimitedGuest,
|
||||
permissions: [sharing.path],
|
||||
usedSharingKey: sharing.sharingKey
|
||||
usedSharingKey: sharing.sharingKey,
|
||||
} as UserDTO;
|
||||
const q = ObjectManagers.getInstance().buildAllowListForSharing(sharing as any);
|
||||
u.projectionKey = crypto.createHash('md5').update(JSON.stringify(q)).digest('hex');
|
||||
|
||||
return u;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the result sent to UI is a valid user object
|
||||
* @param result
|
||||
* @param user
|
||||
*/
|
||||
public static shouldBeValidUIUser = (result: any, user: any) => {
|
||||
|
||||
result.should.have.status(200);
|
||||
result.body.should.be.a('object');
|
||||
should.equal(result.body.error, null);
|
||||
const {...u} = result.body.result;
|
||||
// Ensure sensitive fields are not leaked
|
||||
(u as any).should.not.have.property('password');
|
||||
// Ensure server does not leak internal allow/block queries
|
||||
(u as any).should.not.have.property('allowQuery');
|
||||
(u as any).should.not.have.property('blockQuery');
|
||||
(u as any).should.not.have.property('overrideAllowBlockList');
|
||||
|
||||
// Check core identity fields
|
||||
(u as any).should.have.property('name', user.name);
|
||||
(u as any).should.have.property('role', user.role);
|
||||
if (typeof user.id !== 'undefined') {
|
||||
(u as any).should.have.property('id', user.id);
|
||||
}
|
||||
if (typeof user.usedSharingKey !== 'undefined') {
|
||||
(u as any).should.have.property('usedSharingKey', user.usedSharingKey);
|
||||
}
|
||||
// projectionKey may be present; if present, ensure it is a non-empty string
|
||||
if (typeof (u as any).projectionKey !== 'undefined') {
|
||||
(u as any).projectionKey.should.be.a('string');
|
||||
((u as any).projectionKey as string).length.should.be.greaterThan(0);
|
||||
}
|
||||
u.should.deep.equal(user);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ describe('SharingRouter', () => {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
password: 'test',
|
||||
role: UserRoles.User,
|
||||
permissions: null
|
||||
role: UserRoles.User
|
||||
};
|
||||
const {password: pass, ...expectedUser} = testUser;
|
||||
const tempDir = path.join(__dirname, '../../tmp');
|
||||
@@ -51,14 +50,6 @@ describe('SharingRouter', () => {
|
||||
await fs.promises.rm(tempDir, {recursive: true, force: true});
|
||||
};
|
||||
|
||||
const shouldBeValidUser = (result: any, user: any) => {
|
||||
|
||||
result.should.have.status(200);
|
||||
result.body.should.be.a('object');
|
||||
should.equal(result.body.error, null);
|
||||
const {...u} = result.body.result;
|
||||
u.should.deep.equal(user);
|
||||
};
|
||||
|
||||
const shareLogin = async (srv: Server, sharingKey: string, password?: string): Promise<any> => {
|
||||
return (request.execute(srv.Server) as SuperAgentStatic)
|
||||
@@ -77,14 +68,14 @@ describe('SharingRouter', () => {
|
||||
Config.Sharing.passwordRequired = true;
|
||||
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
|
||||
const res = await shareLogin(server, sharing.sharingKey, sharing.password);
|
||||
shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
|
||||
RouteTestingHelper.shouldBeValidUIUser(res, RouteTestingHelper.getExpectedSharingUserForUI(sharing));
|
||||
});
|
||||
|
||||
it('should login with passworded share when password not required', async () => {
|
||||
Config.Sharing.passwordRequired = false;
|
||||
const sharing = await RouteTestingHelper.createSharing(testUser, 'secret_pass');
|
||||
const res = await shareLogin(server, sharing.sharingKey, sharing.password);
|
||||
shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
|
||||
RouteTestingHelper.shouldBeValidUIUser(res, RouteTestingHelper.getExpectedSharingUserForUI(sharing));
|
||||
});
|
||||
|
||||
|
||||
@@ -92,7 +83,7 @@ describe('SharingRouter', () => {
|
||||
Config.Sharing.passwordRequired = false;
|
||||
const sharing = await RouteTestingHelper.createSharing(testUser );
|
||||
const res = await shareLogin(server, sharing.sharingKey, sharing.password);
|
||||
shouldBeValidUser(res, RouteTestingHelper.getExpectedSharingUser(sharing));
|
||||
RouteTestingHelper.shouldBeValidUIUser(res, RouteTestingHelper.getExpectedSharingUserForUI(sharing));
|
||||
});
|
||||
|
||||
|
||||
@@ -15,28 +15,25 @@ import {DatabaseType} from '../../../../src/common/config/private/PrivateConfig'
|
||||
import {ProjectPath} from '../../../../src/backend/ProjectPath';
|
||||
import * as chai from "chai";
|
||||
import {default as chaiHttp, request} from "chai-http";
|
||||
import {DBTestHelper} from '../../DBTestHelper';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
const should = chai.should();
|
||||
chai.use(chaiHttp);
|
||||
|
||||
describe('UserRouter', () => {
|
||||
const sqlHelper = new DBTestHelper(DatabaseType.sqlite);
|
||||
|
||||
const testUser: UserDTO = {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
password: 'test',
|
||||
role: UserRoles.User,
|
||||
permissions: null
|
||||
role: UserRoles.User
|
||||
};
|
||||
const {password, ...expectedUser} = testUser;
|
||||
const tempDir = path.join(__dirname, '../../tmp');
|
||||
let server: Server;
|
||||
const setUp = async () => {
|
||||
await fs.promises.rm(tempDir, {recursive: true, force: true});
|
||||
Config.Database.type = DatabaseType.sqlite;
|
||||
Config.Database.dbFolder = tempDir;
|
||||
ProjectPath.reset();
|
||||
await sqlHelper.initDB()
|
||||
|
||||
server = new Server(false);
|
||||
await server.onStarted.wait();
|
||||
@@ -45,18 +42,10 @@ describe('UserRouter', () => {
|
||||
await SQLConnection.close();
|
||||
};
|
||||
const tearDown = async () => {
|
||||
await ObjectManagers.reset();
|
||||
await fs.promises.rm(tempDir, {recursive: true, force: true});
|
||||
await sqlHelper.clearDB()
|
||||
};
|
||||
|
||||
const checkUserResult = (result: any, user: any) => {
|
||||
|
||||
result.should.have.status(200);
|
||||
result.body.should.be.a('object');
|
||||
should.equal(result.body.error, null);
|
||||
const {...u} = result.body.result;
|
||||
u.should.deep.equal(user);
|
||||
};
|
||||
|
||||
const login = async (srv: Server): Promise<any> => {
|
||||
const result = await (request.execute(srv.Server) as SuperAgentStatic)
|
||||
@@ -69,7 +58,7 @@ describe('UserRouter', () => {
|
||||
} as LoginCredential
|
||||
});
|
||||
|
||||
checkUserResult(result, expectedUser);
|
||||
RouteTestingHelper.shouldBeValidUIUser(result, expectedUser);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -106,7 +95,7 @@ describe('UserRouter', () => {
|
||||
.get(Config.Server.apiPath + '/user/me')
|
||||
.set('Cookie', loginRes.res.headers['set-cookie']);
|
||||
|
||||
checkUserResult(result, expectedUser);
|
||||
RouteTestingHelper.shouldBeValidUIUser(result, expectedUser);
|
||||
});
|
||||
|
||||
it('it should not authenticate', async () => {
|
||||
@@ -134,7 +123,7 @@ describe('UserRouter', () => {
|
||||
.set('Cookie', loginRes.res.headers['set-cookie']);
|
||||
|
||||
// should return with logged in user, not limited sharing one
|
||||
checkUserResult(result, expectedUser);
|
||||
RouteTestingHelper.shouldBeValidUIUser(result, expectedUser);
|
||||
});
|
||||
|
||||
|
||||
@@ -150,7 +139,7 @@ describe('UserRouter', () => {
|
||||
const result = await request.execute(server.Server)
|
||||
.get(Config.Server.apiPath + '/user/me?' + QueryParams.gallery.sharingKey_query + '=' + sharing.sharingKey);
|
||||
|
||||
checkUserResult(result, RouteTestingHelper.getExpectedSharingUser(sharing));
|
||||
RouteTestingHelper.shouldBeValidUIUser(result, RouteTestingHelper.getExpectedSharingUserForUI(sharing));
|
||||
});
|
||||
|
||||
it('it should not authenticate with sharing key without password', async () => {
|
||||
@@ -183,7 +172,7 @@ describe('UserRouter', () => {
|
||||
} as UserDTO;
|
||||
|
||||
|
||||
checkUserResult(result, expectedGuestUser);
|
||||
RouteTestingHelper.shouldBeValidUIUser(result, expectedGuestUser);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -78,61 +78,6 @@ describe('Authentication middleware', (sqlHelper: DBTestHelper) => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('authorisePath', () => {
|
||||
|
||||
const req = {
|
||||
session: {
|
||||
context: {
|
||||
user: {permissions: null as string[]}
|
||||
}
|
||||
},
|
||||
sessionOptions: {},
|
||||
query: {},
|
||||
params: {
|
||||
path: '/test'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const authoriseDirPath = AuthenticationMWs.authoriseDirectories('path');
|
||||
const test = (relativePath: string): Promise<string | number> => {
|
||||
return new Promise((resolve) => {
|
||||
req.params.path = path.normalize(relativePath);
|
||||
authoriseDirPath(req as any, {sendStatus: resolve} as any, () => {
|
||||
resolve('ok');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
it('should catch unauthorized path usage', async () => {
|
||||
req.session.context.user.permissions = [path.normalize('/sub/subsub')];
|
||||
expect(await test('/sub/subsub')).to.be.eql('ok');
|
||||
expect(await test('/test')).to.be.eql(403);
|
||||
expect(await test('/')).to.be.eql(403);
|
||||
expect(await test('/sub/test')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub/test')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub/test/test2')).to.be.eql(403);
|
||||
req.session.context.user.permissions = [path.normalize('/sub/subsub'), path.normalize('/sub/subsub2')];
|
||||
expect(await test('/sub/subsub2')).to.be.eql('ok');
|
||||
expect(await test('/sub/subsub')).to.be.eql('ok');
|
||||
expect(await test('/test')).to.be.eql(403);
|
||||
expect(await test('/')).to.be.eql(403);
|
||||
expect(await test('/sub/test')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub/test')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub2/test')).to.be.eql(403);
|
||||
req.session.context.user.permissions = [path.normalize('/sub/subsub*')];
|
||||
expect(await test('/b')).to.be.eql(403);
|
||||
expect(await test('/sub')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub2')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub2/test')).to.be.eql(403);
|
||||
expect(await test('/sub/subsub')).to.be.eql('ok');
|
||||
expect(await test('/sub/subsub/test')).to.be.eql('ok');
|
||||
expect(await test('/sub/subsub/test/two')).to.be.eql('ok');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('inverseAuthenticate', () => {
|
||||
|
||||
it('should call next with error on authenticated', (done: (err?: any) => void) => {
|
||||
@@ -553,10 +498,10 @@ describe('Authentication middleware', (sqlHelper: DBTestHelper) => {
|
||||
|
||||
describe('authoriseMedia', () => {
|
||||
|
||||
const buildReq = (mediaRelPath: string, permissions: string[]) => ({
|
||||
const buildReq = (mediaRelPath: string) => ({
|
||||
session: {
|
||||
context: {
|
||||
user: {permissions}
|
||||
user: {role: UserRoles.LimitedGuest}
|
||||
}
|
||||
},
|
||||
params: {
|
||||
@@ -574,15 +519,8 @@ describe('Authentication middleware', (sqlHelper: DBTestHelper) => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should deny if directory permission fails', async () => {
|
||||
const req = buildReq('/private/dir/photo.jpg', [path.normalize('/allowed')]);
|
||||
// No GalleryManager mock needed; should fail before DB check
|
||||
const result = await run(req);
|
||||
expect(result).to.eql(403);
|
||||
});
|
||||
|
||||
it('should call next if directory permitted and GalleryManager.authoriseMedia allows', async () => {
|
||||
const req = buildReq('/allowed/dir/photo.jpg', [path.normalize('/allowed*')]);
|
||||
it('should call next if GalleryManager.authoriseMedia allows', async () => {
|
||||
const req = buildReq('/allowed/dir/photo.jpg');
|
||||
|
||||
// Mock GalleryManager to allow
|
||||
(ObjectManagers.getInstance() as any).GalleryManager = {
|
||||
@@ -594,7 +532,7 @@ describe('Authentication middleware', (sqlHelper: DBTestHelper) => {
|
||||
});
|
||||
|
||||
it('should deny if GalleryManager.authoriseMedia denies', async () => {
|
||||
const req = buildReq('/allowed/dir/photo.jpg', [path.normalize('/allowed*')]);
|
||||
const req = buildReq('/allowed/dir/photo.jpg');
|
||||
|
||||
// Mock GalleryManager to deny
|
||||
(ObjectManagers.getInstance() as any).GalleryManager = {
|
||||
@@ -606,7 +544,7 @@ describe('Authentication middleware', (sqlHelper: DBTestHelper) => {
|
||||
});
|
||||
|
||||
it('should deny (403) on error thrown by GalleryManager.authoriseMedia', async () => {
|
||||
const req = buildReq('/allowed/dir/photo.jpg', [path.normalize('/allowed*')]);
|
||||
const req = buildReq('/allowed/dir/photo.jpg');
|
||||
|
||||
(ObjectManagers.getInstance() as any).GalleryManager = {
|
||||
authoriseMedia: async () => {
|
||||
@@ -619,6 +557,55 @@ describe('Authentication middleware', (sqlHelper: DBTestHelper) => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('authoriseMetaFiles', () => {
|
||||
const buildReq = (metaPath: string) => ({
|
||||
session: {
|
||||
context: {
|
||||
user: {role: UserRoles.LimitedGuest}
|
||||
}
|
||||
},
|
||||
params: {
|
||||
metaPath: path.normalize(metaPath)
|
||||
}
|
||||
});
|
||||
|
||||
const run = (req: any) => new Promise((resolve) => {
|
||||
const res = {sendStatus: (code: number) => resolve(code)} as any;
|
||||
const next = () => resolve('ok');
|
||||
const mw = AuthenticationMWs.authoriseMetaFiles('metaPath');
|
||||
AuthenticationMWs.normalizePathParam('metaPath')(req as any, {} as any, () => {
|
||||
(mw as any)(req as any, res, next);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call next if GalleryManager.authoriseMetaFile allows', async () => {
|
||||
const req = buildReq('/some/dir/notes.md');
|
||||
(ObjectManagers.getInstance() as any).GalleryManager = {
|
||||
authoriseMetaFile: async (_session: any, _path: string) => true
|
||||
} as any;
|
||||
const result = await run(req);
|
||||
expect(result).to.eql('ok');
|
||||
});
|
||||
|
||||
it('should deny if GalleryManager.authoriseMetaFile denies', async () => {
|
||||
const req = buildReq('/some/dir/notes.md');
|
||||
(ObjectManagers.getInstance() as any).GalleryManager = {
|
||||
authoriseMetaFile: async (_session: any, _path: string) => false
|
||||
} as any;
|
||||
const result = await run(req);
|
||||
expect(result).to.eql(403);
|
||||
});
|
||||
|
||||
it('should deny (403) on error thrown by GalleryManager.authoriseMetaFile', async () => {
|
||||
const req = buildReq('/some/dir/notes.md');
|
||||
(ObjectManagers.getInstance() as any).GalleryManager = {
|
||||
authoriseMetaFile: async () => { throw new Error('db error'); }
|
||||
} as any;
|
||||
const result = await run(req);
|
||||
expect(result).to.eql(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should call next on logout', (done: (err?: any) => void) => {
|
||||
const req: any = {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {ANDSearchQuery, SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes}
|
||||
import {UserRoles} from '../../../../src/common/entities/UserDTO';
|
||||
import {Brackets} from 'typeorm';
|
||||
import {DBTestHelper} from '../../DBTestHelper';
|
||||
import {SharingEntity} from '../../../../src/backend/model/database/enitites/SharingEntity';
|
||||
|
||||
declare let describe: any;
|
||||
declare const it: any;
|
||||
@@ -31,7 +32,6 @@ describe('ObjectManagers', (sqlHelper: DBTestHelper) => {
|
||||
user.id = 1;
|
||||
user.name = 'testuser';
|
||||
user.role = UserRoles.Admin;
|
||||
user.permissions = ['/test'];
|
||||
|
||||
// Create the context
|
||||
const context = await ObjectManagers.getInstance().buildContext(user);
|
||||
@@ -214,4 +214,96 @@ describe('ObjectManagers', (sqlHelper: DBTestHelper) => {
|
||||
expect(context1.user.projectionKey).to.be.eql(context2.user.projectionKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildAllowListForSharing', () => {
|
||||
// Reset ObjectManagers before each test
|
||||
beforeEach(async () => {
|
||||
await sqlHelper.initDB();
|
||||
});
|
||||
|
||||
afterEach(sqlHelper.clearDB);
|
||||
|
||||
it('should return sharing.searchQuery unchanged when creator has no allow/block query', async () => {
|
||||
const creator = new UserEntity();
|
||||
creator.id = 10;
|
||||
creator.name = 'creator1';
|
||||
creator.role = UserRoles.Admin;
|
||||
creator.overrideAllowBlockList = true; // use only user queries; none set
|
||||
|
||||
const sharing = new SharingEntity();
|
||||
sharing.creator = creator as any;
|
||||
sharing.searchQuery = {
|
||||
type: SearchQueryTypes.directory,
|
||||
text: '/shared/path',
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
} as TextSearch;
|
||||
|
||||
const result = ObjectManagers.getInstance().buildAllowListForSharing(sharing);
|
||||
expect(result).to.be.eql(sharing.searchQuery);
|
||||
});
|
||||
|
||||
it('should AND creator allowQuery with sharing.searchQuery', async () => {
|
||||
const creator = new UserEntity();
|
||||
creator.id = 11;
|
||||
creator.name = 'creator2';
|
||||
creator.role = UserRoles.Admin;
|
||||
creator.overrideAllowBlockList = true;
|
||||
creator.allowQuery = {
|
||||
type: SearchQueryTypes.directory,
|
||||
text: '/allowed/by/creator',
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
} as TextSearch;
|
||||
|
||||
const sharing = new SharingEntity();
|
||||
sharing.creator = creator as any;
|
||||
sharing.searchQuery = {
|
||||
type: SearchQueryTypes.file_name,
|
||||
text: 'holiday',
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
} as TextSearch;
|
||||
|
||||
const result = ObjectManagers.getInstance().buildAllowListForSharing(sharing) as ANDSearchQuery;
|
||||
expect(result.type).to.be.eql(SearchQueryTypes.AND);
|
||||
expect(result.list).to.have.lengthOf(2);
|
||||
expect(result.list[0]).to.be.eql(creator.allowQuery);
|
||||
expect(result.list[1]).to.be.eql(sharing.searchQuery);
|
||||
});
|
||||
|
||||
it('should AND creator (allow AND negated block) with sharing.searchQuery', async () => {
|
||||
const creator = new UserEntity();
|
||||
creator.id = 12;
|
||||
creator.name = 'creator3';
|
||||
creator.role = UserRoles.Admin;
|
||||
creator.overrideAllowBlockList = true;
|
||||
creator.allowQuery = {
|
||||
type: SearchQueryTypes.directory,
|
||||
text: '/allowed',
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
} as TextSearch;
|
||||
creator.blockQuery = {
|
||||
type: SearchQueryTypes.file_name,
|
||||
text: 'secret',
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
negate: false
|
||||
} as TextSearch;
|
||||
|
||||
const sharing = new SharingEntity();
|
||||
sharing.creator = creator as any;
|
||||
sharing.searchQuery = {
|
||||
type: SearchQueryTypes.directory,
|
||||
text: '/event/path',
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
} as TextSearch;
|
||||
|
||||
const result = ObjectManagers.getInstance().buildAllowListForSharing(sharing) as ANDSearchQuery;
|
||||
expect(result.type).to.be.eql(SearchQueryTypes.AND);
|
||||
expect(result.list).to.have.lengthOf(2);
|
||||
const creatorQuery = result.list[0] as ANDSearchQuery;
|
||||
expect(creatorQuery.type).to.be.eql(SearchQueryTypes.AND);
|
||||
expect(creatorQuery.list).to.have.lengthOf(2);
|
||||
expect(creatorQuery.list[0]).to.be.eql(creator.allowQuery);
|
||||
expect((creatorQuery.list[1] as TextSearch).negate).to.be.true;
|
||||
expect(result.list[1]).to.be.eql(sharing.searchQuery);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {DBTestHelper} from '../../../DBTestHelper';
|
||||
import {GalleryManager} from '../../../../../src/backend/model/database/GalleryManager';
|
||||
import {ParentDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
|
||||
@@ -338,6 +339,130 @@ describe('GalleryManager', (sqlHelper: DBTestHelper) => {
|
||||
expect(calledArgs[1]).to.equal(true);
|
||||
});
|
||||
});
|
||||
describe('GalleryManager authorisation', () => {
|
||||
|
||||
let gm: GalleryManager;
|
||||
|
||||
before(async () => {
|
||||
await sqlHelper.initDB();
|
||||
await sqlHelper.setUpTestGallery();
|
||||
await ObjectManagers.getInstance().init();
|
||||
gm = new GalleryManager();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await sqlHelper.clearDB();
|
||||
});
|
||||
|
||||
|
||||
describe('authoriseMedia', () => {
|
||||
it('returns true without projection', async () => {
|
||||
const session = new SessionContext();
|
||||
const root = sqlHelper.testGalleyEntities.dir;
|
||||
const media = sqlHelper.testGalleyEntities.p; // Photo1_*.jpg in root
|
||||
const mediaPath = path.join(root.path, root.name, media.name);
|
||||
|
||||
const res = await gm.authoriseMedia(session, mediaPath);
|
||||
expect(res).to.equal(true);
|
||||
});
|
||||
|
||||
it('enforces projection: allows only matching media name (like "Photo1")', async () => {
|
||||
const searchQuery = {
|
||||
type: SearchQueryTypes.file_name,
|
||||
text: 'photo1',
|
||||
matchType: TextSearchQueryMatchTypes.like,
|
||||
};
|
||||
const session = await ObjectManagers.getInstance().buildContext({
|
||||
allowQuery: searchQuery,
|
||||
overrideAllowBlockList: true,
|
||||
} as any);
|
||||
|
||||
const root = sqlHelper.testGalleyEntities.dir;
|
||||
const p1 = sqlHelper.testGalleyEntities.p; // Photo1_*.jpg
|
||||
const p2 = sqlHelper.testGalleyEntities.p2; // Photo2_*.jpg
|
||||
|
||||
const p1Path = path.join(root.path, root.name, p1.name);
|
||||
const p2Path = path.join(root.path, root.name, p2.name);
|
||||
|
||||
expect(await gm.authoriseMedia(session, p1Path)).to.equal(true);
|
||||
expect(await gm.authoriseMedia(session, p2Path)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false when file exists but under different directory (directory constraint)', async () => {
|
||||
const searchQuery = {
|
||||
type: SearchQueryTypes.file_name,
|
||||
text: 'photo1',
|
||||
matchType: TextSearchQueryMatchTypes.like,
|
||||
};
|
||||
const session = await ObjectManagers.getInstance().buildContext({
|
||||
allowQuery: searchQuery,
|
||||
overrideAllowBlockList: true,
|
||||
} as any);
|
||||
|
||||
const subDir = sqlHelper.testGalleyEntities.subDir; // The Phantom Menace
|
||||
const p1 = sqlHelper.testGalleyEntities.p; // lives in root
|
||||
const wrongPath = path.join(subDir.path, subDir.name, p1.name);
|
||||
|
||||
expect(await gm.authoriseMedia(session, wrongPath)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false for non-existent file under projection', async () => {
|
||||
const searchQuery = {
|
||||
type: SearchQueryTypes.file_name,
|
||||
text: 'photo1',
|
||||
matchType: TextSearchQueryMatchTypes.like,
|
||||
};
|
||||
const session = await ObjectManagers.getInstance().buildContext({
|
||||
allowQuery: searchQuery,
|
||||
overrideAllowBlockList: true,
|
||||
} as any);
|
||||
|
||||
const root = sqlHelper.testGalleyEntities.dir;
|
||||
const nonExist = path.join(root.path, root.name, 'does_not_exist.jpg');
|
||||
expect(await gm.authoriseMedia(session, nonExist)).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authoriseMetaFile', () => {
|
||||
it('returns true without projection', async () => {
|
||||
const session = new SessionContext();
|
||||
const root = sqlHelper.testGalleyEntities.dir;
|
||||
const metaPath = path.join(root.path, root.name, 'README.md');
|
||||
expect(await gm.authoriseMetaFile(session, metaPath)).to.equal(true);
|
||||
});
|
||||
|
||||
it('allows directory that contains matching media (projection applied)', async () => {
|
||||
const searchQuery = {
|
||||
type: SearchQueryTypes.file_name,
|
||||
text: 'photo1',
|
||||
matchType: TextSearchQueryMatchTypes.like,
|
||||
};
|
||||
const session = await ObjectManagers.getInstance().buildContext({
|
||||
allowQuery: searchQuery,
|
||||
overrideAllowBlockList: true,
|
||||
} as any);
|
||||
|
||||
const root = sqlHelper.testGalleyEntities.dir; // root contains Photo1_*.jpg
|
||||
const metaPath = path.join(root.path, root.name, 'folder.gpx');
|
||||
expect(await gm.authoriseMetaFile(session, metaPath)).to.equal(true);
|
||||
});
|
||||
|
||||
it('denies directory that has no matching media under projection', async () => {
|
||||
const searchQuery = {
|
||||
type: SearchQueryTypes.file_name,
|
||||
text: 'photo1',
|
||||
matchType: TextSearchQueryMatchTypes.like,
|
||||
};
|
||||
const session = await ObjectManagers.getInstance().buildContext({
|
||||
allowQuery: searchQuery,
|
||||
overrideAllowBlockList: true,
|
||||
} as any);
|
||||
|
||||
const subDir = sqlHelper.testGalleyEntities.subDir; // contains Photo3_*.jpg
|
||||
const metaPath = path.join(subDir.path, subDir.name, 'info.md');
|
||||
expect(await gm.authoriseMetaFile(session, metaPath)).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1576,7 +1576,7 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
|
||||
(it('should flatter SOME_OF query', () => {
|
||||
const sm = new SearchManagerTest();
|
||||
const parser = new SearchQueryParser();
|
||||
const alphabet = 'abcdefghijklmnopqrstu';
|
||||
const alphabet = 'abcdefghijklmnopqrs';
|
||||
|
||||
|
||||
const shortestDepth = (q: SearchQueryDTO): number => {
|
||||
|
||||
@@ -1,26 +1,8 @@
|
||||
import {expect} from 'chai';
|
||||
import {UserDTO, UserDTOUtils} from '../../../src/common/entities/UserDTO';
|
||||
import {UserRoles} from '../../../src/common/entities/UserDTO';
|
||||
|
||||
describe('UserDTO', () => {
|
||||
|
||||
|
||||
it('should check available path', () => {
|
||||
expect(UserDTOUtils.isDirectoryPathAvailable('/', ['/'])).to.be.equals(true);
|
||||
expect(UserDTOUtils.isDirectoryPathAvailable('/', ['/subfolder', '/'])).to.be.equals(true);
|
||||
expect(UserDTOUtils.isDirectoryPathAvailable('/abc', ['/subfolder', '/'])).to.be.equals(false);
|
||||
expect(UserDTOUtils.isDirectoryPathAvailable('/abc', ['/subfolder', '/*'])).to.be.equals(true);
|
||||
expect(UserDTOUtils.isDirectoryPathAvailable('/abc', ['/subfolder'])).to.be.equals(false);
|
||||
expect(UserDTOUtils.isDirectoryPathAvailable('/abc/two', ['/subfolder'])).to.be.equals(false);
|
||||
expect(UserDTOUtils.isDirectoryPathAvailable('/abc/two', ['/'])).to.be.equals(false);
|
||||
expect(UserDTOUtils.isDirectoryPathAvailable('/abc/two', ['/*'])).to.be.equals(true);
|
||||
it('should expose UserRoles enum', () => {
|
||||
expect(UserRoles.Admin).to.be.a('number');
|
||||
});
|
||||
|
||||
it('should check directory', () => {
|
||||
expect(UserDTOUtils.isDirectoryAvailable({path: '/', name: 'abc'} as any, ['/*'])).to.be.equals(true);
|
||||
expect(UserDTOUtils.isDirectoryAvailable({path: '/', name: 'abc'} as any, ['/'])).to.be.equals(false);
|
||||
expect(UserDTOUtils.isDirectoryAvailable({path: '.\\', name: '.'} as any, ['/'])).to.be.equals(true);
|
||||
expect(UserDTOUtils.isDirectoryAvailable({path: '/', name: 'abc'} as any, ['/*', '/asdad'])).to.be.equals(true);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user