Compare commits

..

4 Commits

Author SHA1 Message Date
standardci 5be7db7788 chore(release): publish new version
- @standardnotes/home-server@1.13.33
 - @standardnotes/syncing-server@1.76.1
2023-08-08 09:29:54 +00:00
Karol Sójko 3bd1547ce3 fix(syncing-server): race condition when adding admin user to newly created shared vault (#688) 2023-08-08 11:02:10 +02:00
standardci a1fe15f7a9 chore(release): publish new version
- @standardnotes/api-gateway@1.70.0
 - @standardnotes/home-server@1.13.32
 - @standardnotes/syncing-server@1.76.0
2023-08-07 16:09:21 +00:00
Karol Sójko 19b8921f28 feat(syncing-server): limit shared vaults creation based on role (#687)
* feat(syncing-server): limit shared vaults creation based on role

* fix: add role names emptyness validation

* fix: roles passing to response locals
2023-08-07 17:35:47 +02:00
23 changed files with 159 additions and 33 deletions
+2 -1
View File
@@ -19,7 +19,8 @@
"publish": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
"postversion": "./scripts/push-tags-one-by-one.sh",
"upgrade:snjs": "yarn workspaces foreach --verbose run upgrade:snjs",
"e2e": "yarn build packages/home-server && PORT=3123 yarn workspace @standardnotes/home-server start"
"e2e": "yarn build packages/home-server && PORT=3123 yarn workspace @standardnotes/home-server start",
"start": "yarn build packages/home-server && yarn workspace @standardnotes/home-server start"
},
"devDependencies": {
"@commitlint/cli": "^17.0.2",
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.70.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.3...@standardnotes/api-gateway@1.70.0) (2023-08-07)
### Features
* **syncing-server:** limit shared vaults creation based on role ([#687](https://github.com/standardnotes/api-gateway/issues/687)) ([19b8921](https://github.com/standardnotes/api-gateway/commit/19b8921f286ff8f88c427e8ddd4512a8d61edb4f))
## [1.69.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.2...@standardnotes/api-gateway@1.69.3) (2023-08-03)
**Note:** Version bump only for package @standardnotes/api-gateway
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.69.3",
"version": "1.70.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -1,5 +1,4 @@
import { CrossServiceTokenData } from '@standardnotes/security'
import { RoleName } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Request, Response } from 'express'
import { BaseMiddleware } from 'inversify-express-utils'
@@ -51,10 +50,6 @@ export abstract class AuthMiddleware extends BaseMiddleware {
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.freeUser =
decodedToken.roles.length === 1 &&
decodedToken.roles.find((role) => role.name === RoleName.NAMES.CoreUser) !== undefined
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({
authorizationHeaderValue: authHeaderValue,
@@ -1,5 +1,4 @@
import { CrossServiceTokenData } from '@standardnotes/security'
import { RoleName } from '@standardnotes/domain-core'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
@@ -60,9 +59,6 @@ export class WebSocketAuthMiddleware extends BaseMiddleware {
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.freeUser =
decodedToken.roles.length === 1 &&
decodedToken.roles.find((role) => role.name === RoleName.NAMES.CoreUser) !== undefined
response.locals.user = decodedToken.user
response.locals.roles = decodedToken.roles
} catch (error) {
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.13.33](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.32...@standardnotes/home-server@1.13.33) (2023-08-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.32](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.31...@standardnotes/home-server@1.13.32) (2023-08-07)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.31](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.30...@standardnotes/home-server@1.13.31) (2023-08-07)
**Note:** Version bump only for package @standardnotes/home-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.13.31",
"version": "1.13.33",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.76.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.76.0...@standardnotes/syncing-server@1.76.1) (2023-08-08)
### Bug Fixes
* **syncing-server:** race condition when adding admin user to newly created shared vault ([#688](https://github.com/standardnotes/syncing-server-js/issues/688)) ([3bd1547](https://github.com/standardnotes/syncing-server-js/commit/3bd1547ce3f599306f3942ce0a46f98cebfd33a4))
# [1.76.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.75.4...@standardnotes/syncing-server@1.76.0) (2023-08-07)
### Features
* **syncing-server:** limit shared vaults creation based on role ([#687](https://github.com/standardnotes/syncing-server-js/issues/687)) ([19b8921](https://github.com/standardnotes/syncing-server-js/commit/19b8921f286ff8f88c427e8ddd4512a8d61edb4f))
## [1.75.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.75.3...@standardnotes/syncing-server@1.75.4) (2023-08-03)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.75.4",
"version": "1.76.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -3,6 +3,7 @@ import { SharedVault } from './SharedVault'
export interface SharedVaultRepositoryInterface {
findByUuid(uuid: Uuid): Promise<SharedVault | null>
countByUserUuid(userUuid: Uuid): Promise<number>
findByUuids(uuids: Uuid[], lastSyncTime?: number): Promise<SharedVault[]>
save(sharedVault: SharedVault): Promise<void>
remove(sharedVault: SharedVault): Promise<void>
@@ -115,4 +115,20 @@ describe('AddUserToSharedVault', () => {
expect(result.isFailed()).toBe(false)
expect(sharedVaultUserRepository.save).toHaveBeenCalled()
})
it('should add a user to a shared vault and skip checking if shared vault exists to avoid race conditions', async () => {
sharedVaultRepository.findByUuid = jest.fn().mockResolvedValueOnce(null)
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: validUuid,
userUuid: validUuid,
permission: 'read',
skipSharedVaultExistenceCheck: true,
})
expect(result.isFailed()).toBe(false)
expect(sharedVaultUserRepository.save).toHaveBeenCalled()
})
})
@@ -20,9 +20,11 @@ export class AddUserToSharedVault implements UseCaseInterface<SharedVaultUser> {
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
if (!sharedVault) {
return Result.fail('Attempting to add a shared vault user to a non-existent shared vault')
if (!dto.skipSharedVaultExistenceCheck) {
const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
if (!sharedVault) {
return Result.fail('Attempting to add a shared vault user to a non-existent shared vault')
}
}
const userUuidOrError = Uuid.create(dto.userUuid)
@@ -2,4 +2,5 @@ export interface AddUserToSharedVaultDTO {
sharedVaultUuid: string
userUuid: string
permission: string
skipSharedVaultExistenceCheck?: boolean
}
@@ -1,5 +1,5 @@
import { TimerInterface } from '@standardnotes/time'
import { Result } from '@standardnotes/domain-core'
import { Result, RoleName } from '@standardnotes/domain-core'
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
import { AddUserToSharedVault } from '../AddUserToSharedVault/AddUserToSharedVault'
@@ -29,12 +29,25 @@ describe('CreateSharedVault', () => {
const result = await useCase.execute({
userUuid: 'invalid-uuid',
userRoleNames: [RoleName.NAMES.ProUser],
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is not a valid uuid: invalid-uuid')
})
it('should return a failure result if the user role names are empty', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
userRoleNames: [],
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Given value is empty: ')
})
it('should return a failure result if the shared vault could not be created', async () => {
const useCase = createUseCase()
@@ -45,6 +58,7 @@ describe('CreateSharedVault', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
userRoleNames: [RoleName.NAMES.ProUser],
})
expect(result.isFailed()).toBe(true)
@@ -60,6 +74,7 @@ describe('CreateSharedVault', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
userRoleNames: [RoleName.NAMES.ProUser],
})
expect(result.isFailed()).toBe(true)
@@ -71,13 +86,43 @@ describe('CreateSharedVault', () => {
await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
userRoleNames: [RoleName.NAMES.ProUser],
})
expect(addUserToSharedVault.execute).toHaveBeenCalledWith({
sharedVaultUuid: expect.any(String),
userUuid: '00000000-0000-0000-0000-000000000000',
permission: 'admin',
skipSharedVaultExistenceCheck: true,
})
expect(sharedVaultRepository.save).toHaveBeenCalled()
})
it('should return a failure result if a plus user has reached the limit of shared vaults', async () => {
const useCase = createUseCase()
sharedVaultRepository.countByUserUuid = jest.fn().mockResolvedValue(3)
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
userRoleNames: [RoleName.NAMES.PlusUser],
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('You have reached the limit of shared vaults for your account.')
})
it('should return a failure result if a core user has reached the limit of shared vaults', async () => {
const useCase = createUseCase()
sharedVaultRepository.countByUserUuid = jest.fn().mockResolvedValue(1)
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
userRoleNames: [RoleName.NAMES.CoreUser],
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('You have reached the limit of shared vaults for your account.')
})
})
@@ -1,4 +1,12 @@
import { Result, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import {
Result,
RoleName,
SharedVaultUserPermission,
Timestamps,
UseCaseInterface,
Uuid,
Validator,
} from '@standardnotes/domain-core'
import { CreateSharedVaultResult } from './CreateSharedVaultResult'
import { CreateSharedVaultDTO } from './CreateSharedVaultDTO'
import { TimerInterface } from '@standardnotes/time'
@@ -22,6 +30,19 @@ export class CreateSharedVault implements UseCaseInterface<CreateSharedVaultResu
}
const userUuid = userUuidOrError.getValue()
const userRoleNamesValidationResult = Validator.isNotEmpty(dto.userRoleNames)
if (userRoleNamesValidationResult.isFailed()) {
return Result.fail(userRoleNamesValidationResult.getError())
}
const userSharedVaultLimit = this.getUserSharedVaultLimit(dto.userRoleNames)
if (userSharedVaultLimit !== undefined) {
const userSharedVaultCount = await this.sharedVaultRepository.countByUserUuid(userUuid)
if (userSharedVaultCount >= userSharedVaultLimit) {
return Result.fail('You have reached the limit of shared vaults for your account.')
}
}
const timestamps = Timestamps.create(
this.timer.getTimestampInMicroseconds(),
this.timer.getTimestampInMicroseconds(),
@@ -43,7 +64,8 @@ export class CreateSharedVault implements UseCaseInterface<CreateSharedVaultResu
const sharedVaultUserOrError = await this.addUserToSharedVault.execute({
sharedVaultUuid: sharedVault.id.toString(),
userUuid: dto.userUuid,
permission: 'admin',
permission: SharedVaultUserPermission.PERMISSIONS.Admin,
skipSharedVaultExistenceCheck: true,
})
if (sharedVaultUserOrError.isFailed()) {
return Result.fail(sharedVaultUserOrError.getError())
@@ -52,4 +74,16 @@ export class CreateSharedVault implements UseCaseInterface<CreateSharedVaultResu
return Result.ok({ sharedVault, sharedVaultUser })
}
private getUserSharedVaultLimit(userRoleNames: string[]): number | undefined {
if (userRoleNames.includes(RoleName.NAMES.ProUser)) {
return undefined
}
if (userRoleNames.includes(RoleName.NAMES.PlusUser)) {
return 3
}
return 1
}
}
@@ -1,3 +1,4 @@
export interface CreateSharedVaultDTO {
userUuid: string
userRoleNames: string[]
}
@@ -45,7 +45,6 @@ describe('CheckIntegrity', () => {
it('should return an empty result if there are no integrity mismatches', async () => {
const result = await createUseCase().execute({
userUuid: '1-2-3',
freeUser: false,
integrityPayloads: [
{
uuid: '1-2-3',
@@ -71,7 +70,6 @@ describe('CheckIntegrity', () => {
it('should return a mismatch item that has a different update at timemstap', async () => {
const result = await createUseCase().execute({
userUuid: '1-2-3',
freeUser: false,
integrityPayloads: [
{
uuid: '1-2-3',
@@ -102,7 +100,6 @@ describe('CheckIntegrity', () => {
it('should return a mismatch item that is missing on the client side', async () => {
const result = await createUseCase().execute({
userUuid: '1-2-3',
freeUser: false,
integrityPayloads: [
{
uuid: '1-2-3',
@@ -3,5 +3,4 @@ import { IntegrityPayload } from '@standardnotes/responses'
export type CheckIntegrityDTO = {
userUuid: string
integrityPayloads: IntegrityPayload[]
freeUser: boolean
}
@@ -91,7 +91,6 @@ export class BaseItemsController extends BaseHttpController {
const result = await this.checkIntegrity.execute({
userUuid: response.locals.user.uuid,
integrityPayloads,
freeUser: response.locals.freeUser,
})
if (result.isFailed()) {
@@ -2,6 +2,7 @@ import { Request, Response } from 'express'
import { BaseHttpController, results } from 'inversify-express-utils'
import { HttpStatusCode } from '@standardnotes/responses'
import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
import { Role } from '@standardnotes/security'
import { GetSharedVaults } from '../../../Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults'
import { SharedVault } from '../../../Domain/SharedVault/SharedVault'
@@ -59,6 +60,7 @@ export class BaseSharedVaultsController extends BaseHttpController {
async createSharedVault(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.createSharedVaultUseCase.execute({
userUuid: response.locals.user.uuid,
userRoleNames: response.locals.roles.map((role: Role) => role.name),
})
if (result.isFailed()) {
@@ -61,10 +61,12 @@ describe('InversifyExpressAuthMiddleware', () => {
await createMiddleware().handler(request, response, next)
expect(response.locals.user).toEqual({ uuid: '123' })
expect(response.locals.roleNames).toEqual(['CORE_USER', 'PRO_USER'])
expect(response.locals.roles).toEqual([
{ uuid: '1-2-3', name: RoleName.NAMES.CoreUser },
{ uuid: '2-3-4', name: RoleName.NAMES.ProUser },
])
expect(response.locals.session).toEqual({ uuid: '234' })
expect(response.locals.readOnlyAccess).toBeFalsy()
expect(response.locals.freeUser).toEqual(false)
expect(next).toHaveBeenCalled()
})
@@ -90,8 +92,6 @@ describe('InversifyExpressAuthMiddleware', () => {
await createMiddleware().handler(request, response, next)
expect(response.locals.freeUser).toEqual(true)
expect(next).toHaveBeenCalled()
})
@@ -124,7 +124,10 @@ describe('InversifyExpressAuthMiddleware', () => {
await createMiddleware().handler(request, response, next)
expect(response.locals.user).toEqual({ uuid: '123' })
expect(response.locals.roleNames).toEqual(['CORE_USER', 'PRO_USER'])
expect(response.locals.roles).toEqual([
{ uuid: '1-2-3', name: RoleName.NAMES.CoreUser },
{ uuid: '2-3-4', name: RoleName.NAMES.ProUser },
])
expect(response.locals.session).toEqual({ uuid: '234', readonly_access: true })
expect(response.locals.readOnlyAccess).toBeTruthy()
@@ -3,7 +3,6 @@ import { BaseMiddleware } from 'inversify-express-utils'
import { verify } from 'jsonwebtoken'
import { CrossServiceTokenData } from '@standardnotes/security'
import * as winston from 'winston'
import { RoleName } from '@standardnotes/domain-core'
export class InversifyExpressAuthMiddleware extends BaseMiddleware {
constructor(private authJWTSecret: string, private logger: winston.Logger) {
@@ -23,9 +22,7 @@ export class InversifyExpressAuthMiddleware extends BaseMiddleware {
const decodedToken = <CrossServiceTokenData>verify(authToken, this.authJWTSecret, { algorithms: ['HS256'] })
response.locals.user = decodedToken.user
response.locals.roleNames = decodedToken.roles.map((role) => role.name)
response.locals.freeUser =
response.locals.roleNames.length === 1 && response.locals.roleNames[0] === RoleName.NAMES.CoreUser
response.locals.roles = decodedToken.roles
response.locals.session = decodedToken.session
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
@@ -11,6 +11,17 @@ export class TypeORMSharedVaultRepository implements SharedVaultRepositoryInterf
private mapper: MapperInterface<SharedVault, TypeORMSharedVault>,
) {}
async countByUserUuid(userUuid: Uuid): Promise<number> {
const count = await this.ormRepository
.createQueryBuilder('shared_vault')
.where('shared_vault.user_uuid = :userUuid', {
userUuid: userUuid.value,
})
.getCount()
return count
}
async findByUuids(uuids: Uuid[], lastSyncTime?: number | undefined): Promise<SharedVault[]> {
const queryBuilder = this.ormRepository
.createQueryBuilder('shared_vault')