|
|
@@ -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() {
|
|
|
+ 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() {
|