diff --git a/backend/middlewares/SharingMWs.ts b/backend/middlewares/SharingMWs.ts index 2fb6eb6c..8f5c75f6 100644 --- a/backend/middlewares/SharingMWs.ts +++ b/backend/middlewares/SharingMWs.ts @@ -4,23 +4,13 @@ import {ObjectManagers} from '../model/ObjectManagers'; import {ErrorCodes, ErrorDTO} from '../../common/entities/Error'; import {Config} from '../../common/config/private/Config'; import {QueryParams} from '../../common/QueryParams'; +import * as path from 'path'; const LOG_TAG = '[SharingMWs]'; export class SharingMWs { - private static generateKey(): string { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - - return s4() + s4(); - } - - public static async getSharing(req: Request, res: Response, next: NextFunction) { if (Config.Client.Sharing.enabled === false) { return next(); @@ -59,7 +49,7 @@ export class SharingMWs { } - const directoryName = req.params.directory || '/'; + const directoryName = path.normalize(req.params.directory || '/'); const sharing: SharingDTO = { id: null, sharingKey: sharingKey, @@ -90,7 +80,7 @@ export class SharingMWs { return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'updateSharing filed is missing')); } const updateSharing: CreateSharingDTO = req.body.updateSharing; - const directoryName = req.params.directory || '/'; + const directoryName = path.normalize(req.params.directory || '/'); const sharing: SharingDTO = { id: updateSharing.id, path: directoryName, @@ -110,4 +100,14 @@ export class SharingMWs { } } + + private static generateKey(): string { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + + return s4() + s4(); + } } diff --git a/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts b/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts index 5fa98f10..027e37e6 100644 --- a/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts +++ b/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts @@ -85,7 +85,7 @@ export class ThumbnailGeneratorMWs { } // load parameters - const mediaPath = path.resolve(ProjectPath.ImageFolder, photo.directory.path, photo.directory.name, photo.name); + const mediaPath = path.join(ProjectPath.ImageFolder, photo.directory.path, photo.directory.name, photo.name); const size: number = Config.Client.Thumbnail.personThumbnailSize; const personName = photo.metadata.faces[0].name; // generate thumbnail path diff --git a/backend/middlewares/user/AuthenticationMWs.ts b/backend/middlewares/user/AuthenticationMWs.ts index 0bfe3946..8f5158ac 100644 --- a/backend/middlewares/user/AuthenticationMWs.ts +++ b/backend/middlewares/user/AuthenticationMWs.ts @@ -7,6 +7,7 @@ import {Config} from '../../../common/config/private/Config'; import {PasswordHelper} from '../../model/PasswordHelper'; import {Utils} from '../../../common/Utils'; import {QueryParams} from '../../../common/QueryParams'; +import * as path from 'path'; export class AuthenticationMWs { @@ -54,21 +55,30 @@ export class AuthenticationMWs { return next(); } - public static authoriseDirectory(req: Request, res: Response, next: NextFunction) { - if (req.session.user.permissions == null || - req.session.user.permissions.length === 0 || - req.session.user.permissions[0] === '/*') { - return next(); - } - const directoryName = req.params.directory || '/'; - if (UserDTO.isPathAvailable(directoryName, req.session.user.permissions) === true) { + public static normalizePathParam(paramName: string) { + return (req: Request, res: Response, next: NextFunction) => { + req.params[paramName] = path.normalize(req.params[paramName] || path.sep).replace(/^(\.\.[\/\\])+/, ''); return next(); - } - - return next(new ErrorDTO(ErrorCodes.PERMISSION_DENIED)); + }; } + public static authorisePath(paramName: string, isDirectory: boolean) { + return (req: Request, res: Response, next: NextFunction) => { + let p: string = req.params[paramName]; + if (!isDirectory) { + p = path.dirname(p); + } + + if (!UserDTO.isDirectoryPathAvailable(p, req.session.user.permissions, path.sep)) { + return res.sendStatus(403); + } + + return next(); + }; + } + + public static authorise(role: UserRoles) { return (req: Request, res: Response, next: NextFunction) => { if (req.session.user.role < role) { @@ -102,12 +112,12 @@ export class AuthenticationMWs { return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND)); } - let path = sharing.path; + let sharingPath = sharing.path; if (sharing.includeSubfolders === true) { - path += '*'; + sharingPath += '*'; } - req.session.user = {name: 'Guest', role: UserRoles.LimitedGuest, permissions: [path]}; + req.session.user = {name: 'Guest', role: UserRoles.LimitedGuest, permissions: [sharingPath]}; return next(); } catch (err) { @@ -152,6 +162,12 @@ export class AuthenticationMWs { } + public static logout(req: Request, res: Response, next: NextFunction) { + delete req.session.user; + delete req.session.rememberMe; + return next(); + } + private static async getSharingUser(req: Request) { if (Config.Client.Sharing.enabled === true && (!!req.params[QueryParams.gallery.sharingKey_short] || !!req.params[QueryParams.gallery.sharingKey_long])) { @@ -166,20 +182,19 @@ export class AuthenticationMWs { return null; } - let path = sharing.path; + let sharingPath = sharing.path; if (sharing.includeSubfolders === true) { - path += '*'; + sharingPath += '*'; } - return {name: 'Guest', role: UserRoles.LimitedGuest, permissions: [path]}; + return { + name: 'Guest', + role: UserRoles.LimitedGuest, + permissions: [sharingPath], + usedSharingKey: sharing.sharingKey + }; } return null; } - public static logout(req: Request, res: Response, next: NextFunction) { - delete req.session.user; - delete req.session.rememberMe; - return next(); - } - } diff --git a/backend/model/Localizations.ts b/backend/model/Localizations.ts index 999bc518..bfcd3291 100644 --- a/backend/model/Localizations.ts +++ b/backend/model/Localizations.ts @@ -11,7 +11,7 @@ export class Localizations { public static init() { const notLanguage = ['assets']; const dirCont = fs.readdirSync(ProjectPath.FrontendFolder) - .filter(f => fs.statSync(path.resolve(ProjectPath.FrontendFolder, f)).isDirectory()); + .filter(f => fs.statSync(path.join(ProjectPath.FrontendFolder, f)).isDirectory()); Config.Client.languages = dirCont.filter(d => notLanguage.indexOf(d) === -1); Config.Client.languages.push('en'); } diff --git a/backend/model/sql/SearchManager.ts b/backend/model/sql/SearchManager.ts index 12d76500..53a9f9a5 100644 --- a/backend/model/sql/SearchManager.ts +++ b/backend/model/sql/SearchManager.ts @@ -257,7 +257,6 @@ export class SearchManager implements ISearchManager { const rawAndEntities = await query.orderBy('media.id').getRawAndEntities(); const media: MediaEntity[] = rawAndEntities.entities; - // console.log(rawAndEntities.raw); let rawIndex = 0; for (let i = 0; i < media.length; i++) { if (rawAndEntities.raw[rawIndex].faces_id === null || diff --git a/backend/model/threading/DiskMangerWorker.ts b/backend/model/threading/DiskMangerWorker.ts index 52000590..0641fad0 100644 --- a/backend/model/threading/DiskMangerWorker.ts +++ b/backend/model/threading/DiskMangerWorker.ts @@ -69,7 +69,7 @@ export class DiskMangerWorker { try { for (let i = 0; i < list.length; i++) { const file = list[i]; - const fullFilePath = path.normalize(path.resolve(absoluteDirectoryName, file)); + const fullFilePath = path.normalize(path.join(absoluteDirectoryName, file)); if (fs.statSync(fullFilePath).isDirectory()) { if (photosOnly === true) { continue; diff --git a/backend/routes/GalleryRouter.ts b/backend/routes/GalleryRouter.ts index 0c3cb8d6..8b020b49 100644 --- a/backend/routes/GalleryRouter.ts +++ b/backend/routes/GalleryRouter.ts @@ -1,5 +1,5 @@ import {AuthenticationMWs} from '../middlewares/user/AuthenticationMWs'; -import {Express, NextFunction, Request, Response} from 'express'; +import {Express} from 'express'; import {GalleryMWs} from '../middlewares/GalleryMWs'; import {RenderingMWs} from '../middlewares/RenderingMWs'; import {ThumbnailGeneratorMWs} from '../middlewares/thumbnail/ThumbnailGeneratorMWs'; @@ -28,7 +28,8 @@ export class GalleryRouter { private static addDirectoryList(app: Express) { app.get(['/api/gallery/content/:directory(*)', '/api/gallery/', '/api/gallery//'], AuthenticationMWs.authenticate, - AuthenticationMWs.authoriseDirectory, + AuthenticationMWs.normalizePathParam('directory'), + AuthenticationMWs.authorisePath('directory', true), VersionMWs.injectGalleryVersion, GalleryMWs.listDirectory, ThumbnailGeneratorMWs.addThumbnailInformation, @@ -41,7 +42,8 @@ export class GalleryRouter { private static addGetImage(app: Express) { app.get(['/api/gallery/content/:mediaPath(*\.(jpg|jpeg|jpe|webp|png|gif|svg))'], AuthenticationMWs.authenticate, - // TODO: authorize path + AuthenticationMWs.normalizePathParam('mediaPath'), + AuthenticationMWs.authorisePath('mediaPath', false), GalleryMWs.loadFile, RenderingMWs.renderFile ); @@ -50,7 +52,8 @@ export class GalleryRouter { private static addGetVideo(app: Express) { app.get(['/api/gallery/content/:mediaPath(*\.(mp4|ogg|ogv|webm))'], AuthenticationMWs.authenticate, - // TODO: authorize path + AuthenticationMWs.normalizePathParam('mediaPath'), + AuthenticationMWs.authorisePath('mediaPath', false), GalleryMWs.loadFile, RenderingMWs.renderFile ); @@ -59,7 +62,8 @@ export class GalleryRouter { private static addGetMetaFile(app: Express) { app.get(['/api/gallery/content/:mediaPath(*\.(gpx))'], AuthenticationMWs.authenticate, - // TODO: authorize path + AuthenticationMWs.normalizePathParam('mediaPath'), + AuthenticationMWs.authorisePath('mediaPath', false), GalleryMWs.loadFile, RenderingMWs.renderFile ); @@ -68,8 +72,8 @@ export class GalleryRouter { private static addRandom(app: Express) { app.get(['/api/gallery/random'], AuthenticationMWs.authenticate, + AuthenticationMWs.authorise(UserRoles.Guest), VersionMWs.injectGalleryVersion, - // TODO: authorize path GalleryMWs.getRandomImage, GalleryMWs.loadFile, RenderingMWs.renderFile @@ -79,7 +83,8 @@ export class GalleryRouter { private static addGetImageThumbnail(app: Express) { app.get('/api/gallery/content/:mediaPath(*\.(jpg|jpeg|jpe|webp|png|gif|svg))/thumbnail/:size?', AuthenticationMWs.authenticate, - // TODO: authorize path + AuthenticationMWs.normalizePathParam('mediaPath'), + AuthenticationMWs.authorisePath('mediaPath', false), GalleryMWs.loadFile, ThumbnailGeneratorMWs.generateThumbnailFactory(ThumbnailSourceType.Image), RenderingMWs.renderFile @@ -89,7 +94,8 @@ export class GalleryRouter { private static addGetVideoThumbnail(app: Express) { app.get('/api/gallery/content/:mediaPath(*\.(mp4|ogg|ogv|webm))/thumbnail/:size?', AuthenticationMWs.authenticate, - // TODO: authorize path + AuthenticationMWs.normalizePathParam('mediaPath'), + AuthenticationMWs.authorisePath('mediaPath', false), GalleryMWs.loadFile, ThumbnailGeneratorMWs.generateThumbnailFactory(ThumbnailSourceType.Video), RenderingMWs.renderFile @@ -100,7 +106,8 @@ export class GalleryRouter { private static addGetVideoIcon(app: Express) { app.get('/api/gallery/content/:mediaPath(*\.(mp4|ogg|ogv|webm))/icon', AuthenticationMWs.authenticate, - // TODO: authorize path + AuthenticationMWs.normalizePathParam('mediaPath'), + AuthenticationMWs.authorisePath('mediaPath', false), GalleryMWs.loadFile, ThumbnailGeneratorMWs.generateIconFactory(ThumbnailSourceType.Video), RenderingMWs.renderFile @@ -110,7 +117,8 @@ export class GalleryRouter { private static addGetImageIcon(app: Express) { app.get('/api/gallery/content/:mediaPath(*\.(jpg|jpeg|jpe|webp|png|gif|svg))/icon', AuthenticationMWs.authenticate, - // TODO: authorize path + AuthenticationMWs.normalizePathParam('mediaPath'), + AuthenticationMWs.authorisePath('mediaPath', false), GalleryMWs.loadFile, ThumbnailGeneratorMWs.generateIconFactory(ThumbnailSourceType.Image), RenderingMWs.renderFile diff --git a/backend/routes/PublicRouter.ts b/backend/routes/PublicRouter.ts index cb46c1c3..7b1f4c2a 100644 --- a/backend/routes/PublicRouter.ts +++ b/backend/routes/PublicRouter.ts @@ -44,7 +44,7 @@ export class PublicRouter { }; const renderIndex = (req: Request, res: Response, next: Function) => { - ejs.renderFile(path.resolve(ProjectPath.FrontendFolder, req['localePath'], 'index.html'), + ejs.renderFile(path.join(ProjectPath.FrontendFolder, req['localePath'], 'index.html'), res.tpl, (err, str) => { if (err) { return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, err.message)); @@ -79,7 +79,7 @@ export class PublicRouter { }); - app.get(['/', '/login', '/gallery*', '/share*', '/admin', '/duplicates', '/faces', '/search*'], + app.get(['/', '/login', '/gallery*', '/share*', '/admin', '/duplicates', '/faces', '/search*'], AuthenticationMWs.tryAuthenticate, setLocale, renderIndex @@ -90,28 +90,26 @@ export class PublicRouter { ); }); + const renderFile = (subDir: string = '') => { + return (req: Request, res: Response) => { + const file = path.join(ProjectPath.FrontendFolder, req['localePath'], subDir, req.params.file); + fs.exists(file, (exists: boolean) => { + if (!exists) { + return res.sendStatus(404); + } + res.sendFile(file); + }); + }; + }; + app.get('/assets/:file(*)', setLocale, - (req: Request, res: Response) => { - const file = path.resolve(ProjectPath.FrontendFolder, req['localePath'], 'assets', req.params.file); - fs.exists(file, (exists: boolean) => { - if (!exists) { - return res.sendStatus(404); - } - res.sendFile(file); - }); - }); + AuthenticationMWs.normalizePathParam('file'), + renderFile('assets')); app.get('/:file', setLocale, - (req: Request, res: Response) => { - const file = path.resolve(ProjectPath.FrontendFolder, req['localePath'], req.params.file); - fs.exists(file, (exists: boolean) => { - if (!exists) { - return res.sendStatus(404); - } - res.sendFile(file); - }); - }); + AuthenticationMWs.normalizePathParam('file'), + renderFile()); } } diff --git a/common/entities/UserDTO.ts b/common/entities/UserDTO.ts index b20944c1..549f86dc 100644 --- a/common/entities/UserDTO.ts +++ b/common/entities/UserDTO.ts @@ -15,25 +15,26 @@ export interface UserDTO { name: string; password: string; role: UserRoles; + usedSharingKey?: string; permissions: string[]; // user can only see these permissions. if ends with *, its recursive } export module UserDTO { - export const isPathAvailable = (path: string, permissions: string[]): boolean => { - if (permissions == null || permissions.length === 0 || permissions[0] === '/*') { + export const isDirectoryPathAvailable = (path: string, permissions: string[], separator = '/'): boolean => { + if (permissions == null || permissions.length === 0 || permissions[0] === separator + '*') { return true; } for (let i = 0; i < permissions.length; i++) { let permission = permissions[i]; if (permission[permission.length - 1] === '*') { permission = permission.slice(0, -1); - if (path.startsWith(permission)) { + if (path.startsWith(permission) && (!path[permission.length] || path[permission.length] === separator)) { return true; } } else if (path === permission) { return true; - } else if (path === '.' && permission === '/') { + } else if (path === '.' && permission === separator) { return true; } @@ -42,7 +43,6 @@ export module UserDTO { }; export const isDirectoryAvailable = (directory: DirectoryDTO, permissions: string[]): boolean => { - - return isPathAvailable(Utils.concatUrls(directory.path, directory.name), permissions); + return isDirectoryPathAvailable(Utils.concatUrls(directory.path, directory.name), permissions); }; } diff --git a/frontend/app/gallery/gallery.component.ts b/frontend/app/gallery/gallery.component.ts index 2156193d..4a0546f6 100644 --- a/frontend/app/gallery/gallery.component.ts +++ b/frontend/app/gallery/gallery.component.ts @@ -53,7 +53,6 @@ export class GalleryComponent implements OnInit, OnDestroy { private rndService: SeededRandomService) { this.mapEnabled = Config.Client.Map.enabled; this.SearchTypes = SearchTypes; - PageHelper.showScrollY(); } diff --git a/frontend/app/gallery/gallery.service.ts b/frontend/app/gallery/gallery.service.ts index 5ff59800..1ed773c7 100644 --- a/frontend/app/gallery/gallery.service.ts +++ b/frontend/app/gallery/gallery.service.ts @@ -18,6 +18,9 @@ export class GalleryService { public content: BehaviorSubject; public sorting: BehaviorSubject; + lastRequest: { directory: string } = { + directory: null + }; private lastDirectory: DirectoryDTO; private searchId: any; private ongoingSearch: { @@ -38,10 +41,6 @@ export class GalleryService { this.sorting = new BehaviorSubject(Config.Client.Other.defaultPhotoSortingMethod); } - lastRequest: { directory: string } = { - directory: null - }; - setSorting(sorting: SortingMethods): void { this.sorting.next(sorting); if (this.content.value.directory) { diff --git a/frontend/app/gallery/navigator/navigator.gallery.component.ts b/frontend/app/gallery/navigator/navigator.gallery.component.ts index 93e3f64b..c3f938bd 100644 --- a/frontend/app/gallery/navigator/navigator.gallery.component.ts +++ b/frontend/app/gallery/navigator/navigator.gallery.component.ts @@ -70,7 +70,7 @@ export class GalleryNavigatorComponent implements OnChanges { if (dirs.length === 0) { arr.push({name: this.RootFolderName, route: null}); } else { - arr.push({name: this.RootFolderName, route: UserDTO.isPathAvailable('/', user.permissions) ? '/' : null}); + arr.push({name: this.RootFolderName, route: UserDTO.isDirectoryPathAvailable('/', user.permissions) ? '/' : null}); } // create rest navigation @@ -79,7 +79,7 @@ export class GalleryNavigatorComponent implements OnChanges { if (dirs.length - 1 === index) { arr.push({name: name, route: null}); } else { - arr.push({name: name, route: UserDTO.isPathAvailable(route, user.permissions) ? route : null}); + arr.push({name: name, route: UserDTO.isDirectoryPathAvailable(route, user.permissions) ? route : null}); } }); diff --git a/frontend/app/gallery/share.service.ts b/frontend/app/gallery/share.service.ts index 0a334657..fd0bd190 100644 --- a/frontend/app/gallery/share.service.ts +++ b/frontend/app/gallery/share.service.ts @@ -2,8 +2,9 @@ import {Injectable} from '@angular/core'; import {NetworkService} from '../model/network/network.service'; import {CreateSharingDTO, SharingDTO} from '../../../common/entities/SharingDTO'; import {Router, RoutesRecognized} from '@angular/router'; -import {BehaviorSubject} from 'rxjs'; +import {BehaviorSubject, Observable} from 'rxjs'; import {QueryParams} from '../../../common/QueryParams'; +import {UserDTO} from '../../../common/entities/UserDTO'; @Injectable() export class ShareService { @@ -17,7 +18,8 @@ export class ShareService { private resolve: () => void; - constructor(private _networkService: NetworkService, private router: Router) { + constructor(private networkService: NetworkService, + private router: Router) { this.sharing = new BehaviorSubject(null); this.ReadyPR = new Promise((resolve: () => void) => { if (this.inited === true) { @@ -30,28 +32,52 @@ export class ShareService { if (val instanceof RoutesRecognized) { this.param = val.state.root.firstChild.params[QueryParams.gallery.sharingKey_long] || null; this.queryParam = val.state.root.firstChild.queryParams[QueryParams.gallery.sharingKey_short] || null; - const changed = this.sharingKey !== this.param || this.queryParam; + + const changed = this.sharingKey !== (this.param || this.queryParam); if (changed) { - this.sharingKey = this.param || this.queryParam; + this.sharingKey = this.param || this.queryParam || this.sharingKey; this.getSharing(); } if (this.resolve) { this.resolve(); + this.resolve = null; this.inited = true; } } }); + + } + + public setUserObs(userOB: Observable) { + + userOB.subscribe((user) => { + console.log(user); + if (user && !!user.usedSharingKey) { + if (user.usedSharingKey !== this.sharingKey) { + this.sharingKey = user.usedSharingKey; + this.getSharing(); + } + if (this.resolve) { + this.resolve(); + this.resolve = null; + this.inited = true; + } + } + }); } public wait(): Promise { + if (this.inited) { + return Promise.resolve(); + } return this.ReadyPR; } public createSharing(dir: string, includeSubfolders: boolean, valid: number): Promise { - return this._networkService.postJson('/share/' + dir, { + return this.networkService.postJson('/share/' + dir, { createSharing: { includeSubfolders: includeSubfolders, valid: valid @@ -60,7 +86,7 @@ export class ShareService { } public updateSharing(dir: string, sharingId: number, includeSubfolders: boolean, password: string, valid: number): Promise { - return this._networkService.putJson('/share/' + dir, { + return this.networkService.putJson('/share/' + dir, { updateSharing: { id: sharingId, includeSubfolders: includeSubfolders, @@ -80,7 +106,7 @@ export class ShareService { } public async getSharing(): Promise { - const sharing = await this._networkService.getJson('/share/' + this.getSharingKey()); + const sharing = await this.networkService.getJson('/share/' + this.getSharingKey()); this.sharing.next(sharing); return sharing; } diff --git a/frontend/app/model/navigation.service.ts b/frontend/app/model/navigation.service.ts index 949c9bf0..8d89cddb 100644 --- a/frontend/app/model/navigation.service.ts +++ b/frontend/app/model/navigation.service.ts @@ -28,7 +28,7 @@ export class NavigationService { public async toGallery() { await this._shareService.wait(); if (this._shareService.isSharing()) { - return this._router.navigate(['gallery', ''], {queryParams: {sk: this._shareService.getSharingKey()}}); + return this._router.navigate(['share', this._shareService.getSharingKey()]); } else { return this._router.navigate(['gallery', '']); } diff --git a/frontend/app/model/network/authentication.service.ts b/frontend/app/model/network/authentication.service.ts index 16515b1d..3b5df278 100644 --- a/frontend/app/model/network/authentication.service.ts +++ b/frontend/app/model/network/authentication.service.ts @@ -8,6 +8,7 @@ import {Config} from '../../../../common/config/public/Config'; import {NetworkService} from './network.service'; import {ErrorCodes, ErrorDTO} from '../../../../common/entities/Error'; import {CookieNames} from '../../../../common/CookieNames'; +import {ShareService} from '../../gallery/share.service'; declare module ServerInject { export let user: UserDTO; @@ -19,9 +20,10 @@ export class AuthenticationService { public user: BehaviorSubject; constructor(private _userService: UserService, - private _networkService: NetworkService) { + private _networkService: NetworkService, + private shareSerice: ShareService) { this.user = new BehaviorSubject(null); - + this.shareSerice.setUserObs(this.user); // picking up session.. if (this.isAuthenticated() === false && Cookie.get(CookieNames.session) != null) { if (typeof ServerInject !== 'undefined' && typeof ServerInject.user !== 'undefined') { @@ -48,6 +50,34 @@ export class AuthenticationService { } + public async login(credential: LoginCredential): Promise { + const user = await this._userService.login(credential); + this.user.next(user); + return user; + } + + public async shareLogin(password: string): Promise { + const user = await this._userService.shareLogin(password); + this.user.next(user); + return user; + } + + public isAuthenticated(): boolean { + if (Config.Client.authenticationRequired === false) { + return true; + } + return !!this.user.value; + } + + public isAuthorized(role: UserRoles) { + return this.user.value && this.user.value.role >= role; + } + + public async logout() { + await this._userService.logout(); + this.user.next(null); + } + private async getSessionUser(): Promise { try { this.user.next(await this._userService.getSessionUser()); @@ -58,35 +88,4 @@ export class AuthenticationService { } - public async login(credential: LoginCredential): Promise { - const user = await this._userService.login(credential); - this.user.next(user); - return user; - } - - - public async shareLogin(password: string): Promise { - const user = await this._userService.shareLogin(password); - this.user.next(user); - return user; - } - - - public isAuthenticated(): boolean { - if (Config.Client.authenticationRequired === false) { - return true; - } - return !!(this.user.value && this.user.value != null); - } - - public isAuthorized(role: UserRoles) { - return this.user.value && this.user.value.role >= role; - } - - public logout() { - this._userService.logout(); - this.user.next(null); - } - - } diff --git a/frontend/app/model/network/user.service.ts b/frontend/app/model/network/user.service.ts index 683653f3..4a5cdcec 100644 --- a/frontend/app/model/network/user.service.ts +++ b/frontend/app/model/network/user.service.ts @@ -29,7 +29,7 @@ export class UserService { await this._shareService.wait(); if (Config.Client.Sharing.enabled === true) { if (this._shareService.isSharing()) { - return this._networkService.getJson('/user/login?sk=' + this._shareService.getSharingKey()); + return this._networkService.getJson('/user/login', {sk: this._shareService.getSharingKey()}); } } return this._networkService.getJson('/user/login'); diff --git a/package-lock.json b/package-lock.json index 4791fd55..4ab37d63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14438,6 +14438,26 @@ } } }, + "tslint-no-unused-expression-chai": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/tslint-no-unused-expression-chai/-/tslint-no-unused-expression-chai-0.1.4.tgz", + "integrity": "sha512-frEWKNTcq7VsaWKgUxMDOB2N/cmQadVkUtUGIut+2K4nv/uFXPfgJyPjuNC/cHyfUVqIkHMAvHOCL+d/McU3nQ==", + "dev": true, + "requires": { + "tsutils": "^3.0.0" + }, + "dependencies": { + "tsutils": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.8.0.tgz", + "integrity": "sha512-XQdPhgcoTbCD8baXC38PQ0vpTZ8T3YrE+vR66YIj/xvDt1//8iAhafpIT/4DmvzzC1QFapEImERu48Pa01dIUA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, "tsutils": { "version": "2.29.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", diff --git a/package.json b/package.json index 34734fae..50c66bd3 100644 --- a/package.json +++ b/package.json @@ -114,13 +114,13 @@ "run-sequence": "2.2.1", "rxjs": "6.4.0", "rxjs-compat": "6.4.0", + "terser": "3.16.1", "ts-helpers": "1.1.2", "ts-node": "8.0.2", "tslint": "5.12.1", "typescript": "3.2.4", "xlf-google-translate": "1.0.0-beta.13", - "zone.js": "0.8.29", - "terser": "3.16.1" + "zone.js": "0.8.29" }, "//": [ "TODO: remove terser version lock once webpack is fixed" diff --git a/test/backend/SQLTestHelper.ts b/test/backend/SQLTestHelper.ts index 147ed1a8..54b75709 100644 --- a/test/backend/SQLTestHelper.ts +++ b/test/backend/SQLTestHelper.ts @@ -18,8 +18,8 @@ export class SQLTestHelper { dbPath: string; constructor(public dbType: DatabaseType) { - this.tempDir = path.resolve(__dirname, './tmp'); - this.dbPath = path.resolve(__dirname, './tmp', 'test.db'); + this.tempDir = path.join(__dirname, './tmp'); + this.dbPath = path.join(__dirname, './tmp', 'test.db'); } diff --git a/test/backend/unit/middlewares/user/AuthenticationMWs.ts b/test/backend/unit/middlewares/user/AuthenticationMWs.ts index daa898d9..e45cdb14 100644 --- a/test/backend/unit/middlewares/user/AuthenticationMWs.ts +++ b/test/backend/unit/middlewares/user/AuthenticationMWs.ts @@ -6,8 +6,13 @@ import {ObjectManagers} from '../../../../../backend/model/ObjectManagers'; import {UserManager} from '../../../../../backend/model/memory/UserManager'; import {Config} from '../../../../../common/config/private/Config'; import {IUserManager} from '../../../../../backend/model/interfaces/IUserManager'; +import * as path from 'path'; +declare const describe: any; +declare const it: any; +declare const beforeEach: any; + describe('Authentication middleware', () => { beforeEach(() => { @@ -15,7 +20,7 @@ describe('Authentication middleware', () => { }); describe('authenticate', () => { - it('should call next on authenticated', (done) => { + it('should call next on authenticated', (done: () => void) => { const req: any = { session: { user: 'A user' @@ -24,7 +29,7 @@ describe('Authentication middleware', () => { query: {}, params: {} }; - const next: any = (err: ErrorDTO) => { + const next = (err: ErrorDTO) => { expect(err).to.be.undefined; done(); }; @@ -32,7 +37,7 @@ describe('Authentication middleware', () => { }); - it('should call next with error on not authenticated', (done) => { + it('should call next with error on not authenticated', (done: () => void) => { const req: any = { session: {}, sessionOptions: {}, @@ -40,8 +45,7 @@ describe('Authentication middleware', () => { params: {} }; Config.Client.authenticationRequired = true; - const res: any = {}; - const next: any = (err: ErrorDTO) => { + const next = (err: ErrorDTO) => { expect(err).not.to.be.undefined; expect(err.code).to.be.eql(ErrorCodes.NOT_AUTHENTICATED); done(); @@ -51,15 +55,69 @@ describe('Authentication middleware', () => { }); }); + + describe('authorisePath', () => { + + const req = { + session: { + user: {permissions: null} + }, + sessionOptions: {}, + query: {}, + params: { + path: '/test' + } + }; + + + const authoriseDirPath = AuthenticationMWs.authorisePath('path', true); + const test = (relativePath: string): Promise => { + return new Promise((resolve) => { + req.params.path = path.normalize(relativePath); + authoriseDirPath(req, {sendStatus: resolve}, () => { + resolve('ok'); + }); + resolve(); + }); + }; + + it('should catch unauthorized path usage', async () => { + req.session.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.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.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) => { + it('should call next with error on authenticated', (done: () => void) => { const req: any = { session: {}, sessionOptions: {}, }; const res: any = {}; - const next: any = (err:ErrorDTO) => { + const next: any = (err: ErrorDTO) => { expect(err).to.be.undefined; done(); }; @@ -68,15 +126,14 @@ describe('Authentication middleware', () => { }); - it('should call next error on authenticated', (done) => { + it('should call next error on authenticated', (done: () => void) => { const req: any = { session: { user: 'A user' }, sessionOptions: {}, }; - const res: any = {}; - const next: any = (err: ErrorDTO) => { + const next = (err: ErrorDTO) => { expect(err).not.to.be.undefined; expect(err.code).to.be.eql(ErrorCodes.ALREADY_AUTHENTICATED); done(); @@ -87,7 +144,7 @@ describe('Authentication middleware', () => { }); describe('authorise', () => { - it('should call next on authorised', (done) => { + it('should call next on authorised', (done: () => void) => { const req: any = { session: { user: { @@ -96,7 +153,7 @@ describe('Authentication middleware', () => { }, sessionOptions: {} }; - const next: any = (err:ErrorDTO) => { + const next = (err: ErrorDTO) => { expect(err).to.be.undefined; done(); }; @@ -104,7 +161,7 @@ describe('Authentication middleware', () => { }); - it('should call next with error on not authorised', (done) => { + it('should call next with error on not authorised', (done: () => void) => { const req: any = { session: { user: { @@ -113,7 +170,7 @@ describe('Authentication middleware', () => { }, sessionOptions: {} }; - const next: any = (err: ErrorDTO) => { + const next = (err: ErrorDTO) => { expect(err).not.to.be.undefined; expect(err.code).to.be.eql(ErrorCodes.NOT_AUTHORISED); done(); @@ -129,12 +186,12 @@ describe('Authentication middleware', () => { }); describe('should call input ErrorDTO next on missing...', () => { - it('body', (done) => { + it('body', (done: () => void) => { const req: any = { query: {}, params: {} }; - const next: any = (err: ErrorDTO) => { + const next = (err: ErrorDTO) => { expect(err).not.to.be.undefined; expect(err.code).to.be.eql(ErrorCodes.INPUT_ERROR); done(); @@ -143,13 +200,13 @@ describe('Authentication middleware', () => { }); - it('loginCredential', (done) => { + it('loginCredential', (done: () => void) => { const req: any = { body: {}, query: {}, params: {} }; - const next: any = (err: ErrorDTO) => { + const next = (err: ErrorDTO) => { expect(err).not.to.be.undefined; expect(err.code).to.be.eql(ErrorCodes.INPUT_ERROR); done(); @@ -160,13 +217,13 @@ describe('Authentication middleware', () => { }); - it('loginCredential content', (done) => { + it('loginCredential content', (done: () => void) => { const req: any = { body: {loginCredential: {}}, query: {}, params: {} }; - const next: any = (err: ErrorDTO) => { + const next = (err: ErrorDTO) => { expect(err).not.to.be.undefined; expect(err.code).to.be.eql(ErrorCodes.INPUT_ERROR); done(); @@ -177,7 +234,7 @@ describe('Authentication middleware', () => { }); }); - it('should call next with error on not finding user', (done) => { + it('should call next with error on not finding user', (done: () => void) => { const req: any = { body: { loginCredential: { @@ -188,7 +245,7 @@ describe('Authentication middleware', () => { query: {}, params: {} }; - const next: any = (err: ErrorDTO) => { + const next = (err: ErrorDTO) => { expect(err).not.to.be.undefined; expect(err.code).to.be.eql(ErrorCodes.CREDENTIAL_NOT_FOUND); done(); @@ -203,7 +260,7 @@ describe('Authentication middleware', () => { }); - it('should call next with user on the session on finding user', (done) => { + it('should call next with user on the session on finding user', (done: () => void) => { const req: any = { session: {}, body: { @@ -215,7 +272,7 @@ describe('Authentication middleware', () => { query: {}, params: {} }; - const next: any = (err: ErrorDTO) => { + const next = (err: ErrorDTO) => { expect(err).to.be.undefined; expect(req.session.user).to.be.eql('test user'); done(); @@ -233,7 +290,7 @@ describe('Authentication middleware', () => { describe('logout', () => { - it('should call next on logout', (done) => { + it('should call next on logout', (done: () => void) => { const req: any = { session: { user: { @@ -241,7 +298,7 @@ describe('Authentication middleware', () => { } } }; - const next: any = (err:ErrorDTO) => { + const next: any = (err: ErrorDTO) => { expect(err).to.be.undefined; expect(req.session.user).to.be.undefined; done(); diff --git a/test/common/unit/UserDTO.ts b/test/common/unit/UserDTO.ts index b20e5bc2..11463516 100644 --- a/test/common/unit/UserDTO.ts +++ b/test/common/unit/UserDTO.ts @@ -5,14 +5,14 @@ describe('UserDTO', () => { it('should check available path', () => { - expect(UserDTO.isPathAvailable('/', ['/'])).to.be.equals(true); - expect(UserDTO.isPathAvailable('/', ['/subfolder', '/'])).to.be.equals(true); - expect(UserDTO.isPathAvailable('/abc', ['/subfolder', '/'])).to.be.equals(false); - expect(UserDTO.isPathAvailable('/abc', ['/subfolder', '/*'])).to.be.equals(true); - expect(UserDTO.isPathAvailable('/abc', ['/subfolder'])).to.be.equals(false); - expect(UserDTO.isPathAvailable('/abc/two', ['/subfolder'])).to.be.equals(false); - expect(UserDTO.isPathAvailable('/abc/two', ['/'])).to.be.equals(false); - expect(UserDTO.isPathAvailable('/abc/two', ['/*'])).to.be.equals(true); + expect(UserDTO.isDirectoryPathAvailable('/', ['/'])).to.be.equals(true); + expect(UserDTO.isDirectoryPathAvailable('/', ['/subfolder', '/'])).to.be.equals(true); + expect(UserDTO.isDirectoryPathAvailable('/abc', ['/subfolder', '/'])).to.be.equals(false); + expect(UserDTO.isDirectoryPathAvailable('/abc', ['/subfolder', '/*'])).to.be.equals(true); + expect(UserDTO.isDirectoryPathAvailable('/abc', ['/subfolder'])).to.be.equals(false); + expect(UserDTO.isDirectoryPathAvailable('/abc/two', ['/subfolder'])).to.be.equals(false); + expect(UserDTO.isDirectoryPathAvailable('/abc/two', ['/'])).to.be.equals(false); + expect(UserDTO.isDirectoryPathAvailable('/abc/two', ['/*'])).to.be.equals(true); }); it('should check directory', () => {