Skip to content

Commit

Permalink
Release 3.10.0
Browse files Browse the repository at this point in the history
  • Loading branch information
TheSlimvReal authored Aug 26, 2022
2 parents 5ab42d7 + 19a0dff commit 2a63ffb
Show file tree
Hide file tree
Showing 29 changed files with 1,106 additions and 562 deletions.
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"flag-icons": "^6.6.4",
"hammerjs": "^2.0.8",
"json-query": "^2.2.2",
"keycloak-js": "^18.0.1",
"lodash-es": "^4.17.21",
"md5": "^2.3.0",
"moment": "^2.29.4",
Expand Down
18 changes: 18 additions & 0 deletions src/app/core/session/auth/auth-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Available authentication providers.
*/
export enum AuthProvider {
/**
* Default auth provider using CouchDB's `_users` database and permission settings.
* This is the simplest setup as no other service besides the CouchDB is required.
* However, this provider comes with limited functionality.
*/
CouchDB = "couchdb",

/**
* Keycloak is used to authenticate and manage users.
* This requires keycloak and potentially other services to be running.
* Also, the client configuration has to be placed in a file called `keycloak.json` in the `assets` folder.
*/
Keycloak = "keycloak",
}
37 changes: 37 additions & 0 deletions src/app/core/session/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { HttpHeaders } from "@angular/common/http";
import { DatabaseUser } from "../session-service/local-user";

/**
* Abstract class that handles user authentication and password change.
* Implement this for different authentication providers.
* See {@link AuthProvider} for available options.
*/
export abstract class AuthService {
/**
* Authenticate a user with credentials.
* @param username The username of the user
* @param password The password of the user
* @returns Promise that resolves with the user if the login was successful, rejects otherwise.
*/
abstract authenticate(
username: string,
password: string
): Promise<DatabaseUser>;

/**
* Authenticate a user without credentials based on a still valid session.
* @returns Promise that resolves with the user if the session is still valid, rejects otherwise.
*/
abstract autoLogin(): Promise<DatabaseUser>;

/**
* Add headers to requests send by PouchDB if required for authentication.
* @param headers the object where further headers can be added
*/
abstract addAuthHeader(headers: HttpHeaders);

/**
* Clear the local session of the currently logged-in user.
*/
abstract logout(): Promise<void>;
}
111 changes: 111 additions & 0 deletions src/app/core/session/auth/couchdb/couchdb-auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { TestBed } from "@angular/core/testing";

import { CouchdbAuthService } from "./couchdb-auth.service";
import {
HttpClient,
HttpErrorResponse,
HttpStatusCode,
} from "@angular/common/http";
import { of, throwError } from "rxjs";
import {
TEST_PASSWORD,
TEST_USER,
} from "../../../../utils/mocked-testing.module";

describe("CouchdbAuthService", () => {
let service: CouchdbAuthService;
let mockHttpClient: jasmine.SpyObj<HttpClient>;
let dbUser = { name: TEST_USER, roles: ["user_app"] };

beforeEach(() => {
mockHttpClient = jasmine.createSpyObj(["get", "post", "put"]);
mockHttpClient.get.and.returnValue(throwError(() => new Error()));
mockHttpClient.post.and.callFake((_url, body) => {
if (body.name === TEST_USER && body.password === TEST_PASSWORD) {
return of(dbUser as any);
} else {
return throwError(
() =>
new HttpErrorResponse({
status: HttpStatusCode.Unauthorized,
})
);
}
});

TestBed.configureTestingModule({
providers: [
CouchdbAuthService,
{ provide: HttpClient, useValue: mockHttpClient },
],
});
service = TestBed.inject(CouchdbAuthService);
});

it("should be created", () => {
expect(service).toBeTruthy();
});

it("should return the current user after successful login", async () => {
const user = await service.authenticate(TEST_USER, TEST_PASSWORD);
expect(user).toEqual(dbUser);
});

it("should login, given that CouchDB cookie is still valid", async () => {
const responseObject = {
ok: true,
userCtx: dbUser,
info: {
authentication_handlers: ["cookie", "default"],
authenticated: "default",
},
};
mockHttpClient.get.and.returnValue(of(responseObject));
const user = await service.autoLogin();

expect(user).toEqual(responseObject.userCtx);
});

it("should not login, given that there is no valid CouchDB cookie", () => {
const responseObject = {
ok: true,
userCtx: {
name: null,
roles: [],
},
info: {
authentication_handlers: ["cookie", "default"],
},
};
mockHttpClient.get.and.returnValue(of(responseObject));
return expectAsync(service.autoLogin()).toBeRejected();
});

it("should reject if current user cant be fetched", () => {
mockHttpClient.get.and.returnValue(throwError(() => new Error()));

return expectAsync(
service.changePassword("username", "wrongPW", "")
).toBeRejected();
});

it("should report error when new Password cannot be saved", async () => {
mockHttpClient.get.and.returnValues(of({}));
mockHttpClient.put.and.returnValue(throwError(() => new Error()));

await expectAsync(
service.changePassword("username", "testPW", "")
).toBeRejected();
expect(mockHttpClient.get).toHaveBeenCalled();
expect(mockHttpClient.put).toHaveBeenCalled();
});

it("should not fail if get and put requests are successful", () => {
mockHttpClient.get.and.returnValues(of({}));
mockHttpClient.put.and.returnValues(of({}));

return expectAsync(
service.changePassword("username", "testPW", "newPW")
).not.toBeRejected();
});
});
112 changes: 112 additions & 0 deletions src/app/core/session/auth/couchdb/couchdb-auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Injectable } from "@angular/core";
import { AuthService } from "../auth.service";
import {
HttpClient,
HttpErrorResponse,
HttpHeaders,
HttpStatusCode,
} from "@angular/common/http";
import { firstValueFrom } from "rxjs";
import { DatabaseUser } from "../../session-service/local-user";
import { AppSettings } from "../../../app-config/app-settings";

@Injectable()
export class CouchdbAuthService extends AuthService {
private static readonly COUCHDB_USER_ENDPOINT = `${AppSettings.DB_PROXY_PREFIX}/_users/org.couchdb.user`;

constructor(private http: HttpClient) {
super();
}

addAuthHeader() {
// auth happens through cookie
return;
}

authenticate(username: string, password: string): Promise<DatabaseUser> {
return firstValueFrom(
this.http.post<DatabaseUser>(
`${AppSettings.DB_PROXY_PREFIX}/_session`,
{ name: username, password: password },
{ withCredentials: true }
)
);
}

autoLogin(): Promise<any> {
return firstValueFrom(
this.http.get<{ userCtx: DatabaseUser }>(
`${AppSettings.DB_PROXY_PREFIX}/_session`,
{ withCredentials: true }
)
).then((res: any) => {
if (res.userCtx.name) {
return res.userCtx;
} else {
throw new HttpErrorResponse({
status: HttpStatusCode.Unauthorized,
});
}
});
}

/**
* Function to change the password of a user
* @param username The username for which the password should be changed
* @param oldPassword The current plaintext password of the user
* @param newPassword The new plaintext password of the user
* @return Promise that resolves once the password is changed in _user and the database
*/
public async changePassword(
username?: string,
oldPassword?: string,
newPassword?: string
): Promise<void> {
let userResponse;
try {
// TODO due to cookie-auth, the old password is actually not checked
userResponse = await this.getCouchDBUser(username, oldPassword);
} catch (e) {
throw new Error("Current password incorrect or server not available");
}

userResponse.password = newPassword;
try {
await this.saveNewPasswordToCouchDB(username, oldPassword, userResponse);
} catch (e) {
throw new Error(
"Could not save new password, please contact your system administrator"
);
}
}

private getCouchDBUser(username: string, password: string): Promise<any> {
const userUrl = CouchdbAuthService.COUCHDB_USER_ENDPOINT + ":" + username;
const headers: HttpHeaders = new HttpHeaders({
Authorization: "Basic " + btoa(username + ":" + password),
});
return firstValueFrom(this.http.get(userUrl, { headers: headers }));
}

private saveNewPasswordToCouchDB(
username: string,
oldPassword: string,
userObj: any
): Promise<any> {
const userUrl = CouchdbAuthService.COUCHDB_USER_ENDPOINT + ":" + username;
const headers: HttpHeaders = new HttpHeaders({
Authorization: "Basic " + btoa(username + ":" + oldPassword),
});
return firstValueFrom(
this.http.put(userUrl, userObj, { headers: headers })
);
}

logout(): Promise<any> {
return firstValueFrom(
this.http.delete(`${AppSettings.DB_PROXY_PREFIX}/_session`, {
withCredentials: true,
})
);
}
}
Loading

0 comments on commit 2a63ffb

Please sign in to comment.