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

OIDC/Auth2 integration #4183

Closed
wants to merge 21 commits into from
Closed
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
14 changes: 14 additions & 0 deletions client/package-lock.json

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

3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@
"rxjs": "^7.8.1",
"tinycolor2": "1.6.0",
"video.js": "^8.17.4",
"zone.js": "^0.14.10"
"zone.js": "^0.14.10",
"angular-oauth2-oidc": "^17.0.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.2.5",
Expand Down
4 changes: 3 additions & 1 deletion client/src/app/domain/definitions/permission.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ Meeting specific information: Structure level, Group, Participant number, About
},
{
display_name: _(`Can see sensitive data`),
help_text: _(`Can see email, username and SSO identification of all participants.`),
help_text: _(
`Can see email, username, membership number, SSO identification and locked out state of all participants.`
),
value: Permission.userCanSeeSensitiveData
},
{
Expand Down
1 change: 1 addition & 0 deletions client/src/app/domain/interfaces/auth-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface AuthToken {
sessionId: string;
iat: number;
exp: number;
rawAccessToken: string;
}
2 changes: 2 additions & 0 deletions client/src/app/domain/models/meeting-users/meeting-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class MeetingUser extends BaseDecimalModel<MeetingUser> {
public readonly number!: string;
public readonly about_me!: string;
public readonly vote_weight!: number;
public readonly locked_out!: boolean;

public user_id!: Id;
public meeting_id!: Id;
Expand Down Expand Up @@ -42,6 +43,7 @@ export class MeetingUser extends BaseDecimalModel<MeetingUser> {
`number`,
`about_me`,
`vote_weight`,
`locked_out`,
`user_id`,
`meeting_id`,
`personal_note_ids`,
Expand Down
5 changes: 0 additions & 5 deletions client/src/app/gateways/auth-adapter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,4 @@ export class AuthAdapterService {
public whoAmI(): Promise<AuthServiceResponse> {
return this.http.post<AuthServiceResponse>(`${this.authUrl}/who-am-i/`);
}

public async startSamlLogin(): Promise<string> {
const { message } = await this.http.get<AuthServiceResponse>(`/system/saml/getUrl`);
return message;
}
}
40 changes: 23 additions & 17 deletions client/src/app/gateways/base-icc-gateway.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { filter, Observable } from 'rxjs';
import { SharedWorkerService } from '../openslides-main-module/services/shared-worker.service';
import { ActiveMeetingIdService } from '../site/pages/meetings/services/active-meeting-id.service';
import { WorkerResponse } from '../worker/interfaces';
import { AutoupdateReceiveData } from '../worker/sw-autoupdate.interfaces';
import { ICC_ENDPOINT, ICCConnectMessage, ICCDisconnectMessage } from '../worker/sw-icc.interfaces';
import { HttpService } from './http.service';

const ICC_ENDPOINT = `icc`;

export const ICC_PATH = `/system/icc`;

/**
Expand Down Expand Up @@ -76,13 +76,16 @@ export abstract class BaseICCGatewayService<ICCResponseType> {
onReastartSub.unsubscribe();
msgSub.unsubscribe();
onClosedSub.unsubscribe();
this.sharedWorker.sendMessage(`icc`, {
action: `disconnect`,
params: {
type: this.receivePath,
meetingId
this.sharedWorker.sendMessage({
receiver: `icc`,
msg: {
action: `disconnect`,
params: {
type: this.receivePath,
meetingId
}
}
} as any);
} as ICCDisconnectMessage);
};
}

Expand Down Expand Up @@ -116,16 +119,19 @@ export abstract class BaseICCGatewayService<ICCResponseType> {
}

private sendConnectToWorker(meetingId: number): void {
this.sharedWorker.sendMessage(ICC_ENDPOINT, {
action: `connect`,
params: {
type: this.receivePath,
meetingId
this.sharedWorker.sendMessage(<ICCConnectMessage>{
receiver: ICC_ENDPOINT,
msg: {
action: `connect`,
params: {
type: this.receivePath,
meetingId
}
}
} as any);
});
}

private messageObservable(meetingId: number): Observable<WorkerResponse> {
private messageObservable(meetingId: number): Observable<AutoupdateReceiveData> {
return this.sharedWorker
.listenTo(ICC_ENDPOINT)
.pipe(
Expand All @@ -135,10 +141,10 @@ export abstract class BaseICCGatewayService<ICCResponseType> {
data.content?.type === this.receivePath &&
data.content?.meeting_id === meetingId
)
);
) as Observable<AutoupdateReceiveData>;
}

private closedObservable(meetingId: number): Observable<WorkerResponse> {
private closedObservable(meetingId: number): Observable<WorkerResponse<any>> {
return this.sharedWorker
.listenTo(ICC_ENDPOINT)
.pipe(
Expand Down
49 changes: 39 additions & 10 deletions client/src/app/gateways/http.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpResponse } from '@angular/common/http';
import { Injectable, Injector } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslateService } from '@ngx-translate/core';
import { firstValueFrom, Observable } from 'rxjs';
import {HttpClient, HttpErrorResponse, HttpHeaders, HttpResponse} from '@angular/common/http';
import {Injectable, Injector} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar';
import {TranslateService} from '@ngx-translate/core';
import {firstValueFrom, Observable} from 'rxjs';

import {
formatQueryParams,
Expand All @@ -11,10 +11,11 @@ import {
QueryParams,
ResponseType
} from '../infrastructure/definitions/http';
import { ProcessError } from '../infrastructure/errors';
import { toBase64 } from '../infrastructure/utils/functions';
import { ActionWorkerWatchService } from './action-worker-watch/action-worker-watch.service';
import { ErrorMapService } from './error-mapping/error-map.service';
import {ProcessError} from '../infrastructure/errors';
import {toBase64} from '../infrastructure/utils/functions';
import {ActionWorkerWatchService} from './action-worker-watch/action-worker-watch.service';
import {ErrorMapService} from './error-mapping/error-map.service';
import { AuthTokenService } from '../site/services/auth-token.service';

type HttpHeadersObj = HttpHeaders | { [header: string]: string | string[] };

Expand All @@ -32,6 +33,8 @@ export interface RequestSettings {
})
export class HttpService {
private _actionWorkerWatch: ActionWorkerWatchService;
private blobCache = new Map<string, string>(); // Cache for blob URLs

private get actionWorkerWatch(): ActionWorkerWatchService {
if (!this._actionWorkerWatch) {
this._actionWorkerWatch = this.injector.get(ActionWorkerWatchService);
Expand All @@ -44,7 +47,8 @@ export class HttpService {
private errorMapper: ErrorMapService,
private injector: Injector,
private snackBar: MatSnackBar,
private translate: TranslateService
private translate: TranslateService,
private authTokenService: AuthTokenService
) {}

/**
Expand Down Expand Up @@ -219,4 +223,29 @@ export class HttpService {
return { 'ngsw-bypass': `true`, ...headers };
}
}

public async getBlobUrl(url: string): Promise<string | null> {
await this.authTokenService.waitForValidToken();
try {
if (this.blobCache.has(url)) {
return this.blobCache.get(url);
}

// Fetch the resource as a blob
const response = await fetch(url, { headers: { Authentication: `Bearer ${this.authTokenService.rawAccessToken}` } });
if (!response.ok) {
throw new Error('Network response was not ok');
}
const blob = await response.blob();

// Create a blob URL and cache it
const blobUrl = URL.createObjectURL(blob);
this.blobCache.set(url, blobUrl);

return blobUrl;
} catch (error) {
console.error('Error loading image:', error);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export class MeetingUserRepositoryService extends BaseMeetingRelatedRepository<V
`vote_weight`,
`comment`,
`user_id`,
`number`
`number`,
`locked_out`
]);

const detailFields: TypedFieldset<MeetingUser> = [`about_me`, `user_id`, `meeting_id`];
Expand All @@ -57,7 +58,8 @@ export class MeetingUserRepositoryService extends BaseMeetingRelatedRepository<V
vote_delegated_to_id: partialUser.vote_delegated_to_id,
vote_delegations_from_ids: partialUser.vote_delegations_from_ids,
structure_level_ids: partialUser.structure_level_ids,
group_ids: partialUser.group_ids
group_ids: partialUser.group_ids,
locked_out: partialUser.locked_out
};

if (Object.values(partialPayload).filter(val => val !== undefined).length > 1 && partialPayload.meeting_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ export type RawUser = FullNameInformation & Identifiable & Displayable & { fqid:
export type GeneralUser = ViewUser & ViewMeetingUser;

/**
* Unified type name for state fields like `is_active`, `is_physical_person` and `is_present_in_meetings`.
* Unified type name for state fields like `is_active`, `is_physical_person`, `is_present_in_meetings`
* and 'locked_out'.
*/
export type UserStateField = 'is_active' | 'is_present_in_meetings' | 'is_physical_person';
export type UserStateField = 'is_active' | 'is_present_in_meetings' | 'is_physical_person' | 'locked_out';

export interface AssignMeetingsPayload {
meeting_ids: Id[];
Expand Down Expand Up @@ -130,7 +131,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {

const participantListFields: TypedFieldset<User> = participantListFieldsMinimal
.concat(filterableListFields)
.concat([`is_present_in_meeting_ids`, `default_password`]);
.concat([`is_present_in_meeting_ids`, `default_password`, `committee_ids`, `committee_management_ids`]);

const detailFields: TypedFieldset<User> = [`default_password`, `can_change_own_password`];

Expand Down Expand Up @@ -543,7 +544,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
`comment`,
`about_me`,
`number`,
`structure_level`
`structure_level`,
`locked_out`
];
if (!create) {
fields.push(`member_number`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type KeycloakLoginConfig = {
bootAsKeycloakPage: boolean;
jumpToRoute: string;
loginAction: string;
fieldErrors: {
password: string;
username: string;
};
};

export function getKeycloakLoginConfig(): KeycloakLoginConfig {
// @ts-expect-error bootAsKeycloakPage is a global variable
return window.keycloakLoginConfig ? (window.keycloakLoginConfig as KeycloakLoginConfig) : null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AuthConfig } from 'angular-oauth2-oidc';

export const authCodeFlowConfig: AuthConfig = {
issuer: `https://localhost:8000/idp/realms/os`,
redirectUri: window.location.origin + `/`,
clientId: `os-ui`,
responseType: `code`,
scope: `openid profile email offline_access`,
showDebugInformation: true,
timeoutFactor: 0.75,
openUri: (uri: string) => {
console.debug(`OpenUri: ${uri}`);
window.location.href = uri;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@ import { overloadJsFunctions } from 'src/app/infrastructure/utils/overload-js-fu
import { Deferred } from 'src/app/infrastructure/utils/promises';
import { BaseViewModel } from 'src/app/site/base/base-view-model';
import { UpdateService } from 'src/app/site/modules/site-wrapper/services/update.service';
import { AuthService } from 'src/app/site/services/auth.service';
import { LifecycleService } from 'src/app/site/services/lifecycle.service';
import { OpenSlidesService } from 'src/app/site/services/openslides.service';
import { OpenSlidesStatusService } from 'src/app/site/services/openslides-status.service';
import { ViewModelStoreService } from 'src/app/site/services/view-model-store.service';

import { getKeycloakLoginConfig } from './keycloak-login';

const CURRENT_LANGUAGE_STORAGE_KEY = `currentLanguage`;

function bootAsKeycloakPage(): boolean {
return getKeycloakLoginConfig()?.bootAsKeycloakPage || false;
}

@Component({
selector: `os-root`,
templateUrl: `./openslides-main.component.html`,
Expand All @@ -43,8 +50,13 @@ export class OpenSlidesMainComponent implements OnInit {
private config: DateFnsConfigurationService,
private updateService: UpdateService,
private router: Router,
private modelStore: ViewModelStoreService
private modelStore: ViewModelStoreService,
private authService: AuthService
) {
if (!bootAsKeycloakPage()) {
authService.startOidcWorkflow();
}

overloadJsFunctions();
this.addDebugFunctions();
this.waitForAppLoaded();
Expand Down
Loading
Loading