Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/change exisiting user usertype #359

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/controller/request/user-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ export interface CreateUserRequest extends BaseUserRequest {
* @property {boolean} deleted
* @property {boolean} active
* @property {boolean} extensiveDataProcessing
* @property {string} type
*/
export interface UpdateUserRequest extends Partial<BaseUserRequest> {
active?: boolean;
deleted?: boolean;
extensiveDataProcessing?: boolean
type?: UserType
}


Expand Down
48 changes: 48 additions & 0 deletions src/controller/user-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,22 @@ export default class UserController extends BaseController {
body: { modelName: 'WaiveFinesRequest', allowBlankTarget: true },
},
},
'/:id(\\d+)/tolocaluser':{
POST: {
policy: async (req) => this.roleManager.can(
req.token.roles, 'update', UserController.getRelation(req), 'Authenticator', ['pin'],
),
handler: this.changeToLocalUser.bind(this),
},
},
'/:id(\\d+)/toLocalUser':{
POST: {
policy: async (req) => this.roleManager.can(
req.token.roles, 'update', UserController.getRelation(req), 'Authenticator', ['*'],
),
handler: this.changeToLocalUser.bind(this),
},
},
};
}

Expand Down Expand Up @@ -1650,4 +1666,36 @@ export default class UserController extends BaseController {
this.logger.error(e);
}
}

/**
* POST /users/{id}/tolocalUser
* @summary Change user to a local user
* @tags users - Operations of user controller
* @param {integer} id.path.required - The id of the user
* @operationId toLocalUser
* @security JWT
* @return {UserResponse} 200 - Return changed user
* @return {string} 404 - User not found error.
*/
public async changeToLocalUser(req: RequestWithToken, res:Response): Promise<void> {
const { id: rawId } = req.params;
this.logger.trace('Change user to local user', rawId, 'by', req.token.user);

try {
const id = parseInt(rawId, 10);

const user = await User.findOne({ where: { id:id } });
if (user == null) {
res.status(404).json('Unknown user ID.');
return;
}

const newUserResponse = await UserService.changeToLocalUsers(id);
res.status(200).json(newUserResponse);
} catch (e) {
res.status(500).send();
this.logger.error(e);
}

}
}
86 changes: 86 additions & 0 deletions src/mailer/messages/user-to-local-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* SudoSOS back-end API service.
* Copyright (C) 2024 Study association GEWIS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @license
*/

/**
* This is the module page of the welcome-with-reset.
*
* @module internal/mailer
*/

import MailMessage, { Language, MailLanguageMap } from '../mail-message';
import MailContentBuilder from './mail-content-builder';
import { ResetTokenInfo } from '../../service/authentication-service';

interface UserToLocalUserOptions {
email: string,
resetTokenInfo: ResetTokenInfo,
url?: string;
}

const userToLocalUserDutch = new MailContentBuilder<UserToLocalUserOptions>({
getHTML: (context) => `
<p>Je account is veranderd zodat je zonder GEWIS account kan inloggen.</p>

<p>Voordat je SudoSOS weer kunt gebruiken, dien je een wachtwoord te kiezen door te gaan naar ${context.url + '/passwordreset?token=' + context.resetTokenInfo.password + '&email=' + context.email}</p>

<p>Tot op de borrel!</p>`,
getSubject: 'Welkom bij SudoSOS!',
getTitle: 'Welkom!',
getText: (context) => `
Je account is omgezet zodat je zonder GEWIS account kan inloggen.

Voordat je SudoSOS weer kunt gebruiken, dien je een wachtwoord te kiezen door te gaan naar ${context.url + '/passwordreset?token=' + context.resetTokenInfo.password + '&email=' + context.email}

Tot op de borrel!`,
});

const userToLocalUserEnglish = new MailContentBuilder<UserToLocalUserOptions>({
getHTML: (context) => `
<p>Your account has changed such that you can login without having a GEWIS account.</p>

<p>Before you can actually use SudoSOS again, you have to set a password by going to ${context.url + '/passwordreset?token=' + context.resetTokenInfo.password + '&email=' + context.email}</p>

<p>See you on the borrel!</p>`,
getSubject: 'Welcome to SudoSOS!',
getTitle: 'Welcome!',
getText: (context) => `
Your account has changed such that you can login without having a GEWIS account.

Before you can actually use SudoSOS again, you have to set a password by going to ${context.url + '/passwordreset?token=' + context.resetTokenInfo.password + '&email=' + context.email}

See you on the borrel!`,
});

const mailContents: MailLanguageMap<UserToLocalUserOptions> = {
[Language.DUTCH]: userToLocalUserDutch,
[Language.ENGLISH]: userToLocalUserEnglish,
};

export default class UserToLocalUser extends MailMessage<UserToLocalUserOptions> {
public constructor(options: UserToLocalUserOptions) {
const opt: UserToLocalUserOptions = {
...options,
};
if (!options.url) {
opt.url = process.env.url;
}
super(opt, mailContents);
}
}
13 changes: 9 additions & 4 deletions src/service/authentication-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ import TokenHandler from '../authentication/token-handler';
import RoleManager from '../rbac/role-manager';
import LDAPAuthenticator from '../entity/authenticator/ldap-authenticator';
import MemberAuthenticator from '../entity/authenticator/member-authenticator';
import {
bindUser, getLDAPConnection, getLDAPSettings, LDAPResult, LDAPUser, userFromLDAP,
} from '../helpers/ad';
import { bindUser, getLDAPConnection, getLDAPSettings, LDAPResult, LDAPUser, userFromLDAP } from '../helpers/ad';
import { parseUserToResponse } from '../helpers/revision-to-response';
import HashBasedAuthenticationMethod from '../entity/authenticator/hash-based-authentication-method';
import ResetToken from '../entity/authenticator/reset-token';
Expand All @@ -49,6 +47,7 @@ import NfcAuthenticator from '../entity/authenticator/nfc-authenticator';
import RBACService from './rbac-service';
import Role from '../entity/rbac/role';
import WithManager from '../database/with-manager';
import UserService from './user-service';

export interface AuthenticationContext {
tokenHandler: TokenHandler,
Expand Down Expand Up @@ -308,7 +307,13 @@ export default class AuthenticationService extends WithManager {
const authenticator = await this.manager.findOne(LDAPAuthenticator, { where: { UUID: ADUser.objectGUID }, relations: ['user'] });

// If there is no user associated with the GUID we create the user and bind it.
if (authenticator) return authenticator.user;
if (authenticator) {
if (authenticator.user.type == UserType.LOCAL_USER) {
await UserService.updateUser(authenticator.user.id, { canGoIntoDebt: true, type: UserType.MEMBER });
}

return authenticator.user;
}
return onNewUser(ADUser);
}

Expand Down
17 changes: 17 additions & 0 deletions src/service/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import WelcomeWithReset from '../mailer/messages/welcome-with-reset';
import { Brackets, In } from 'typeorm';
import BalanceService from './balance-service';
import AssignedRole from '../entity/rbac/assigned-role';
import UserToLocalUser from '../mailer/messages/user-to-local-user';

/**
* Parameters used to filter on Get Users functions.
Expand Down Expand Up @@ -291,6 +292,22 @@ export default class UserService {
return true;
}

/**
* Change user to local User
* @param userId - ID of the user to change to local user
*/
public static async changeToLocalUsers(userId: number): Promise<UserResponse> {
const user = await User.findOne({ where: { id: userId } });
if (!user) return undefined;

const resetTokenInfo = await new AuthenticationService().createResetToken(user);
Mailer.getInstance().send(user, new UserToLocalUser({ email: user.email, resetTokenInfo })).then().catch((e) => {
throw e;
});

return UserService.updateUser(userId, { canGoIntoDebt: false, type: UserType.LOCAL_USER });
}

/**
* Combined query to return a users transfers and transactions from the database
* @param user - The user of which to get.
Expand Down
38 changes: 38 additions & 0 deletions test/unit/controller/user-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2592,4 +2592,42 @@ describe('UserController', (): void => {
});
});
});
describe('POST /users/{id}/toLocalUser', ()=> {
it('should return 200 and a correct user response', async () => {
const id = 1;
const user = await User.findOne({ where: { id: id } });
expect(user).to.not.be.null;

const res = await request(ctx.app)
.post(`/users/${id}/toLocalUser`)
.set('Authorization', `Bearer ${ctx.adminToken}`);

expect(res.status).to.be.equal(200);
expect((res.body as UserResponse).type).to.be.equal(UserType.LOCAL_USER);
expect((res.body as UserResponse).canGoIntoDebt).to.be.false;
});
it('should return 403 if not admin', async () => {
const id = 1;
const user = await User.findOne({ where: { id: id } });
expect(user).to.not.be.null;

const res = await request(ctx.app)
.post(`/users/${id}/toLocalUser`)
.set('Authorization', `Bearer ${ctx.userToken}`);

expect(res.status).to.be.equal(403);
});
it('should return 404 if user does not exist', async () => {
const count = await User.count();
const id = count + 1;
const user = await User.findOne({ where: { id } });
expect(user).to.be.null;

const res = await request(ctx.app)
.post(`/users/${id}/toLocalUser`)
.set('Authorization', `Bearer ${ctx.adminToken}`);

expect(res.status).to.be.equal(404);
});
});
});
40 changes: 40 additions & 0 deletions test/unit/service/authentication-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import HashBasedAuthenticationMethod from '../../../src/entity/authenticator/has
import LocalAuthenticator from '../../../src/entity/authenticator/local-authenticator';
import AuthenticationResetTokenRequest from '../../../src/controller/request/authentication-reset-token-request';
import { truncateAllTables } from '../../setup';
import UserService from '../../../src/service/user-service';

export default function userIsAsExpected(user: User | UserResponse, ADResponse: any) {
expect(user.firstName).to.equal(ADResponse.givenName);
Expand Down Expand Up @@ -187,6 +188,45 @@ describe('AuthenticationService', (): void => {

expect(count).to.be.equal(await User.count());
});
it('should login and set user to member again if member can be found in AD', async () => {
const otherValidADUser = {
...ctx.validADUser, givenName: 'Test', objectGUID: Buffer.from('22', 'hex'), sAMAccountName: 'm0041',
};

const clientBindStub = sinon.stub(Client.prototype, 'bind').resolves(null);
const clientSearchStub = sinon.stub(Client.prototype, 'search').resolves({
searchReferences: [],
searchEntries: [otherValidADUser],
});
stubs.push(clientBindStub);
stubs.push(clientSearchStub);

let user: User;
await ctx.connection.transaction(async (manager) => {
const service = new AuthenticationService(manager);
user = await service.LDAPAuthentication('m0041', 'This Is Correct', service.createUserAndBind.bind(service));
});

userIsAsExpected(user, otherValidADUser);

await UserService.changeToLocalUsers(user.id);
expect((await User.findOne({ where: { id: user.id } })).type).to.be.equal(UserType.LOCAL_USER);

let DBUser = await User.findOne(
{ where: { firstName: otherValidADUser.givenName, lastName: otherValidADUser.sn } },
);
expect(DBUser).to.not.be.undefined;

const count = await User.count();

await ctx.connection.transaction(async (manager) => {
const service = new AuthenticationService(manager);
user = await service.LDAPAuthentication('m0041', 'This Is Correct', service.createUserAndBind.bind(service));
});

expect(count).to.be.equal(await User.count());
expect((await User.findOne({ where: { id: user.id } })).type).to.be.equal(UserType.MEMBER);
});
it('should return undefined if wrong password', async () => {
const clientBindStub = sinon.stub(Client.prototype, 'bind').resolves(null);
const clientSearchStub = sinon.stub(Client.prototype, 'search').resolves({
Expand Down
Loading