Skip to content

Commit 977c5be

Browse files
authored
Add in automatic sign out after an interval of inactivity (#652)
1 parent 0117fb9 commit 977c5be

File tree

5 files changed

+160
-8
lines changed

5 files changed

+160
-8
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
## v1.1.0 Release Notes
55

6+
### Added the ability to configure auto sign-out after a period of inactivity, per user domain, if Job Manager is pointing at a CromIAM.
7+
68
### Labels that are set to be hidden on the Job List page will not be shown at the top of the Job Details page.
79

810
#### The full list of labels associated with a job has been moved to a new 'Labels' tab.

api/jobs.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,14 @@ definitions:
259259
description: OAuth 2.0 requested scopes
260260
items:
261261
type: string
262+
forcedLogoutDomains:
263+
type: array
264+
description: Domains where a forced logout will happen after an interval of inactivity has passed.
265+
items:
266+
type: string
267+
forcedLogoutTime:
268+
type: integer
269+
description: Number of milliseconds for the interval of inactivity.
262270

263271
DisplayField:
264272
description: Description of a display field

servers/cromwell/README.md

+77-2
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ Thin shim around [`cromwell`](https://github.com/broadinstitute/cromwell).
119119
- If the field is `editable`, then `fieldType` is required.
120120
- If the field is `editable`, then `filterable` will be ignored.
121121

122-
- (Required, CaaS only) Configure fields to display
123-
- **Note:** If you want to use use Job Manager against Cromwell-as-a-Service, which is using SAM/Google OAuth for authZ/authN, the `capabilities_config.json` must also include some extra fields, as well as proper scopes, which are shown as below:
122+
- (Required, CromIAM only) Configure fields to display
123+
- **Note:** If you want to use use Job Manager against CromIAM, which is using SAM/Google OAuth for authZ/authN, the `capabilities_config.json` must also include some extra fields, as well as proper scopes, which are shown as below:
124124
```json
125125
{
126126
"displayFields": [
@@ -188,6 +188,81 @@ Thin shim around [`cromwell`](https://github.com/broadinstitute/cromwell).
188188
}
189189
```
190190

191+
- (Required, CromIAM with automatic signout) Configure fields to display
192+
- **Note:** If you want to use use Job Manager against CromIAM and you want inactive users to be signed out after a specific interval of time, the `capabilities_config.json` must also include some extra fields, which are shown as below:
193+
```json
194+
{
195+
"displayFields": [
196+
{
197+
"field": "id",
198+
"display": "Workflow ID"
199+
},
200+
{
201+
"field": "name",
202+
"display": "Name",
203+
"filterable": true
204+
},
205+
{
206+
"field": "status",
207+
"display": "Status"
208+
},
209+
{
210+
"field": "submission",
211+
"display": "Submitted",
212+
"fieldType": "date"
213+
},
214+
{
215+
"field": "labels.label",
216+
"display": "Label",
217+
"fieldType": "text",
218+
"editable": true,
219+
"bulkEditable": true
220+
},
221+
{
222+
"field": "labels.flag",
223+
"display": "Flag",
224+
"editable": true,
225+
"bulkEditable": true,
226+
"fieldType": "list",
227+
"validFieldValues": [
228+
"archive",
229+
"follow-up"
230+
]
231+
},
232+
{
233+
"field": "labels.comment",
234+
"display": "Comment",
235+
"fieldType": "text",
236+
"editable": true
237+
}
238+
],
239+
"commonLabels": [
240+
"id",
241+
"name",
242+
"label",
243+
"comment",
244+
"flag"
245+
],
246+
"queryExtensions": [
247+
"hideArchived"
248+
],
249+
"authentication": {
250+
"isRequired": true,
251+
"scopes": [
252+
"openid",
253+
"email",
254+
"profile"
255+
],
256+
"forcedLogoutDomains": [
257+
"foo.bar"
258+
],
259+
"forcedLogoutTime": 20000000
260+
}
261+
}
262+
```
263+
- The `forcedLogoutDomains` setting is an array of user domains where this should apply.
264+
- The `forcedLogoutTime` is the amount of inactive time (in milliseconds) that will trigger an automatic sign-out.
265+
191266
- Link docker compose
192267
- **Note:** You may have completed this already if following the Job Manager [Development instructions](../../README.md#Development)
193268
- Symbolically link the cromwell docker compose file depending on your `CROMWELL_URL`. For Cromwell-as-a-Service, e.g. `https://cromwell.caas-dev.broadinstitute.org/api/workflows/v1`, use `cromwell-caas-compose.yaml` otherwise use `cromwell-instance-compose.yaml`, e.g:

ui/src/app/core/auth.service.ts

+69-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {MatSnackBar} from "@angular/material";
44

55
import {CapabilitiesService} from './capabilities.service';
66
import {ConfigLoaderService} from "../../environments/config-loader.service";
7+
import {Observable} from "rxjs";
78

89
declare const gapi: any;
910

@@ -15,6 +16,12 @@ export class AuthService {
1516
public authToken: string;
1617
public userId: string;
1718
public userEmail: string;
19+
public userDomain: string;
20+
public forcedLogoutDomains: string[];
21+
private logoutTimer: number;
22+
private warningTimer: number;
23+
public logoutInterval: number;
24+
readonly WARNING_INTERVAL = 10000;
1825

1926
private initAuth(scopes: string[]): Promise<any> {
2027
const clientId = this.configLoader.getEnvironmentConfigSynchronous()['clientId'];
@@ -34,11 +41,17 @@ export class AuthService {
3441
this.authToken = user.getAuthResponse().access_token;
3542
this.userId = user.getId();
3643
this.userEmail = user.getBasicProfile().getEmail();
44+
this.userDomain = user.getHostedDomain();
3745
this.authenticated.next(true);
46+
47+
if (this.forcedLogoutDomains && this.forcedLogoutDomains.includes(this.userDomain)) {
48+
this.setUpEventListeners();
49+
}
3850
} else {
3951
this.authToken = undefined;
4052
this.userId = undefined;
4153
this.userEmail = undefined;
54+
this.userDomain = undefined;
4255
this.authenticated.next(false);
4356
}
4457
}
@@ -50,6 +63,12 @@ export class AuthService {
5063
if (!capabilities.authentication || !capabilities.authentication.isRequired) {
5164
return;
5265
}
66+
67+
if (capabilities.authentication.forcedLogoutDomains && capabilities.authentication.forcedLogoutTime && capabilities.authentication.forcedLogoutTime > (this.WARNING_INTERVAL * 2)) {
68+
this.forcedLogoutDomains = capabilities.authentication.forcedLogoutDomains;
69+
this.logoutInterval = capabilities.authentication.forcedLogoutTime;
70+
}
71+
5372
this.initAuthPromise = new Promise<void>( (resolve, reject) => {
5473
gapi.load('client:auth2', {
5574
callback: () => this.initAuth(capabilities.authentication.scopes)
@@ -83,20 +102,66 @@ export class AuthService {
83102
})
84103
}
85104

86-
public signIn(): Promise<any> {
105+
public signIn(): Promise<void> {
87106
return new Promise<void>( (resolve, reject) => {
88107
gapi.auth2.getAuthInstance().signIn()
89108
.then(user => resolve(user))
90109
.catch(error => reject(error))
91110
});
92111
}
93112

94-
public signOut(): Promise<any> {
95-
const auth2 = gapi.auth2.getAuthInstance();
96-
return auth2.signOut();
113+
public signOut(): Promise<void> {
114+
return new Promise<void>( (resolve, reject) => {
115+
gapi.auth2.getAuthInstance().signOut()
116+
.then(user => resolve(user))
117+
.catch(error => reject(error))
118+
});
119+
}
120+
121+
private revokeToken(): Promise<void> {
122+
return new Promise<void>( (resolve, reject) => {
123+
gapi.auth2.getAuthInstance().disconnect()
124+
.then(user => resolve(user))
125+
.catch(error => reject(error))
126+
});
97127
}
98128

99129
private handleError(error): void {
100130
this.snackBar.open('An error occurred: ' + error);
101131
}
132+
133+
private setUpEventListeners(): void {
134+
const mouseWheelStream = Observable.fromEvent(window, "mousewheel");
135+
mouseWheelStream.subscribe(() => this.resetTimers());
136+
137+
const mouseDownStream = Observable.fromEvent(window, "mousedown");
138+
mouseDownStream.subscribe(() => this.resetTimers());
139+
140+
const mouseMoveStream = Observable.fromEvent(window, "mousemove");
141+
mouseMoveStream.subscribe(() => this.resetTimers());
142+
143+
const keyDownStream = Observable.fromEvent(window, "keydown");
144+
keyDownStream.subscribe(() => this.resetTimers());
145+
146+
const keyUpStream = Observable.fromEvent(window, "keyup");
147+
keyUpStream.subscribe(() => this.resetTimers());
148+
149+
this.resetTimers();
150+
}
151+
152+
public resetTimers(): void {
153+
window.clearTimeout(this.logoutTimer);
154+
window.clearTimeout(this.warningTimer);
155+
this.snackBar.dismiss();
156+
157+
this.warningTimer = window.setTimeout(() => {
158+
this.snackBar.open('You are about to be logged out due to inactivity');
159+
}, this.logoutInterval - this.WARNING_INTERVAL);
160+
161+
this.logoutTimer = window.setTimeout(() => {
162+
this.revokeToken().then(() => {
163+
window.location.reload();
164+
});
165+
}, this.logoutInterval);
166+
}
102167
}

ui/src/app/sign-in/sign-in.component.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component, OnInit, ViewContainerRef} from '@angular/core';
2-
import {MatSnackBar, MatSnackBarConfig} from '@angular/material'
2+
import {MatSnackBar} from '@angular/material'
33
import {ActivatedRoute, Router} from '@angular/router';
44

55
import {AuthService} from '../core/auth.service';
@@ -20,7 +20,9 @@ export class SignInComponent implements OnInit {
2020
let returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
2121
this.authService.authenticated.subscribe( (authenticated) => {
2222
if (authenticated) {
23-
this.router.navigateByUrl(returnUrl);
23+
this.router.navigateByUrl(returnUrl).then(() => {
24+
this.authService.resetTimers();
25+
});
2426
}
2527
});
2628
}

0 commit comments

Comments
 (0)