diff --git a/.github/workflows/e2e-ui-tests.yml b/.github/workflows/e2e-ui-tests.yml index f99db48ee0..92491fca43 100644 --- a/.github/workflows/e2e-ui-tests.yml +++ b/.github/workflows/e2e-ui-tests.yml @@ -2,9 +2,9 @@ name: Run E2E Backend + Frontend Tests on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build: @@ -19,54 +19,58 @@ jobs: POSTGRES_PASSWORD: postgres ports: - 5432:5432 + ldap: + image: rroemhild/test-openldap + ports: + - 10389:10389 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Cache node modules - uses: actions/cache@v2 - env: - cache-name: cache-node-modules - with: - # npm cache files are stored in `~/.npm` on Linux/macOS - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- - - name: Setup Node.js - uses: actions/setup-node@v1 - with: - node-version: '14.x' + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: "14.x" - - name: Install project dependencies - run: yarn install --frozen-lockfile + - name: Install project dependencies + run: yarn install --frozen-lockfile - - name: Copy .env-ci to .env - run: cp apps/backend/test/.env-ci apps/backend/.env + - name: Copy .env-ci to .env + run: cp apps/backend/test/.env-ci apps/backend/.env - - name: Create/migrate db - run: | - yarn backend sequelize-cli db:create - yarn backend sequelize-cli db:migrate - yarn backend sequelize-cli db:seed:all + - name: Create/migrate db + run: | + yarn backend sequelize-cli db:create + yarn backend sequelize-cli db:migrate + yarn backend sequelize-cli db:seed:all - - name: Build Heimdall - run: yarn build - env: - NODE_ENV: production + - name: Build Heimdall + run: yarn build + env: + NODE_ENV: production - - name: Cypress run - uses: cypress-io/github-action@v2 - with: - start: yarn start - wait-on: http://127.0.0.1:3000 + - name: Cypress run + uses: cypress-io/github-action@v2 + with: + start: yarn start + wait-on: http://127.0.0.1:3000 - - name: Upload test screenshots - if: failure() - uses: actions/upload-artifact@master - with: - name: cypress-screenshots - path: test/screenshots + - name: Upload test screenshots + if: failure() + uses: actions/upload-artifact@master + with: + name: cypress-screenshots + path: test/screenshots diff --git a/README.md b/README.md index f9a8855f32..e0e363028a 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Proper API documentation does not exist yet. In the meantime here are quick inst ```sh # Create a user (only needs to be done once) -curl -X POST -H "Content-Type: application/json" -d '{"email": "user@example.com", "password": "password", "passwordConfirmation": "password", "role": "user" }' http://localhost:3000/users +curl -X POST -H "Content-Type: application/json" -d '{"email": "user@example.com", "password": "password", "passwordConfirmation": "password", "role": "user", "creationMethod": "local" }' http://localhost:3000/users # Log in curl -X POST -H "Content-Type: application/json" -d '{"email": "user@example.com", "password": "password" }' http://localhost:3000/authn/login # The previous command returns a Bearer Token that needs to get placed in the following command diff --git a/apps/backend/.env-example b/apps/backend/.env-example index 6f2824b1c5..c7f3a0d8fc 100644 --- a/apps/backend/.env-example +++ b/apps/backend/.env-example @@ -10,3 +10,18 @@ JWT_EXPIRE_TIME= HEIMDALL_HEADLESS_TESTS= ADMIN_PASSWORD= + +# LDAP Configuration +LDAP_ENABLED= +LDAP_HOST= +LDAP_PORT= +LDAP_BINDDN= +LDAP_PASSWORD= +# Here you set your LDAP searchbase, for more info see https://docs.oracle.com/cd/E19693-01/819-0997/auto45/index.html +# If you're using Active Directory, you probably want "OU=Users, DC=, DC=local" +LDAP_SEARCHBASE="" +# Here you set your LDAP search filter, for more info see https://confluence.atlassian.com/kb/how-to-write-ldap-search-filters-792496933.html +# If you are using Active Directory Users, you probably want "sAMAccountName={{username}}" +LDAP_SEARCHFILTER="" +LDAP_NAMEFIELD="" +LDAP_MAILFIELD="" diff --git a/apps/backend/config/app_config.ts b/apps/backend/config/app_config.ts index 75e4888b6a..ce0506c4d7 100644 --- a/apps/backend/config/app_config.ts +++ b/apps/backend/config/app_config.ts @@ -64,8 +64,8 @@ export default class AppConfig { dialectOptions: { ssl: Boolean(this.get('DATABASE_SSL')) ? { - rejectUnauthorized: false - } + rejectUnauthorized: false + } : false }, ssl: Boolean(this.get('DATABASE_SSL')) || false diff --git a/apps/backend/migrations/20210128142318-add_account_creation_method_to_users.js b/apps/backend/migrations/20210128142318-add_account_creation_method_to_users.js new file mode 100644 index 0000000000..a2b3be9d87 --- /dev/null +++ b/apps/backend/migrations/20210128142318-add_account_creation_method_to_users.js @@ -0,0 +1,24 @@ +'use strict'; + +const sequelize = require("sequelize"); + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => { + return Promise.all([ + queryInterface.addColumn('Users', 'creationMethod', { + type: sequelize.STRING, + defaultValue: 'local' + }) + ]) + }) + }, + + down: async (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => { + return Promise.all([ + queryInterface.removeColumn('Users', 'creationMethod', { transaction: t }) + ]) + }) + } +}; diff --git a/apps/backend/package.json b/apps/backend/package.json index c40dd02db2..7a18faa8f2 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -68,6 +68,7 @@ "js-levenshtein": "^1.1.6", "passport": "^0.4.1", "passport-jwt": "^4.0.0", + "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", "pg": "^8.2.1", "reflect-metadata": "^0.1.13", diff --git a/apps/backend/src/authn/authn.controller.ts b/apps/backend/src/authn/authn.controller.ts index 5e866c9aa9..bfa882568f 100644 --- a/apps/backend/src/authn/authn.controller.ts +++ b/apps/backend/src/authn/authn.controller.ts @@ -1,4 +1,5 @@ import {Controller, Post, Req, UseGuards} from '@nestjs/common'; +import {AuthGuard} from '@nestjs/passport'; import {Request} from 'express'; import {LocalAuthGuard} from '../guards/local-auth.guard'; import {User} from '../users/user.model'; @@ -10,7 +11,17 @@ export class AuthnController { @UseGuards(LocalAuthGuard) @Post('login') - async login(@Req() req: Request): Promise { + async login( + @Req() req: Request + ): Promise<{userID: string; accessToken: string}> { + return this.authnService.login(req.user as User); + } + + @UseGuards(AuthGuard('ldap')) + @Post('login/ldap') + async loginToLDAP( + @Req() req: Request + ): Promise<{userID: string; accessToken: string}> { return this.authnService.login(req.user as User); } } diff --git a/apps/backend/src/authn/authn.module.ts b/apps/backend/src/authn/authn.module.ts index b5f6b8dfad..6e3f361ae0 100644 --- a/apps/backend/src/authn/authn.module.ts +++ b/apps/backend/src/authn/authn.module.ts @@ -6,11 +6,12 @@ import {UsersModule} from '../users/users.module'; import {AuthnController} from './authn.controller'; import {AuthnService} from './authn.service'; import {JwtStrategy} from './jwt.strategy'; +import {LDAPStrategy} from './ldap.strategy'; import {LocalStrategy} from './local.strategy'; @Module({ imports: [UsersModule, PassportModule, TokenModule, ConfigModule], - providers: [AuthnService, LocalStrategy, JwtStrategy], + providers: [AuthnService, LocalStrategy, JwtStrategy, LDAPStrategy], controllers: [AuthnController] }) export class AuthnModule {} diff --git a/apps/backend/src/authn/authn.service.ts b/apps/backend/src/authn/authn.service.ts index 2078decefa..3a24884c90 100644 --- a/apps/backend/src/authn/authn.service.ts +++ b/apps/backend/src/authn/authn.service.ts @@ -1,6 +1,9 @@ import {Injectable, UnauthorizedException} from '@nestjs/common'; import {JwtService} from '@nestjs/jwt'; import {compare} from 'bcrypt'; +import * as crypto from 'crypto'; +import {ConfigService} from '../config/config.service'; +import {CreateUserDto} from '../users/dto/create-user.dto'; import {User} from '../users/user.model'; import {UsersService} from '../users/users.service'; @@ -8,10 +11,11 @@ import {UsersService} from '../users/users.service'; export class AuthnService { constructor( private usersService: UsersService, + private readonly configService: ConfigService, private jwtService: JwtService ) {} - async validateUser(email: string, password: string): Promise { + async validateUser(email: string, password: string): Promise { let user: User; try { user = await this.usersService.findByEmail(email); @@ -26,6 +30,46 @@ export class AuthnService { } } + async validateOrCreateUser( + email: string, + firstName: string, + lastName: string, + creationMethod: string + ): Promise { + let user: User; + try { + user = await this.usersService.findByEmail(email); + } catch { + const randomPass = crypto.randomBytes(128).toString('hex'); + const createUser: CreateUserDto = { + email: email, + password: randomPass, + passwordConfirmation: randomPass, + firstName: firstName, + lastName: lastName, + organization: '', + title: '', + role: 'user', + creationMethod: creationMethod + }; + await this.usersService.create(createUser); + user = await this.usersService.findByEmail(email); + } + + if (user) { + // If the users info has changed since they last logged in it will be reflected here. + // Because we find the user by their email, we can't detect a change in email. + if (user.firstName !== firstName || user.lastName !== lastName) { + user.firstName = firstName; + user.lastName = lastName; + user.save(); + } + this.usersService.updateLoginMetadata(user); + } + + return user; + } + async login(user: { id: string; email: string; @@ -51,4 +95,12 @@ export class AuthnService { }; } } + + splitName(fullName: string): {firstName: string; lastName: string} { + const nameArray = fullName.split(' '); + return { + firstName: nameArray.slice(0, -1).join(' '), + lastName: nameArray[nameArray.length - 1] + }; + } } diff --git a/apps/backend/src/authn/ldap.strategy.ts b/apps/backend/src/authn/ldap.strategy.ts new file mode 100644 index 0000000000..b0aba70f02 --- /dev/null +++ b/apps/backend/src/authn/ldap.strategy.ts @@ -0,0 +1,47 @@ +import {Injectable} from '@nestjs/common'; +import {PassportStrategy} from '@nestjs/passport'; +import {Request} from 'express'; +import Strategy from 'passport-ldapauth'; +import {ConfigService} from '../config/config.service'; +import {AuthnService} from './authn.service'; + +@Injectable() +export class LDAPStrategy extends PassportStrategy(Strategy, 'ldap') { + constructor( + private readonly authnService: AuthnService, + private readonly configService: ConfigService + ) { + super( + { + passReqToCallback: true, + server: { + url: `ldap://${configService.get('LDAP_HOST')}:${ + configService.get('LDAP_PORT') || 389 + }`, + bindDN: configService.get('LDAP_BINDDN'), + bindCredentials: configService.get('LDAP_PASSWORD'), + searchBase: configService.get('LDAP_SEARCHBASE') || 'disabled', + searchFilter: + configService.get('LDAP_SEARCHFILTER') || + '(sAMAccountName={{username}})', + passReqToCallback: true + } + }, + async (req: Request, user: any, done: any) => { + const {firstName, lastName} = this.authnService.splitName( + user[configService.get('LDAP_NAMEFIELD') || 'name'] + ); + const email: string = + user[configService.get('LDAP_MAILFIELD') || 'mail']; + + req.user = this.authnService.validateOrCreateUser( + email, + firstName, + lastName, + 'ldap' + ); + return done(null, req.user); + } + ); + } +} diff --git a/apps/backend/src/config/config.service.ts b/apps/backend/src/config/config.service.ts index 3b882a6f49..c0dba4d11c 100644 --- a/apps/backend/src/config/config.service.ts +++ b/apps/backend/src/config/config.service.ts @@ -10,7 +10,10 @@ export class ConfigService { } frontendStartupSettings(): StartupSettingsDto { - return new StartupSettingsDto({banner: this.get('WARNING_BANNER') || ''}); + return new StartupSettingsDto({ + banner: this.get('WARNING_BANNER') || '', + ldap: this.get('LDAP_ENABLED')?.toLocaleLowerCase() === 'true' || false + }); } getDbConfig(): SequelizeOptions { diff --git a/apps/backend/src/config/dto/startup-settings.dto.ts b/apps/backend/src/config/dto/startup-settings.dto.ts index 274c576dbb..07ed3572d6 100644 --- a/apps/backend/src/config/dto/startup-settings.dto.ts +++ b/apps/backend/src/config/dto/startup-settings.dto.ts @@ -2,8 +2,10 @@ import {IStartupSettings} from '@heimdall/interfaces'; export class StartupSettingsDto implements IStartupSettings { readonly banner: string; + readonly ldap: boolean; constructor(settings: IStartupSettings) { this.banner = settings.banner; + this.ldap = settings.ldap; } } diff --git a/apps/backend/src/users/dto/create-user.dto.ts b/apps/backend/src/users/dto/create-user.dto.ts index f5b6ce9795..b8a9d7b783 100644 --- a/apps/backend/src/users/dto/create-user.dto.ts +++ b/apps/backend/src/users/dto/create-user.dto.ts @@ -34,4 +34,9 @@ export class CreateUserDto implements ICreateUser { @IsString() @IsIn(['user']) readonly role!: string; + + @IsNotEmpty() + @IsString() + @IsIn(['local', 'ldap', 'oauth']) + readonly creationMethod!: string; } diff --git a/apps/backend/src/users/dto/user.dto.ts b/apps/backend/src/users/dto/user.dto.ts index f05041eafd..5f4a8b4725 100644 --- a/apps/backend/src/users/dto/user.dto.ts +++ b/apps/backend/src/users/dto/user.dto.ts @@ -11,6 +11,7 @@ export class UserDto implements IUser { readonly organization: string | undefined; readonly loginCount: number; readonly lastLogin: Date | undefined; + readonly creationMethod: string; readonly createdAt: Date; readonly updatedAt: Date; @@ -24,6 +25,7 @@ export class UserDto implements IUser { this.organization = user.organization; this.loginCount = user.loginCount; this.lastLogin = user.lastLogin; + this.creationMethod = user.creationMethod; this.createdAt = user.createdAt; this.updatedAt = user.updatedAt; } diff --git a/apps/backend/src/users/user.model.ts b/apps/backend/src/users/user.model.ts index 93d20020b8..ec9da21d42 100644 --- a/apps/backend/src/users/user.model.ts +++ b/apps/backend/src/users/user.model.ts @@ -69,6 +69,10 @@ export class User extends Model { @Column(DataType.STRING) role!: string; + @AllowNull(false) + @Column(DataType.STRING) + creationMethod!: string; + @CreatedAt @AllowNull(false) @Column(DataType.DATE) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 146ff8947c..6fd2750a06 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -47,6 +47,7 @@ export class UsersService { user.title = createUserDto.title || undefined; user.organization = createUserDto.organization || undefined; user.role = createUserDto.role; + user.creationMethod = createUserDto.creationMethod; try { user.encryptedPassword = await hash(createUserDto.password, 14); } catch { diff --git a/apps/backend/test/.env-ci b/apps/backend/test/.env-ci index e53e5e703e..dacaea07ee 100644 --- a/apps/backend/test/.env-ci +++ b/apps/backend/test/.env-ci @@ -5,3 +5,12 @@ DATABASE_PASSWORD=postgres DATABASE_NAME=heimdallts_jest_testing_service_db JWT_SECRET=abc123 NODE_ENV=test + +LDAP_ENABLED=true +LDAP_HOST=localhost +LDAP_PORT=10389 +LDAP_BINDDN=cn=admin,dc=planetexpress,dc=com +LDAP_PASSWORD=GoodNewsEveryone +LDAP_SEARCHBASE=ou=people,dc=planetexpress,dc=com +LDAP_SEARCHFILTER=(uid={{username}}) +LDAP_NAMEFIELD=cn diff --git a/apps/backend/test/constants/users-test.constant.ts b/apps/backend/test/constants/users-test.constant.ts index af28de996a..aab9c26a96 100644 --- a/apps/backend/test/constants/users-test.constant.ts +++ b/apps/backend/test/constants/users-test.constant.ts @@ -15,6 +15,11 @@ export const LOGIN_AUTHENTICATION = { password: 'LETmeiN123$$$tP' }; +export const LDAP_AUTHENTICATION = { + username: 'fry', + password: 'fry' +}; + export const ADMIN_LOGIN_AUTHENTICATION = { email: 'admin@yahoo.com', password: 'LETmeiN123$$$tP' @@ -25,6 +30,11 @@ export const BAD_LOGIN_AUTHENTICATION = { password: 'Invalid_password' }; +export const BAD_LDAP_AUTHENTICATION = { + username: 'fry', + password: 'zoiderg' +}; + // @ts-ignore export const TEST_USER: User = { email: 'abc@yahoo.com', @@ -201,7 +211,8 @@ export const CREATE_USER_DTO_TEST_OBJ: CreateUserDto = { lastName: 'Dummy', title: 'fake title', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; export const CREATE_ADMIN_DTO: CreateUserDto = { @@ -212,7 +223,8 @@ export const CREATE_ADMIN_DTO: CreateUserDto = { lastName: 'Dummy', title: 'Admin', organization: 'Fake Org', - role: 'admin' + role: 'admin', + creationMethod: 'local' }; export const CREATE_SECOND_ADMIN_DTO: CreateUserDto = { @@ -228,7 +240,8 @@ export const CREATE_USER_DTO_TEST_OBJ_2: CreateUserDto = { lastName: 'Dummy', title: 'fake title', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; export const CREATE_USER_DTO_TEST_OBJ_WITH_UNMATCHING_PASSWORDS: CreateUserDto = { @@ -239,7 +252,8 @@ export const CREATE_USER_DTO_TEST_OBJ_WITH_UNMATCHING_PASSWORDS: CreateUserDto = lastName: 'Dummy', title: 'fake title', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; // @ts-ignore @@ -250,7 +264,8 @@ export const CREATE_USER_DTO_TEST_OBJ_WITH_MISSING_FIRST_NAME: CreateUserDto = { lastName: 'Dummy', title: 'fake title', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; // @ts-ignore @@ -261,7 +276,8 @@ export const CREATE_USER_DTO_TEST_OBJ_WITH_MISSING_LAST_NAME: CreateUserDto = { firstName: 'Test', title: 'fake title', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; // @ts-ignore @@ -272,7 +288,8 @@ export const CREATE_USER_DTO_TEST_OBJ_WITH_MISSING_ORGANIZATION: CreateUserDto = firstName: 'Test', lastName: 'Dummy', title: 'fake title', - role: 'user' + role: 'user', + creationMethod: 'local' }; // @ts-ignore @@ -283,7 +300,8 @@ export const CREATE_USER_DTO_TEST_OBJ_WITH_MISSING_TITLE: CreateUserDto = { firstName: 'Test', lastName: 'Dummy', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; // @ts-ignore @@ -294,7 +312,8 @@ export const CREATE_USER_DTO_TEST_OBJ_WITH_MISSING_EMAIL_FIELD: CreateUserDto = lastName: 'Dummy', title: 'fake title', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; // @ts-ignore @@ -306,7 +325,8 @@ export const CREATE_USER_DTO_TEST_OBJ_WITH_INVALID_EMAIL_FIELD: CreateUserDto = lastName: 'Dummy', title: 'fake title', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; // @ts-ignore @@ -317,7 +337,8 @@ export const CREATE_USER_DTO_TEST_OBJ_WITH_MISSING_PASSWORD_FIELD: CreateUserDto lastName: 'Dummy', title: 'fake title', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; // @ts-ignore @@ -328,7 +349,8 @@ export const CREATE_USER_DTO_TEST_OBJ_WITH_MISSING_PASSWORD_CONFIRMATION_FIELD: lastName: 'Dummy', title: 'fake title', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; // @ts-ignore @@ -351,7 +373,8 @@ export const CREATE_USER_DTO_TEST_OBJ_WITH_INVALID_PASSWORD: CreateUserDto = { lastName: 'Dummy', title: 'fake title', organization: 'Fake Org', - role: 'user' + role: 'user', + creationMethod: 'local' }; export const UPDATE_USER_DTO_TEST_OBJ: UpdateUserDto = { diff --git a/apps/frontend/src/components/global/UserModal.vue b/apps/frontend/src/components/global/UserModal.vue index 78561a5578..d715aaa232 100644 --- a/apps/frontend/src/components/global/UserModal.vue +++ b/apps/frontend/src/components/global/UserModal.vue @@ -5,8 +5,10 @@ - - + + Some of the settings are managed by your identity provider. + + @@ -31,6 +34,7 @@ @@ -40,6 +44,7 @@ - + -
+
- Change Password
@@ -86,6 +102,7 @@ id="new_password_field" ref="newPassword" v-model="newPassword" + :disabled="update_unavailable" :error-messages=" requiredFieldError($v.newPassword, 'New Password') " @@ -98,6 +115,7 @@ id="repeat_password_field" ref="repeatPassword" v-model="passwordConfirmation" + :disabled="update_unavailable" :error-messages=" requiredFieldError($v.passwordConfirmation, 'Repeat Password') " @@ -126,6 +144,7 @@ id="closeAndSaveChanges" color="primary" text + :disabled="update_unavailable" @click="updateUserInfo" >Save Changes @@ -142,7 +161,7 @@ import {ServerModule} from '@/store/server'; import {SnackbarModule} from '@/store/snackbar'; import {IUser, IUpdateUser} from '@heimdall/interfaces'; import UserValidatorMixin from '@/mixins/UserValidatorMixin'; -import {required, email, requiredIf, requiredUnless} from 'vuelidate/lib/validators'; +import {required, email, requiredIf} from 'vuelidate/lib/validators'; import {Prop} from 'vue-property-decorator'; @Component({ @@ -155,7 +174,9 @@ import {Prop} from 'vue-property-decorator'; } }, currentPassword: { - required: requiredUnless('admin') + required: requiredIf(function(userInfo){ + return (userInfo.user.role == 'admin') + }) }, newPassword: { required: requiredIf('changePassword') @@ -229,6 +250,10 @@ export default class UserModal extends Vue { this.changePassword = !this.changePassword } + get update_unavailable() { + return this.userInfo.creationMethod == 'ldap'; + } + get title(): string { if(this.admin) { return `Update account information for ${this.user.email}` diff --git a/apps/frontend/src/components/global/login/LDAPLogin.vue b/apps/frontend/src/components/global/login/LDAPLogin.vue new file mode 100644 index 0000000000..9302ed3591 --- /dev/null +++ b/apps/frontend/src/components/global/login/LDAPLogin.vue @@ -0,0 +1,87 @@ + + + diff --git a/apps/frontend/src/components/global/login/LocalLogin.vue b/apps/frontend/src/components/global/login/LocalLogin.vue new file mode 100644 index 0000000000..4913e8e751 --- /dev/null +++ b/apps/frontend/src/components/global/login/LocalLogin.vue @@ -0,0 +1,98 @@ + + diff --git a/apps/frontend/src/store/server.ts b/apps/frontend/src/store/server.ts index 70f3c3d887..e74b82bac1 100644 --- a/apps/frontend/src/store/server.ts +++ b/apps/frontend/src/store/server.ts @@ -19,6 +19,7 @@ export interface IServerState { loading: boolean; token: string; banner: string; + ldap: boolean; userInfo: IUser; } @@ -30,6 +31,7 @@ export interface IServerState { }) class Server extends VuexModule implements IServerState { banner = ''; + ldap = false; serverUrl = ''; serverMode = false; loading = true; @@ -46,6 +48,7 @@ class Server extends VuexModule implements IServerState { organization: '', loginCount: -1, lastLogin: undefined, + creationMethod: '', createdAt: new Date(), updatedAt: new Date() }; @@ -66,6 +69,7 @@ class Server extends VuexModule implements IServerState { @Mutation SET_STARTUP_SETTINGS(settings: IStartupSettings) { this.banner = settings.banner; + this.ldap = settings.ldap; } @Mutation @@ -137,12 +141,24 @@ class Server extends VuexModule implements IServerState { }); } + @Action + public async handleLogin(data: {userID: string; accessToken: string}) { + this.context.commit('SET_USERID', data.userID); + this.context.commit('SET_TOKEN', data.accessToken); + this.GetUserInfo(); + } + @Action({rawError: true}) public async Login(userInfo: {email: string; password: string}) { - return axios.post('/authn/login', userInfo).then(({data}) => { - this.context.commit('SET_TOKEN', data.accessToken); - this.context.commit('SET_USERID', data.userID); - this.GetUserInfo(); + return axios.post('/authn/login', userInfo).then((response) => { + this.handleLogin(response.data); + }); + } + + @Action({rawError: true}) + public async LoginLDAP(userInfo: {username: string; password: string}) { + return axios.post('/authn/login/ldap', userInfo).then((response) => { + this.handleLogin(response.data); }); } @@ -151,6 +167,7 @@ class Server extends VuexModule implements IServerState { email: string; password: string; passwordConfirmation: string; + creationMethod: string; }) { return axios.post('/users', userInfo); } diff --git a/apps/frontend/src/views/Login.vue b/apps/frontend/src/views/Login.vue index d6a6760850..0f2aed1f02 100644 --- a/apps/frontend/src/views/Login.vue +++ b/apps/frontend/src/views/Login.vue @@ -4,61 +4,37 @@ - + Login to Heimdall Server - - - - - - Login - - - - - -
- - Sign Up - -
-
+ + Heimdall Login + Organization Login + + + + + + + +
@@ -69,31 +45,21 @@ diff --git a/apps/frontend/src/views/Signup.vue b/apps/frontend/src/views/Signup.vue index 84fb9deec7..a349ea3b7d 100644 --- a/apps/frontend/src/views/Signup.vue +++ b/apps/frontend/src/views/Signup.vue @@ -113,6 +113,7 @@ export interface SignupHash { password: string; passwordConfirmation: string; role: string; + creationMethod: string; } @Component({ @@ -148,7 +149,8 @@ export default class Signup extends Vue { email: this.email, password: this.password, passwordConfirmation: this.passwordConfirmation, - role: 'user' + role: 'user', + creationMethod: 'local' }; ServerModule.Register(creds) diff --git a/libs/interfaces/config/startup-settings.interface.ts b/libs/interfaces/config/startup-settings.interface.ts index b6ef797d6b..30ae54dd17 100644 --- a/libs/interfaces/config/startup-settings.interface.ts +++ b/libs/interfaces/config/startup-settings.interface.ts @@ -1,3 +1,4 @@ export interface IStartupSettings { readonly banner: string; + readonly ldap: boolean; } diff --git a/libs/interfaces/user/user.interface.ts b/libs/interfaces/user/user.interface.ts index 85faeaab73..0dda577630 100644 --- a/libs/interfaces/user/user.interface.ts +++ b/libs/interfaces/user/user.interface.ts @@ -8,6 +8,7 @@ export interface IUser { readonly organization: string | undefined; readonly loginCount: number; readonly lastLogin: Date | undefined; + readonly creationMethod: string; readonly createdAt: Date; readonly updatedAt: Date; } diff --git a/test/integration/login.spec.ts b/test/integration/login.spec.ts index 3b234b5f00..9b035c733b 100644 --- a/test/integration/login.spec.ts +++ b/test/integration/login.spec.ts @@ -1,8 +1,10 @@ /// import { + BAD_LDAP_AUTHENTICATION, BAD_LOGIN_AUTHENTICATION, CREATE_USER_DTO_TEST_OBJ, + LDAP_AUTHENTICATION, LOGIN_AUTHENTICATION } from '../../apps/backend/test/constants/users-test.constant'; import LoginPage from '../support/pages/LoginPage'; @@ -30,9 +32,20 @@ context('Login', () => { loginPage.login(LOGIN_AUTHENTICATION); toastVerifier.toastTextContains('You have successfully signed in.'); }); + it('authenticates an ldap user with valid credentials', () => { + loginPage.switchToLDAPAuth(); + loginPageVerifier.ldapLoginFormPresent(); + loginPage.ldapLogin(LDAP_AUTHENTICATION); + toastVerifier.toastTextContains('You have successfully signed in.'); + }); it('fails to authenticate a user with invalid credentials', () => { loginPage.login(BAD_LOGIN_AUTHENTICATION); toastVerifier.toastTextContains('Incorrect Username or Password'); }); + it('fails to authenticate an ldap user with invalid credentials', () => { + loginPage.switchToLDAPAuth(); + loginPage.ldapLogin(BAD_LDAP_AUTHENTICATION); + toastVerifier.toastTextContains('Unauthorized'); + }); }); }); diff --git a/test/support/pages/LoginPage.ts b/test/support/pages/LoginPage.ts index 96b4f6b4cb..6ea8f6f30a 100644 --- a/test/support/pages/LoginPage.ts +++ b/test/support/pages/LoginPage.ts @@ -4,4 +4,14 @@ export default class LoginPage { cy.get('input[name=password]').clear().type(user.password); cy.get('#login_button').click(); } + + ldapLogin(user: {username: string; password: string}): void { + cy.get('[data-cy=ldapusername]').clear().type(user.username); + cy.get('[data-cy=ldappassword]').clear().type(user.password); + cy.get('[data-cy=ldapLoginButton]').click(); + } + + switchToLDAPAuth(): void { + cy.get('#select-tab-ldap-login').click(); + } } diff --git a/test/support/verifiers/LoginPageVerifier.ts b/test/support/verifiers/LoginPageVerifier.ts index 657b9c240c..27b67643cd 100644 --- a/test/support/verifiers/LoginPageVerifier.ts +++ b/test/support/verifiers/LoginPageVerifier.ts @@ -5,4 +5,9 @@ export default class LoginPageVerifier { cy.get('label[for=email_field]').should('contain', 'Email'); cy.get('label[for=password_field]').should('contain', 'Password'); } + ldapLoginFormPresent(): void { + cy.get('form[name="login_form"]').should('exist'); + cy.get('label[for=username_field]').should('contain', 'Username'); + cy.get('label[for=password_field]').should('contain', 'Password'); + } } diff --git a/yarn.lock b/yarn.lock index d2250b8c51..fb20ad6041 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3471,6 +3471,13 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/ldapjs@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/ldapjs/-/ldapjs-1.0.9.tgz#1224192d14cc5ab5218fcea72ebb04489c52cb95" + integrity sha512-3PvY7Drp1zoLbcGlothCAkoc5o6Jp9KvUPwHadlHyKp3yPvyeIh7w2zQc9UXMzgDRkoeGXUEODtbEs5XCh9ZyA== + dependencies: + "@types/node" "*" + "@types/lodash@*", "@types/lodash@^4.14.161": version "4.14.168" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" @@ -4556,6 +4563,11 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abstract-logging@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" + integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== + accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -5383,7 +5395,7 @@ asn1.js@^5.2.0: minimalistic-assert "^1.0.0" safer-buffer "^2.1.0" -asn1@~0.2.3: +asn1@^0.2.4, asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== @@ -5783,6 +5795,13 @@ backo2@^1.0.2: resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= +backoff@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/backoff/-/backoff-2.5.0.tgz#f616eda9d3e4b66b8ca7fca79f695722c5f8e26f" + integrity sha1-9hbtqdPktmuMp/ynn2lXIsX44m8= + dependencies: + precond "0.2" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -5826,6 +5845,11 @@ bcrypt@^5.0.0: node-addon-api "^3.0.0" node-pre-gyp "0.15.0" +bcryptjs@^2.4.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= + before-after-hook@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" @@ -13411,6 +13435,37 @@ lazy-ass@^1.6.0: resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= +ldap-filter@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/ldap-filter/-/ldap-filter-0.3.3.tgz#2b14c68a2a9d4104dbdbc910a1ca85fd189e9797" + integrity sha1-KxTGiiqdQQTb28kQocqF/Riel5c= + dependencies: + assert-plus "^1.0.0" + +ldapauth-fork@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ldapauth-fork/-/ldapauth-fork-5.0.1.tgz#18779a9c30371c5bbea02e3b6aaadb60819ad29c" + integrity sha512-EdELQz8zgPruqV2y88PAuAiZCgTaMjex/kEA2PIcOlPYFt75C9QFt5HGZKVQo8Sf/3Mwnr1AtiThHKcq+pRtEg== + dependencies: + "@types/ldapjs" "^1.0.9" + bcryptjs "^2.4.0" + ldapjs "^2.2.1" + lru-cache "^6.0.0" + +ldapjs@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/ldapjs/-/ldapjs-2.2.3.tgz#7ae42c601911c2809f126355a2595ee1d1e21edf" + integrity sha512-143MayI+cSo1PEngge0HMVj3Fb0TneX4Qp9yl9bKs45qND3G64B75GMSxtZCfNuVsvg833aOp1UWG8peFu1LrQ== + dependencies: + abstract-logging "^2.0.0" + asn1 "^0.2.4" + assert-plus "^1.0.0" + backoff "^2.5.0" + ldap-filter "^0.3.3" + once "^1.4.0" + vasync "^2.2.0" + verror "^1.8.1" + left-pad@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" @@ -15642,6 +15697,14 @@ passport-jwt@^4.0.0: jsonwebtoken "^8.2.0" passport-strategy "^1.0.0" +passport-ldapauth@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/passport-ldapauth/-/passport-ldapauth-3.0.1.tgz#1432e8469de18bd46b5b39a46a866b416c1ddded" + integrity sha512-TRRx3BHi8GC8MfCT9wmghjde/EGeKjll7zqHRRfGRxXbLcaDce2OftbQrFG7/AWaeFhR6zpZHtBQ/IkINdLVjQ== + dependencies: + ldapauth-fork "^5.0.1" + passport-strategy "^1.0.0" + passport-local@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" @@ -16344,6 +16407,11 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +precond@0.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" + integrity sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw= + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -19815,12 +19883,19 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +vasync@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/vasync/-/vasync-2.2.0.tgz#cfde751860a15822db3b132bc59b116a4adaf01b" + integrity sha1-z951GGChWCLbOxMrxZsRakra8Bs= + dependencies: + verror "1.10.0" + vendors@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== -verror@1.10.0: +verror@1.10.0, verror@^1.8.1: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=