| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950 |
- import { vi, expect } from 'vitest'
- import path from 'node:path'
- import sinon from 'sinon'
- import mongodb from 'mongodb-legacy'
- import PrivilegeLevels from '../../../../app/src/Features/Authorization/PrivilegeLevels.mjs'
- const modulePath = path.join(
- import.meta.dirname,
- '../../../../app/src/Features/TokenAccess/TokenAccessHandler'
- )
- vi.mock('node:crypto', async () => {
- const originalModule = await vi.importActual('node:crypto')
- return {
- default: {
- ...originalModule,
- timingSafeEqual: vi.fn(originalModule.default.timingSafeEqual),
- },
- }
- })
- const { ObjectId } = mongodb
- describe('TokenAccessHandler', function () {
- beforeEach(async function (ctx) {
- ctx.token = 'abcdefabcdef'
- ctx.projectId = new ObjectId()
- ctx.project = {
- _id: ctx.projectId,
- publicAccesLevel: 'tokenBased',
- owner_ref: new ObjectId(),
- }
- ctx.userId = new ObjectId()
- ctx.req = {}
- vi.doMock('mongodb-legacy', () => ({
- default: { ObjectId },
- }))
- vi.doMock('../../../../app/src/models/Project', () => ({
- Project: (ctx.Project = {}),
- }))
- vi.doMock('@overleaf/metrics', () => ({
- default: (ctx.Metrics = { inc: sinon.stub() }),
- }))
- vi.doMock('@overleaf/settings', () => ({
- default: (ctx.settings = { disableLinkSharing: false }),
- }))
- vi.doMock('../../../../app/src/Features/V1/V1Api', () => ({
- default: (ctx.V1Api = {
- promises: {
- request: sinon.stub(),
- },
- }),
- }))
- ctx.Crypto = (await vi.importMock('node:crypto')).default
- vi.doMock(
- '../../../../app/src/Features/Analytics/AnalyticsManager',
- () => ({
- default: (ctx.Analytics = {
- recordEventForUserInBackground: sinon.stub(),
- }),
- })
- )
- vi.doMock('../../../../app/src/infrastructure/Features', () => ({
- default: (ctx.Features = {}),
- }))
- ctx.TokenAccessHandler = (await import(modulePath)).default
- })
- describe('when link sharing is enabled', function () {
- beforeEach(function (ctx) {
- ctx.Features.hasFeature = sinon
- .stub()
- .withArgs('link-sharing')
- .returns(true)
- })
- describe('getTokenType', function () {
- it('should determine tokens correctly', function (ctx) {
- const specs = {
- abcdefabcdef: 'readOnly',
- aaaaaabbbbbb: 'readOnly',
- '54325aaaaaa': 'readAndWrite',
- '54325aaaaaabbbbbb': 'readAndWrite',
- '': null,
- abc123def: null,
- }
- for (const token of Object.keys(specs)) {
- expect(ctx.TokenAccessHandler.getTokenType(token)).to.equal(
- specs[token]
- )
- }
- })
- })
- describe('getProjectByReadOnlyToken', function () {
- beforeEach(function (ctx) {
- ctx.token = 'abcdefabcdef'
- ctx.Project.findOne = sinon.stub().returns({
- exec: sinon.stub().resolves(ctx.project),
- })
- })
- it('should get the project', async function (ctx) {
- const project =
- await ctx.TokenAccessHandler.promises.getProjectByReadOnlyToken(
- ctx.token
- )
- expect(project).to.exist
- expect(ctx.Project.findOne.callCount).to.equal(1)
- })
- })
- describe('getProjectByReadAndWriteToken', function () {
- beforeEach(function (ctx) {
- ctx.token = '1234abcdefabcdef'
- ctx.project.tokens = {
- readAndWrite: ctx.token,
- readAndWritePrefix: '1234',
- }
- ctx.Project.findOne = sinon.stub().returns({
- exec: sinon.stub().resolves(ctx.project),
- })
- })
- it('should get the project and do timing-safe comparison', async function (ctx) {
- const project =
- await ctx.TokenAccessHandler.promises.getProjectByReadAndWriteToken(
- ctx.token
- )
- expect(project).to.exist
- expect(ctx.Crypto.timingSafeEqual).toHaveBeenCalledTimes(1)
- expect(
- ctx.Crypto.timingSafeEqual.mock.calls[0][0].equals(
- Buffer.from(ctx.token)
- )
- ).toBeTruthy()
- expect(ctx.Project.findOne.callCount).to.equal(1)
- })
- })
- describe('addReadOnlyUserToProject', function () {
- beforeEach(function (ctx) {
- ctx.Project.updateOne = sinon.stub().returns({
- exec: sinon.stub().resolves(null),
- })
- })
- it('should call Project.updateOne', async function (ctx) {
- await ctx.TokenAccessHandler.promises.addReadOnlyUserToProject(
- ctx.userId,
- ctx.projectId,
- ctx.project.owner_ref
- )
- expect(ctx.Project.updateOne.callCount).to.equal(1)
- expect(
- ctx.Project.updateOne.calledWith({
- _id: ctx.projectId,
- })
- ).to.equal(true)
- expect(ctx.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
- 'tokenAccessReadOnly_refs'
- )
- sinon.assert.calledWith(
- ctx.Analytics.recordEventForUserInBackground,
- ctx.userId,
- 'project-joined',
- {
- mode: 'view',
- role: PrivilegeLevels.READ_ONLY,
- projectId: ctx.projectId.toString(),
- ownerId: ctx.project.owner_ref.toString(),
- source: 'link-sharing',
- }
- )
- })
- describe('when Project.updateOne produces an error', function () {
- beforeEach(function (ctx) {
- ctx.Project.updateOne = sinon
- .stub()
- .returns({ exec: sinon.stub().rejects(new Error('woops')) })
- })
- it('should be rejected', async function (ctx) {
- await expect(
- ctx.TokenAccessHandler.promises.addReadOnlyUserToProject(
- ctx.userId,
- ctx.projectId
- )
- ).to.be.rejected
- })
- })
- })
- describe('removeReadAndWriteUserFromProject', function () {
- beforeEach(function (ctx) {
- ctx.Project.updateOne = sinon
- .stub()
- .returns({ exec: sinon.stub().resolves(null) })
- })
- it('should call Project.updateOne', async function (ctx) {
- await ctx.TokenAccessHandler.promises.removeReadAndWriteUserFromProject(
- ctx.userId,
- ctx.projectId
- )
- expect(ctx.Project.updateOne.callCount).to.equal(1)
- expect(
- ctx.Project.updateOne.calledWith({
- _id: ctx.projectId,
- })
- ).to.equal(true)
- expect(ctx.Project.updateOne.lastCall.args[1].$pull).to.have.keys(
- 'tokenAccessReadAndWrite_refs'
- )
- })
- })
- describe('moveReadAndWriteUserToReadOnly', function () {
- beforeEach(function (ctx) {
- ctx.Project.updateOne = sinon
- .stub()
- .returns({ exec: sinon.stub().resolves(null) })
- })
- it('should call Project.updateOne', async function (ctx) {
- await ctx.TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly(
- ctx.userId,
- ctx.projectId
- )
- expect(ctx.Project.updateOne.callCount).to.equal(1)
- expect(
- ctx.Project.updateOne.calledWith({
- _id: ctx.projectId,
- })
- ).to.equal(true)
- expect(ctx.Project.updateOne.lastCall.args[1].$pull).to.have.keys(
- 'tokenAccessReadAndWrite_refs'
- )
- expect(ctx.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
- 'tokenAccessReadOnly_refs'
- )
- })
- })
- describe('grantSessionTokenAccess', function () {
- beforeEach(function (ctx) {
- ctx.req = { session: {}, headers: {} }
- })
- it('should add the token to the session', function (ctx) {
- ctx.TokenAccessHandler.promises.grantSessionTokenAccess(
- ctx.req,
- ctx.projectId,
- ctx.token
- )
- expect(
- ctx.req.session.anonTokenAccess[ctx.projectId.toString()]
- ).to.equal(ctx.token)
- })
- })
- describe('validateTokenForAnonymousAccess', function () {
- describe('when a read-only project is found', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.getTokenType = sinon.stub().returns('readOnly')
- ctx.TokenAccessHandler.promises.getProjectByToken = sinon
- .stub()
- .resolves(ctx.project)
- })
- it('should try to find projects with both kinds of token', async function (ctx) {
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(
- ctx.TokenAccessHandler.promises.getProjectByToken.callCount
- ).to.equal(1)
- })
- it('should allow read-only access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(false)
- expect(isValidReadOnly).to.equal(true)
- })
- })
- describe('when a read-and-write project is found', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.promises.getTokenType = sinon
- .stub()
- .returns('readAndWrite')
- ctx.TokenAccessHandler.promises.getProjectByToken = sinon
- .stub()
- .resolves(ctx.project)
- })
- describe('when Anonymous token access is not enabled', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
- })
- it('should try to find projects with both kinds of token', async function (ctx) {
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(
- ctx.TokenAccessHandler.promises.getProjectByToken.callCount
- ).to.equal(1)
- })
- it('should not allow read-and-write access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(false)
- expect(isValidReadOnly).to.equal(false)
- })
- })
- describe('when anonymous token access is enabled', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.promises.ANONYMOUS_READ_AND_WRITE_ENABLED = true
- })
- it('should try to find projects with both kinds of token', async function (ctx) {
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(
- ctx.TokenAccessHandler.promises.getProjectByToken.callCount
- ).to.equal(1)
- })
- it('should allow read-and-write access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(true)
- expect(isValidReadOnly).to.equal(false)
- })
- })
- })
- describe('when no project is found', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.promises.getProjectByToken = sinon
- .stub()
- .resolves(null)
- })
- it('should try to find projects with both kinds of token', async function (ctx) {
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(
- ctx.TokenAccessHandler.promises.getProjectByToken.callCount
- ).to.equal(1)
- })
- it('should not allow any access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(false)
- expect(isValidReadOnly).to.equal(false)
- })
- })
- describe('when findProject produces an error', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.promises.getProjectByToken = sinon
- .stub()
- .rejects(new Error('woops'))
- })
- it('should try to find projects with both kinds of token', async function (ctx) {
- await expect(
- ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- ).to.be.rejected
- expect(
- ctx.TokenAccessHandler.promises.getProjectByToken.callCount
- ).to.equal(1)
- })
- it('should produce an error and not allow access', async function (ctx) {
- await expect(
- ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- ).to.be.rejected
- })
- })
- describe('when project is not set to token-based access', function () {
- beforeEach(function (ctx) {
- ctx.project.publicAccesLevel = 'private'
- })
- describe('for read-and-write project', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.getTokenType = sinon
- .stub()
- .returns('readAndWrite')
- ctx.TokenAccessHandler.promises.getProjectByToken = sinon
- .stub()
- .resolves(ctx.project)
- })
- it('should not allow any access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(false)
- expect(isValidReadOnly).to.equal(false)
- })
- })
- describe('for read-only project', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.getTokenType = sinon
- .stub()
- .returns('readOnly')
- ctx.TokenAccessHandler.promises.getProjectByToken = sinon
- .stub()
- .resolves(ctx.project)
- })
- it('should not allow any access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(false)
- expect(isValidReadOnly).to.equal(false)
- })
- })
- describe('with nothing', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.promises.getProjectByToken = sinon
- .stub()
- .resolves(null)
- })
- it('should not allow any access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(false)
- expect(isValidReadOnly).to.equal(false)
- })
- })
- })
- })
- })
- describe('when link sharing is disabled', function () {
- beforeEach(function (ctx) {
- ctx.Features.hasFeature = sinon
- .stub()
- .withArgs('link-sharing')
- .returns(false)
- })
- describe('addReadOnlyUserToProject', function () {
- beforeEach(function (ctx) {
- ctx.Project.updateOne = sinon.stub().returns({
- exec: sinon.stub().resolves(null),
- })
- })
- it('should throw an error', async function (ctx) {
- await expect(
- ctx.TokenAccessHandler.promises.addReadOnlyUserToProject(
- ctx.userId,
- ctx.projectId,
- ctx.project.owner_ref
- )
- ).to.be.rejectedWith('link sharing is disabled')
- expect(ctx.Project.updateOne.callCount).to.equal(0)
- })
- })
- describe('grantSessionTokenAccess', function () {
- beforeEach(function (ctx) {
- ctx.req = { session: {}, headers: {} }
- })
- it('should throw an error', function (ctx) {
- expect(() => {
- ctx.TokenAccessHandler.promises.grantSessionTokenAccess(
- ctx.req,
- ctx.projectId,
- ctx.token
- )
- }).to.throw('link sharing is disabled')
- expect(ctx.req.session.anonTokenAccess).to.be.undefined
- })
- })
- describe('validateTokenForAnonymousAccess', function () {
- describe('when a read-only project is found', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.getTokenType = sinon.stub().returns('readOnly')
- ctx.TokenAccessHandler.promises.getProjectByToken = sinon
- .stub()
- .resolves(ctx.project)
- })
- it('should refuse access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(false)
- expect(isValidReadOnly).to.equal(false)
- })
- })
- describe('when a read-and-write project is found', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.promises.getTokenType = sinon
- .stub()
- .returns('readAndWrite')
- ctx.TokenAccessHandler.promises.getProjectByToken = sinon
- .stub()
- .resolves(ctx.project)
- })
- describe('when Anonymous token access is not enabled', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
- })
- it('should refuse access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(false)
- expect(isValidReadOnly).to.equal(false)
- })
- })
- describe('when anonymous token access is enabled', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.promises.ANONYMOUS_READ_AND_WRITE_ENABLED = true
- })
- it('should not try to find any projects', async function (ctx) {
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(
- ctx.TokenAccessHandler.promises.getProjectByToken.callCount
- ).to.equal(0)
- })
- it('should refuse access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(false)
- expect(isValidReadOnly).to.equal(false)
- })
- })
- })
- describe('when no project is found', function () {
- beforeEach(function (ctx) {
- ctx.TokenAccessHandler.promises.getProjectByToken = sinon
- .stub()
- .resolves(null)
- })
- it('should not try to find any projects ', async function (ctx) {
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(
- ctx.TokenAccessHandler.promises.getProjectByToken.callCount
- ).to.equal(0)
- })
- it('should not allow any access', async function (ctx) {
- const { isValidReadAndWrite, isValidReadOnly } =
- await ctx.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
- ctx.projectId,
- ctx.token
- )
- expect(isValidReadAndWrite).to.equal(false)
- expect(isValidReadOnly).to.equal(false)
- })
- })
- })
- })
- describe('getDocPublishedInfo', function () {
- describe('when v1 api not set', function () {
- beforeEach(function (ctx) {
- ctx.settings.apis = { v1: undefined }
- })
- it('should not check access and return default info', async function (ctx) {
- const info =
- await ctx.TokenAccessHandler.promises.getV1DocPublishedInfo(ctx.token)
- expect(ctx.V1Api.promises.request.called).to.equal(false)
- expect(info).to.deep.equal({
- allow: true,
- })
- })
- })
- describe('when v1 api is set', function () {
- beforeEach(function (ctx) {
- ctx.settings.apis = { v1: { url: 'v1Url' } }
- })
- describe('on V1Api.request success', function () {
- beforeEach(function (ctx) {
- ctx.V1Api.promises.request = sinon
- .stub()
- .resolves({ body: 'mock-data' })
- })
- it('should return response body', async function (ctx) {
- const info =
- await ctx.TokenAccessHandler.promises.getV1DocPublishedInfo(
- ctx.token
- )
- expect(
- ctx.V1Api.promises.request.calledWith({
- url: `/api/v1/overleaf/docs/${ctx.token}/is_published`,
- })
- ).to.equal(true)
- expect(info).to.equal('mock-data')
- })
- })
- describe('on V1Api.request error', function () {
- beforeEach(function (ctx) {
- ctx.V1Api.promises.request = sinon.stub().rejects('error')
- })
- it('should be rejected', async function (ctx) {
- await expect(
- ctx.TokenAccessHandler.promises.getV1DocPublishedInfo(ctx.token)
- ).to.be.rejected
- })
- })
- })
- })
- describe('getV1DocInfo', function () {
- describe('when v1 api not set', function () {
- it('should not check access and return default info', async function (ctx) {
- const info = await ctx.TokenAccessHandler.promises.getV1DocInfo(
- ctx.token,
- ctx.v2UserId
- )
- expect(ctx.V1Api.promises.request.called).to.equal(false)
- expect(info).to.deep.equal({
- exists: true,
- exported: false,
- })
- })
- })
- describe('when v1 api is set', function () {
- beforeEach(function (ctx) {
- ctx.settings.apis = { v1: 'v1' }
- })
- describe('on V1Api.request success', function () {
- beforeEach(function (ctx) {
- ctx.V1Api.promises.request = sinon
- .stub()
- .resolves({ body: 'mock-data' })
- })
- it('should return response body', async function (ctx) {
- const info = await ctx.TokenAccessHandler.promises.getV1DocInfo(
- ctx.token,
- ctx.v2UserId
- )
- expect(
- ctx.V1Api.promises.request.calledWith({
- url: `/api/v1/overleaf/docs/${ctx.token}/info`,
- })
- ).to.equal(true)
- expect(info).to.equal('mock-data')
- })
- })
- describe('on V1Api.request error', function () {
- beforeEach(function (ctx) {
- ctx.V1Api.promises.request = sinon.stub().rejects('error')
- })
- it('should be rejected', async function (ctx) {
- await expect(
- ctx.TokenAccessHandler.promises.getV1DocInfo(
- ctx.token,
- ctx.v2UserId
- )
- ).to.be.rejected
- })
- })
- })
- })
- describe('createTokenHashPrefix', function () {
- it('creates a prefix of the hash', function (ctx) {
- const prefix =
- ctx.TokenAccessHandler.createTokenHashPrefix('zxpxjrwdtsgd')
- expect(prefix.length).to.equal(6)
- })
- })
- describe('normalizeTokenHashPrefix', function () {
- const cases = {
- // hex string
- ab2345: 'ab2345',
- '01234f': '01234f',
- '012345': '012345',
- // remove (encoded) hash
- '#012345': '012345',
- '%23012345': '012345',
- // remove trailing special characters
- '012345.': '012345',
- '012345/': '012345',
- // v1 doc
- '%2F1234567%2F': '%2F1234567%2F',
- }
- for (const [input, output] of Object.entries(cases)) {
- it(`should handle ${JSON.stringify(input)}`, function (ctx) {
- expect(ctx.TokenAccessHandler.normalizeTokenHashPrefix(input)).to.equal(
- output
- )
- })
- }
- })
- describe('checkTokenHashPrefix', function () {
- const userId = 'abc123'
- const projectId = 'def456'
- it('sends "match" to metrics when prefix matches the prefix of the hash of the token', function (ctx) {
- const token = 'zxpxjrwdtsgd'
- const prefix = ctx.TokenAccessHandler.createTokenHashPrefix(token)
- ctx.TokenAccessHandler.checkTokenHashPrefix(
- token,
- `#${prefix}`,
- 'readOnly',
- userId,
- { projectId }
- )
- expect(ctx.Metrics.inc).to.have.been.calledWith(
- 'link-sharing.hash-check',
- {
- path: 'readOnly',
- status: 'match',
- }
- )
- })
- it('sends "mismatch" to metrics when prefix does not match the prefix of the hash of the token', function (ctx) {
- const token = 'zxpxjrwdtsgd'
- const prefix = ctx.TokenAccessHandler.createTokenHashPrefix(token)
- ctx.TokenAccessHandler.checkTokenHashPrefix(
- 'anothertoken',
- `#${prefix}`,
- 'readOnly',
- userId,
- { projectId }
- )
- expect(ctx.Metrics.inc).to.have.been.calledWith(
- 'link-sharing.hash-check',
- {
- path: 'readOnly',
- status: 'mismatch',
- }
- )
- expect(ctx.logger.info).toHaveBeenCalledWith(
- {
- tokenHashPrefix: prefix,
- hashPrefixStatus: 'mismatch',
- userId,
- projectId,
- type: 'readOnly',
- },
- 'mismatched token hash prefix'
- )
- })
- it('sends "missing" to metrics when prefix is undefined', function (ctx) {
- ctx.TokenAccessHandler.checkTokenHashPrefix(
- 'anothertoken',
- undefined,
- 'readOnly',
- userId,
- { projectId }
- )
- expect(ctx.Metrics.inc).to.have.been.calledWith(
- 'link-sharing.hash-check',
- {
- path: 'readOnly',
- status: 'missing',
- }
- )
- })
- it('sends "missing" to metrics when URL hash is sent as "#" only', function (ctx) {
- ctx.TokenAccessHandler.checkTokenHashPrefix(
- 'anothertoken',
- '#',
- 'readOnly',
- userId,
- { projectId }
- )
- expect(ctx.Metrics.inc).to.have.been.calledWith(
- 'link-sharing.hash-check',
- {
- path: 'readOnly',
- status: 'missing',
- }
- )
- })
- it('handles encoded hashtags', function (ctx) {
- const token = 'zxpxjrwdtsgd'
- const prefix = ctx.TokenAccessHandler.createTokenHashPrefix(token)
- ctx.TokenAccessHandler.checkTokenHashPrefix(
- token,
- `%23${prefix}`,
- 'readOnly',
- userId,
- { projectId }
- )
- expect(ctx.Metrics.inc).to.have.been.calledWith(
- 'link-sharing.hash-check',
- {
- path: 'readOnly',
- status: 'match',
- }
- )
- })
- it('sends "mismatch-v1-format" for suspected v1 URLs with 7 numbers in URL fragment', function (ctx) {
- const token = '4112142489ddsbkrdzhxrq'
- const prefix = '%2F1234567%2F'
- ctx.TokenAccessHandler.checkTokenHashPrefix(
- token,
- `#${prefix}`,
- 'readAndWrite',
- userId,
- { projectId }
- )
- expect(ctx.Metrics.inc).to.have.been.calledWith(
- 'link-sharing.hash-check',
- {
- path: 'readAndWrite',
- status: 'mismatch-v1-format',
- }
- )
- })
- it('sends "mismatch-v1-format" for suspected v1 URLs with 8 numbers in URL fragment', function (ctx) {
- const token = '4112142489ddsbkrdzhxrq'
- const prefix = '%2F12345678%2F'
- ctx.TokenAccessHandler.checkTokenHashPrefix(
- token,
- `#${prefix}`,
- 'readAndWrite',
- userId,
- { projectId }
- )
- expect(ctx.Metrics.inc).to.have.been.calledWith(
- 'link-sharing.hash-check',
- {
- path: 'readAndWrite',
- status: 'mismatch-v1-format',
- }
- )
- })
- })
- })
|