Merge pull request #3173 from overleaf/jel-institution-sso-audit-log

Update audit log when institution SSO is linked/unlinked

GitOrigin-RevId: 264ffbed025dccb4dd202f86afe545c3bf0c1f76
This commit is contained in:
Jessica Lawshe
2020-09-15 02:04:24 +00:00
committed by Copybot
parent 7c093ba115
commit aee83bc0cf
7 changed files with 446 additions and 173 deletions
@@ -11,12 +11,17 @@ describe('SAMLIdentityManager', function() {
NotFoundError: sinon.stub(),
SAMLIdentityExistsError: sinon.stub()
}
this.userId = 'user-id-1'
this.user = {
_id: 'user-id-1',
_id: this.userId,
email: 'not-linked@overleaf.com',
emails: [{ email: 'not-linked@overleaf.com' }],
samlIdentifiers: []
}
this.auditLog = {
initiatorId: this.userId,
ipAddress: '0:0:0:0'
}
this.userAlreadyLinked = {
_id: 'user-id-2',
email: 'linked@overleaf.com',
@@ -43,6 +48,9 @@ describe('SAMLIdentityManager', function() {
warn: sinon.stub()
}
this.SAMLIdentityManager = SandboxedModule.require(modulePath, {
globals: {
console: console
},
requires: {
'../Email/EmailHandler': (this.EmailHandler = {
sendEmail: sinon.stub().yields()
@@ -73,6 +81,11 @@ describe('SAMLIdentityManager', function() {
})
})
},
'../User/UserAuditLogHandler': (this.UserAuditLogHandler = {
promises: {
addEntry: sinon.stub().resolves()
}
}),
'../User/UserGetter': (this.UserGetter = {
getUser: sinon.stub(),
promises: {
@@ -108,76 +121,157 @@ describe('SAMLIdentityManager', function() {
})
describe('linkAccounts', function() {
it('should throw an error if missing data', async function() {
let error
try {
await this.SAMLIdentityManager.linkAccounts(null, null, null, null)
} catch (e) {
error = e
} finally {
expect(error).to.exist
}
})
describe('when email is already associated with another Overleaf account', function() {
beforeEach(function() {
this.UserGetter.promises.getUserByAnyEmail.resolves(
this.userEmailExists
)
})
it('should throw an EmailExistsError error', async function() {
describe('errors', function() {
it('should throw an error if missing data', async function() {
let error
try {
await this.SAMLIdentityManager.linkAccounts(
'user-id-1',
'not-linked-id',
'exists@overleaf.com',
'provider-id',
true
null,
null,
null,
null,
null
)
} catch (e) {
error = e
} finally {
expect(error).to.be.instanceof(this.Errors.EmailExistsError)
expect(this.User.findOneAndUpdate).to.not.have.been.called
expect(error).to.exist
}
})
})
describe('when institution identifier is already associated with another Overleaf account', function() {
beforeEach(function() {
this.UserGetter.promises.getUserByAnyEmail.resolves(
this.userAlreadyLinked
)
describe('when email is already associated with another Overleaf account', function() {
beforeEach(function() {
this.UserGetter.promises.getUserByAnyEmail.resolves(
this.userEmailExists
)
})
it('should throw an EmailExistsError error', async function() {
let error
try {
await this.SAMLIdentityManager.linkAccounts(
'user-id-1',
'not-linked-id',
'exists@overleaf.com',
'provider-id',
'provider-name',
true,
{
intiatorId: 'user-id-1',
ip: '0:0:0:0'
}
)
} catch (e) {
error = e
} finally {
expect(error).to.be.instanceof(this.Errors.EmailExistsError)
expect(this.User.findOneAndUpdate).to.not.have.been.called
}
})
})
it('should throw an SAMLIdentityExistsError error', async function() {
describe('when institution identifier is already associated with another Overleaf account', function() {
beforeEach(function() {
this.UserGetter.promises.getUserByAnyEmail.resolves(
this.userAlreadyLinked
)
})
it('should throw an SAMLIdentityExistsError error', async function() {
let error
try {
await this.SAMLIdentityManager.linkAccounts(
'user-id-1',
'already-linked-id',
'linked@overleaf.com',
'provider-id',
'provider-name',
true,
{
intiatorId: 'user-id-1',
ip: '0:0:0:0'
}
)
} catch (e) {
error = e
} finally {
expect(error).to.be.instanceof(this.Errors.SAMLIdentityExistsError)
expect(this.User.findOneAndUpdate).to.not.have.been.called
}
})
})
it('should pass back errors via UserAuditLogHandler', async function() {
let error
const anError = new Error('oops')
this.UserAuditLogHandler.promises.addEntry.rejects(anError)
try {
await this.SAMLIdentityManager.linkAccounts(
'user-id-1',
'already-linked-id',
'linked@overleaf.com',
'provider-id',
true
this.user._id,
'externalUserId',
this.user.email,
'1',
'Overleaf University',
undefined,
{
intiatorId: 'user-id-1',
ipAddress: '0:0:0:0'
}
)
} catch (e) {
error = e
} finally {
expect(error).to.be.instanceof(this.Errors.SAMLIdentityExistsError)
expect(this.User.findOneAndUpdate).to.not.have.been.called
expect(error).to.exist
expect(error).to.equal(anError)
expect(this.EmailHandler.sendEmail).to.not.have.been.called
expect(this.User.update).to.not.have.been.called
}
})
})
describe('after linking', function() {
it('should send an email notification', function() {
describe('success', function() {
it('should update the user audit log', function() {
const auditLog = {
intiatorId: 'user-id-1',
ip: '0:0:0:0'
}
this.SAMLIdentityManager.linkAccounts(
this.user._id,
'externalUserId',
this.user.email,
'1',
'Overleaf University',
undefined,
auditLog,
() => {
expect(
this.UserAuditLogHandler.promises.addEntry
).to.have.been.calledWith(
this.user._id,
'link-institution-sso',
auditLog.initiatorId,
auditLog.ip,
{
institutionEmail: this.user.email,
providerId: '1',
providerName: 'Overleaf University'
}
)
}
)
})
it('should send an email notification', function() {
this.SAMLIdentityManager.linkAccounts(
this.user._id,
'externalUserId',
this.user.email,
'1',
'Overleaf University',
undefined,
{
intiatorId: 'user-id-1',
ipAddress: '0:0:0:0'
},
() => {
expect(this.User.update).to.have.been.called
expect(this.EmailHandler.sendEmail).to.have.been.called
@@ -188,18 +282,94 @@ describe('SAMLIdentityManager', function() {
})
describe('unlinkAccounts', function() {
it('should send an email notification', function() {
this.SAMLIdentityManager.unlinkAccounts(
const linkedEmail = 'another@example.com'
it('should update the audit log', async function() {
await this.SAMLIdentityManager.unlinkAccounts(
this.user._id,
linkedEmail,
this.user.email,
'1',
'Overleaf University',
() => {
expect(this.User.update).to.have.been.called
expect(this.EmailHandler.sendEmail).to.have.been.called
this.auditLog
)
expect(
this.UserAuditLogHandler.promises.addEntry
).to.have.been.calledOnce.and.calledWithMatch(
this.user._id,
'unlink-institution-sso',
this.auditLog.initiatorId,
this.auditLog.ipAddress,
{
institutionEmail: linkedEmail,
providerId: '1',
providerName: 'Overleaf University'
}
)
})
it('should remove the identifier', async function() {
await this.SAMLIdentityManager.unlinkAccounts(
this.user._id,
linkedEmail,
this.user.email,
'1',
'Overleaf University',
this.auditLog
)
const query = {
_id: this.user._id
}
const update = {
$pull: {
samlIdentifiers: {
providerId: '1'
}
}
}
expect(this.User.update).to.have.been.calledOnce.and.calledWithMatch(
query,
update
)
})
it('should send an email notification', async function() {
await this.SAMLIdentityManager.unlinkAccounts(
this.user._id,
linkedEmail,
this.user.email,
'1',
'Overleaf University',
this.auditLog
)
expect(this.User.update).to.have.been.called
expect(this.EmailHandler.sendEmail).to.have.been.calledOnce
const emailArgs = this.EmailHandler.sendEmail.lastCall.args
expect(emailArgs[0]).to.equal('securityAlert')
expect(emailArgs[1].to).to.equal(this.user.email)
})
describe('errors', function() {
it('should pass back errors via UserAuditLogHandler', async function() {
let error
const anError = new Error('oops')
this.UserAuditLogHandler.promises.addEntry.rejects(anError)
try {
await this.SAMLIdentityManager.unlinkAccounts(
this.user._id,
linkedEmail,
this.user.email,
'1',
'Overleaf University',
this.auditLog
)
} catch (e) {
error = e
} finally {
expect(error).to.exist
expect(error).to.equal(anError)
expect(this.EmailHandler.sendEmail).to.not.have.been.called
expect(this.User.update).to.not.have.been.called
}
})
})
})
describe('entitlementAttributeMatches', function() {
@@ -66,11 +66,6 @@ describe('UserEmailsController', function() {
}
}),
'../Helpers/EmailHelper': this.EmailHelper,
'./UserAuditLogHandler': (this.UserAuditLogHandler = {
promises: {
addEntry: sinon.stub().resolves()
}
}),
'./UserEmailsConfirmationHandler': (this.UserEmailsConfirmationHandler = {
promises: {
sendConfirmationEmail: sinon.stub().resolves()
@@ -121,16 +116,14 @@ describe('UserEmailsController', function() {
.yields()
})
it('adds an entry to user audit log', function(done) {
it('passed audit log to addEmailAddress', function(done) {
this.res.sendStatus = sinon.stub()
this.res.sendStatus.callsFake(() => {
this.UserAuditLogHandler.promises.addEntry.should.have.been.calledWith(
this.user._id,
'add-email',
this.user._id,
this.req.ip,
{ newSecondaryEmail: this.newEmail }
)
const addCall = this.UserUpdater.promises.addEmailAddress.lastCall
expect(addCall.args[3]).to.deep.equal({
initiatorId: this.user._id,
ipAddress: this.req.ip
})
done()
})
this.UserEmailsController.add(this.req, this.res)
@@ -246,22 +239,6 @@ describe('UserEmailsController', function() {
})
this.UserEmailsController.add(this.req, this.res, this.next)
})
describe('errors', function() {
describe('via UserAuditLogHandler', function() {
beforeEach(function() {
this.UserAuditLogHandler.promises.addEntry.throws('oops')
})
it('should not add email and should return error', function(done) {
this.UserEmailsController.add(this.req, this.res, error => {
expect(error).to.exist
this.UserUpdater.promises.addEmailAddress.should.not.have.been
.called
done()
})
})
})
})
})
describe('remove', function() {
@@ -21,8 +21,8 @@ describe('UserUpdater', function() {
this.UserGetter = {
getUserEmail: sinon.stub(),
getUserByAnyEmail: sinon.stub(),
ensureUniqueEmailAddress: sinon.stub(),
promises: {
ensureUniqueEmailAddress: sinon.stub(),
getUser: sinon.stub()
}
}
@@ -55,10 +55,13 @@ describe('UserUpdater', function() {
timeAsyncMethod: sinon.stub()
},
'./UserGetter': this.UserGetter,
'../Institutions/InstitutionsAPI': {
'../Institutions/InstitutionsAPI': (this.InstitutionsAPI = {
addAffiliation: this.addAffiliation,
removeAffiliation: this.removeAffiliation
},
removeAffiliation: this.removeAffiliation,
promises: {
addAffiliation: sinon.stub()
}
}),
'../Email/EmailHandler': (this.EmailHandler = {
promises: {
sendEmail: sinon.stub()
@@ -140,7 +143,7 @@ describe('UserUpdater', function() {
ipAddress: '0:0:0:0'
}
this.UserGetter.getUserEmail.callsArgWith(1, null, this.stubbedUser.email)
this.UserUpdater.addEmailAddress = sinon.stub().callsArgWith(2)
this.UserUpdater.addEmailAddress = sinon.stub().callsArgWith(4)
this.UserUpdater.setDefaultEmailAddress = sinon.stub().yields()
this.UserUpdater.removeEmailAddress = sinon.stub().callsArgWith(2)
})
@@ -153,7 +156,7 @@ describe('UserUpdater', function() {
err => {
should.not.exist(err)
this.UserUpdater.addEmailAddress
.calledWith(this.stubbedUser._id, this.newEmail)
.calledWith(this.stubbedUser._id, this.newEmail, {}, this.auditLog)
.should.equal(true)
this.UserUpdater.setDefaultEmailAddress
.calledWith(
@@ -200,23 +203,29 @@ describe('UserUpdater', function() {
describe('addEmailAddress', function() {
beforeEach(function() {
this.UserGetter.ensureUniqueEmailAddress = sinon.stub().callsArgWith(1)
this.UserUpdater.updateUser = sinon.stub().callsArgWith(2, null)
this.UserGetter.promises.ensureUniqueEmailAddress = sinon
.stub()
.resolves()
this.UserUpdater.promises.updateUser = sinon.stub().resolves()
})
it('add email', function(done) {
this.UserUpdater.addEmailAddress(
this.stubbedUser._id,
this.newEmail,
{},
{ initiatorId: this.stubbedUser._id, ipAddress: '127:0:0:0' },
err => {
this.UserGetter.ensureUniqueEmailAddress.called.should.equal(true)
should.not.exist(err)
this.UserGetter.promises.ensureUniqueEmailAddress.called.should.equal(
true
)
expect(err).to.not.exist
const reversedHostname = this.newEmail
.split('@')[1]
.split('')
.reverse()
.join('')
this.UserUpdater.updateUser
this.UserUpdater.promises.updateUser
.calledWith(this.stubbedUser._id, {
$push: {
emails: {
@@ -242,10 +251,13 @@ describe('UserUpdater', function() {
this.stubbedUser._id,
this.newEmail,
affiliationOptions,
{ initiatorId: this.stubbedUser._id, ipAddress: '127:0:0:0' },
err => {
should.not.exist(err)
this.addAffiliation.calledOnce.should.equal(true)
const { args } = this.addAffiliation.lastCall
this.InstitutionsAPI.promises.addAffiliation.calledOnce.should.equal(
true
)
const { args } = this.InstitutionsAPI.promises.addAffiliation.lastCall
args[0].should.equal(this.stubbedUser._id)
args[1].should.equal(this.newEmail)
args[2].should.equal(affiliationOptions)
@@ -255,22 +267,79 @@ describe('UserUpdater', function() {
})
it('handle affiliation error', function(done) {
this.addAffiliation.callsArgWith(3, new Error('nope'))
this.InstitutionsAPI.promises.addAffiliation.rejects(new Error('nope'))
this.UserUpdater.addEmailAddress(
this.stubbedUser._id,
this.newEmail,
{},
{ initiatorId: this.stubbedUser._id, ipAddress: '127:0:0:0' },
err => {
should.exist(err)
this.UserUpdater.updateUser.called.should.equal(false)
this.UserUpdater.promises.updateUser.called.should.equal(false)
done()
}
)
})
it('validates email', function(done) {
this.UserUpdater.addEmailAddress(this.stubbedUser._id, 'bar', err => {
should.exist(err)
done()
this.UserUpdater.addEmailAddress(
this.stubbedUser._id,
'bar',
{},
{ initiatorId: this.stubbedUser._id, ipAddress: '127:0:0:0' },
err => {
should.exist(err)
done()
}
)
})
it('updates the audit log', function(done) {
this.ip = '127:0:0:0'
this.UserUpdater.addEmailAddress(
this.stubbedUser._id,
this.newEmail,
{},
{ initiatorId: this.stubbedUser._id, ipAddress: this.ip },
error => {
expect(error).to.not.exist
this.InstitutionsAPI.promises.addAffiliation.calledOnce.should.equal(
true
)
const { args } = this.UserAuditLogHandler.promises.addEntry.lastCall
expect(args[0]).to.equal(this.stubbedUser._id)
expect(args[1]).to.equal('add-email')
expect(args[2]).to.equal(this.stubbedUser._id)
expect(args[3]).to.equal(this.ip)
expect(args[4]).to.deep.equal({
newSecondaryEmail: this.newEmail
})
done()
}
)
})
describe('errors', function() {
describe('via UserAuditLogHandler', function() {
const anError = new Error('oops')
beforeEach(function() {
this.UserAuditLogHandler.promises.addEntry.throws(anError)
})
it('should not add email and should return error', function(done) {
this.UserUpdater.addEmailAddress(
this.stubbedUser._id,
this.newEmail,
{},
{ initiatorId: this.stubbedUser._id, ipAddress: '127:0:0:0' },
error => {
expect(error).to.exist
expect(error).to.equal(anError)
expect(this.UserUpdater.promises.updateUser).to.not.have.been
.called
done()
}
)
})
})
})
})