import config from '../lib/config' import WebDAV from '../lib/WebDAV' import TestUtility from '../lib/utilities' describe('create, read, modify, delete events for regular user', function() { const webdav = new WebDAV(config.username, config.password) const webdav_su = new WebDAV(config.superuser, config.superuser_password) const webdav_subscriber = new WebDAV(config.subscriber_username, config.subscriber_password) const utility = new TestUtility(webdav) const event_template = `BEGIN:VCALENDAR PRODID:-//Inverse//Event Generator//EN VERSION:2.0 BEGIN:VEVENT SEQUENCE:0 TRANSP:OPAQUE UID:12345-%(class)-%(filename) SUMMARY:%(class) event (orig. title) DTSTART:20090805T100000Z DTEND:20090805T140000Z CLASS:%(class) DESCRIPTION:%(class) description LOCATION:location %(organizer_line)%(attendee_line)CREATED:20090805T100000Z DTSTAMP:20090805T100000Z END:VEVENT END:VCALENDAR` const task_template = `BEGIN:VCALENDAR PRODID:-//Inverse//Event Generator//EN VERSION:2.0 BEGIN:VTODO CREATED:20100122T201440Z LAST-MODIFIED:20100201T175246Z DTSTAMP:20100201T175246Z UID:12345-%(class)-%(filename) SUMMARY:%(class) event (orig. title) CLASS:%(class) DESCRIPTION:%(class) description STATUS:IN-PROCESS PERCENT-COMPLETE:0 END:VTODO END:VCALENDAR` const resource = `/SOGo/dav/${config.username}/Calendar/test-dav-acl/` const classToICSClass = { 'pu': 'PUBLIC', 'pr': 'PRIVATE', 'co': 'CONFIDENTIAL' } let user const _checkViewEventRight = function(operation, event, eventClass, right) { if (right) { expect(event) .withContext(`Returned event during operation '${operation}'`) .toBeTruthy() if (['v', 'r', 'm'].includes(right)) { const iscClass = classToICSClass[eventClass] const expectedEvent = utility.formatTemplate(event_template, { 'class': iscClass, 'filename': `${iscClass.toLowerCase()}-event.ics` }) expect(event).toBe(expectedEvent) } else if (right == 'd') { _testEventIsSecureVersion(eventClass, event) } else { throw new Error(`Right '${right} is not supported`) } } else { expect(event).toBeFalsy() } } const _currentUserPrivilegeSet = async function(resource, expectedCode = 207) { const results = await webdav_subscriber.currentUserPrivilegeSet(resource) expect(results.length).toBe(1) const response = results[0] expect(response.status).toBe(expectedCode) let privileges = [] if (expectedCode < 300) { privileges = response.props.currentUserPrivilegeSet.privilege.map(o => { return Object.keys(o)[0] }) } return privileges } const _deleteEvent = async function(client, filename, expectedCode = 204) { const response = await client.deleteObject(resource + filename) expect(response.status).toBe(expectedCode) } const _getEvent = async function(eventClass, isInvitation = false) { const iscClass = classToICSClass[eventClass].toLowerCase() const filename = (isInvitation ? `invitation-${iscClass}` : iscClass) + '-event.ics' const [{ status, raw = '' }] = await webdav_subscriber.getObject(resource, filename) if (status == 200) return raw.replace(/\r\n/g,'\n') else return undefined } const _multigetEvent = async function(eventClass) { const iscClass = classToICSClass[eventClass].toLowerCase() const filename = `${iscClass}-event.ics` let event = undefined const results = await webdav_subscriber.calendarMultiGet(resource, filename) if (results.status !== 404) { results.find(o => { if (o.href == resource + filename) { const { props: { calendarData = '' } } = o event = calendarData.replace(/\r\n/g,'\n') return true } return false }) } return event } const _propfindEvent = async function(eventClass) { const iscClass = classToICSClass[eventClass].toLowerCase() const filename = `${iscClass}-event.ics` const results = await webdav_subscriber.propfindEvent(resource + filename) let event = undefined if (results.status !== 404) { results.find(o => { if (o.href == resource + filename) { const { props: { calendarData = '' } } = o event = calendarData.replace(/\r\n/g,'\n') return true } return false }) } return event } const _putEvent = async function(client, filename, eventClass = 'PUBLIC', expectedCode = 201, organizer, attendee, partstat = 'NEEDS-ACTION') { const organizer_line = organizer ? `ORGANIZER:${organizer}\n` : '' const attendee_line = attendee ? `ATTENDEE;PARTSTAT=${partstat}:${attendee}\n` : '' const event = utility.formatTemplate(event_template, { 'class': eventClass, 'filename': filename, organizer_line, attendee_line }) const response = await client.createCalendarObject(resource, filename, event) expect(response.status).toBe(expectedCode) } const _webdavSyncEvent = async function(eventClass) { const iscClass = classToICSClass[eventClass].toLowerCase() const filename = `${iscClass}-event.ics` let event = undefined const results = await webdav_subscriber.syncColletion(resource) if (results.status !== 404) { results.find(o => { if (o.href == resource + filename) { const { props: { calendarData = '' } } = o event = calendarData.length ? calendarData.replace(/\r\n/g,'\n') : undefined return true } return false }) } return event } const _testCreate = async function(rights) { let expectedCode if (rights.c) expectedCode = 201 else if (Object.keys(rights).length === 0) expectedCode = 404 else expectedCode = 403 return _putEvent(webdav_subscriber, 'creation-test.ics', 'PUBLIC', expectedCode) } const _testCollectionDAVAcl = async function(rights) { let expectedPrivileges = [] if (Object.keys(rights).length > 0) { expectedPrivileges.push('read', 'readCurrentUserPrivilegeSet', 'readFreeBusy') } if (rights.c) { expectedPrivileges.push( 'bind', 'writeContent', 'schedule', 'schedulePost', 'schedulePostVevent', 'schedulePostVtodo', 'schedulePostVjournal', 'schedulePostVfreebusy', 'scheduleDeliver', 'scheduleDeliverVevent', 'scheduleDeliverVtodo', 'scheduleDeliverVjournal', 'scheduleDeliverVfreebusy', 'scheduleRespond', 'scheduleRespondVevent', 'scheduleRespondVtodo' ) } if (rights.d) { expectedPrivileges.push('unbind') } const expectedCode = (expectedPrivileges.length == 0) ? 404 : 207 const privileges = await _currentUserPrivilegeSet(resource, expectedCode) // When comparing privileges on DAV collection, we remove all 'default' // privileges on the collection. for (const c of ['Public', 'Private', 'Confidential']) { for (const r of ['viewdant', 'viewwhole', 'modify', 'respondto']) { const i = privileges.indexOf(`${r}${c}Records`) if (i >= 0) { privileges.splice(i, 1) } } } // for (const privilege of ['read', 'readCurrentUserPrivilegeSet', 'readFreeBusy']) { for (const expectedPrivilege of expectedPrivileges) { expect(privileges).toContain(expectedPrivilege) } } const _testEventIsSecureVersion = function(eventClass, event) { const iscClass = classToICSClass[eventClass].toLowerCase().replace(/^\w/, c => c.toUpperCase()) const expectedDict = { version: 'VERSION:2.0', prodid: 'PRODID:-//Inverse//Event Generator//EN', summary: `SUMMARY:(${iscClass} event)`, dtstart: 'DTSTART:20090805T100000Z', dtend: 'DTEND:20090805T140000Z', dtstamp: 'DTSTAMP:20090805T100000Z', 'x-sogo-secure': 'X-SOGO-SECURE:YES' } const eventDict = utility.versitDict(event) // Ignore UID for (const key of Object.keys(eventDict).filter(k => k !== 'uid')) { expect(expectedDict[key]) .withContext(`Key ${key} of secure event is expected`) .toBeTruthy() if (expectedDict[key]) expect(expectedDict[key]) .withContext(`Value of key ${key} of secure event is valid`) .toBe(eventDict[key]) } for (const key of Object.keys(expectedDict)) { expect(eventDict[key]) .withContext(`Key ${key} of secure event is present`) .toBeTruthy() } } const _testModify = async function(eventClass, right, errorCode) { const iscClass = classToICSClass[eventClass] const filename = `${iscClass.toLowerCase()}-event.ics` let expectedCode = errorCode if (['r', 'm'].includes(right)) expectedCode = 204 return _putEvent(webdav_subscriber, filename, iscClass, expectedCode) } const _testRespondTo = async function(eventClass, right, errorCode) { const iscClass = classToICSClass[eventClass] const filename = `invitation-${iscClass.toLowerCase()}-event.ics` let expectedCode = errorCode if (['r', 'm'].includes(right)) expectedCode = 204 await _putEvent(webdav, filename, iscClass, 201, 'mailto:nobody@somewhere.com', user.email, 'NEEDS-ACTION') // here we only do 'passive' validation: if a user has a "respond to" // right, only the attendee entry will me modified. The change of // organizer must thus be silently ignored below. await _putEvent(webdav_subscriber, filename, iscClass, expectedCode, 'mailto:someone@nowhere.com', user.email, 'ACCEPTED') if (expectedCode == 204) { const attendee_line = `ATTENDEE;PARTSTAT=ACCEPTED:${user.email}\n` let expectedEvent if (right == 'r') { expectedEvent = utility.formatTemplate(event_template, { 'class': iscClass, 'filename': filename, organizer_line: 'ORGANIZER;CN=nobody@somewhere.com:mailto:nobody@somewhere.com\n', attendee_line }) } else { expectedEvent = utility.formatTemplate(event_template, { 'class': iscClass, 'filename': filename, organizer_line: 'ORGANIZER;CN=someone@nowhere.com:mailto:someone@nowhere.com\n', attendee_line }) } const event = await _getEvent(eventClass, true) expect(utility.componentsAreEqual(expectedEvent, event)) .withContext('Calendars of organizer and attendee are identical') .toBe(true) } } const _testEventDAVAcl = async function(eventClass, right, errorCode) { const iscClass = classToICSClass[eventClass].toLowerCase() for (const suffix of ['event', 'task']) { const filename = `${iscClass}-${suffix}.ics` let expectedCode = errorCode let expectedPrivileges = [] if (right) { expectedCode = 207 expectedPrivileges.push('readCurrentUserPrivilegeSet', 'viewDateAndTime', 'read') if (right != 'd') { expectedPrivileges.push('viewWholeComponent') if (right != 'v') { expectedPrivileges.push('respondToComponent', 'writeContent') if (right != 'r') { expectedPrivileges.push('writeProperties', 'write') } } } } const privileges = await _currentUserPrivilegeSet(resource + filename, expectedCode) if (errorCode != expectedCode) { for (const expectedPrivilege of expectedPrivileges) { expect(privileges).toContain(expectedPrivilege) } } } } const _testEventRight = async function(eventClass, rights) { const right = Object.keys(rights).includes(eventClass) ? rights[eventClass] : undefined let event event = await _getEvent(eventClass) _checkViewEventRight('GET', event, eventClass, right) event = await _propfindEvent(eventClass) _checkViewEventRight('PROPFIND', event, eventClass, right) event = await _multigetEvent(eventClass) _checkViewEventRight('multiget', event, eventClass, right) event = await _webdavSyncEvent(eventClass) _checkViewEventRight('webdav-sync', event, eventClass, right) const errorCode = (Object.keys(rights).length > 0) ? 403 : 404 await _testModify(eventClass, right, errorCode) await _testRespondTo(eventClass, right, errorCode) await _testEventDAVAcl(eventClass, right, errorCode) } const _testDelete = async function(rights) { let expectedCode = 403 if (rights && rights.d) { expectedCode = 204 } else if (Object.keys(rights) == 0) { expectedCode = 404 } for (const eventClass of Object.values(classToICSClass)) { await _deleteEvent(webdav_subscriber, `${eventClass.toLocaleLowerCase()}-event.ics`, expectedCode) } } const _testRights = async function(rights) { const results = await utility.setupCalendarRights(resource, config.subscriber_username, rights) expect(results.length).toBe(1) expect(results[0].status) .withContext(`Setup rights (${JSON.stringify(rights)}) on ${resource}`) .toBe(204) await _testCreate(rights) await _testCollectionDAVAcl(rights) await _testEventRight('pu', rights) await _testEventRight('pr', rights) await _testEventRight('co', rights) await _testDelete(rights) } beforeEach(async function() { user = await utility.fetchUserInfo(config.username) await webdav.deleteObject(resource) await webdav.makeCalendar(resource) for (const c of Object.values(classToICSClass)) { // Create event for each class const eventFilename = `${c.toLowerCase()}-event.ics` const event = utility.formatTemplate(event_template, { 'class': c, 'filename': eventFilename }) let response = await webdav.createCalendarObject(resource, eventFilename, event) expect(response.status) .withContext(`HTTP status when creating event with ${c} class`) .toBe(201) // Create task for each class const taskFilename = `${c.toLowerCase()}-task.ics` const task = utility.formatTemplate(task_template, { 'class': c, 'filename': taskFilename }) response = await webdav.createCalendarObject(resource, taskFilename, task) expect(response.status) .withContext(`HTTP status when creating task with ${c} class`) .toBe(201) } }) afterEach(async function() { await webdav_su.deleteObject(resource) }) // DAVCalendarAclTest it("'view all' on a specific class (PUBLIC)", async function() { await _testRights({ pu: 'v' }) }) it("'modify' PUBLIC, 'view all' PRIVATE, 'view d&t' confidential", async function() { await _testRights({ pu: 'm', pr: 'v', co: 'd' }) }) it("'create' only", async function() { await _testRights({ c: true }) }) it("'delete' only", async function() { await _testRights({ d: true }) }) it("'create', 'delete', 'view d&t' PUBLIC, 'modify' PRIVATE", async function() { await _testRights({ c: true, d: true, pu: 'd', pr: 'm' }) }) it("'create', 'respond to' PUBLIC", async function() { await _testRights({ c: true, pu: 'r' }) }) it("no right given", async function() { await _testRights({}) }) })