Skip to content

Commit

Permalink
Add in automatic sign out after an interval of inactivity (#652)
Browse files Browse the repository at this point in the history
  • Loading branch information
rsasch authored May 22, 2019
1 parent 0117fb9 commit 977c5be
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 8 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

## v1.1.0 Release Notes

### Added the ability to configure auto sign-out after a period of inactivity, per user domain, if Job Manager is pointing at a CromIAM.

### Labels that are set to be hidden on the Job List page will not be shown at the top of the Job Details page.

#### The full list of labels associated with a job has been moved to a new 'Labels' tab.
Expand Down
8 changes: 8 additions & 0 deletions api/jobs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,14 @@ definitions:
description: OAuth 2.0 requested scopes
items:
type: string
forcedLogoutDomains:
type: array
description: Domains where a forced logout will happen after an interval of inactivity has passed.
items:
type: string
forcedLogoutTime:
type: integer
description: Number of milliseconds for the interval of inactivity.

DisplayField:
description: Description of a display field
Expand Down
79 changes: 77 additions & 2 deletions servers/cromwell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ Thin shim around [`cromwell`](https://github.com/broadinstitute/cromwell).
- If the field is `editable`, then `fieldType` is required.
- If the field is `editable`, then `filterable` will be ignored.

- (Required, CaaS only) Configure fields to display
- **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:
- (Required, CromIAM only) Configure fields to display
- **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:
```json
{
"displayFields": [
Expand Down Expand Up @@ -188,6 +188,81 @@ Thin shim around [`cromwell`](https://github.com/broadinstitute/cromwell).
}
```

- (Required, CromIAM with automatic signout) Configure fields to display
- **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:
```json
{
"displayFields": [
{
"field": "id",
"display": "Workflow ID"
},
{
"field": "name",
"display": "Name",
"filterable": true
},
{
"field": "status",
"display": "Status"
},
{
"field": "submission",
"display": "Submitted",
"fieldType": "date"
},
{
"field": "labels.label",
"display": "Label",
"fieldType": "text",
"editable": true,
"bulkEditable": true
},
{
"field": "labels.flag",
"display": "Flag",
"editable": true,
"bulkEditable": true,
"fieldType": "list",
"validFieldValues": [
"archive",
"follow-up"
]
},
{
"field": "labels.comment",
"display": "Comment",
"fieldType": "text",
"editable": true
}
],
"commonLabels": [
"id",
"name",
"label",
"comment",
"flag"
],
"queryExtensions": [
"hideArchived"
],
"authentication": {
"isRequired": true,
"scopes": [
"openid",
"email",
"profile"
],
"forcedLogoutDomains": [
"foo.bar"
],
"forcedLogoutTime": 20000000
}
}
```
- The `forcedLogoutDomains` setting is an array of user domains where this should apply.
- The `forcedLogoutTime` is the amount of inactive time (in milliseconds) that will trigger an automatic sign-out.

- Link docker compose
- **Note:** You may have completed this already if following the Job Manager [Development instructions](../../README.md#Development)
- 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:
Expand Down
73 changes: 69 additions & 4 deletions ui/src/app/core/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {MatSnackBar} from "@angular/material";

import {CapabilitiesService} from './capabilities.service';
import {ConfigLoaderService} from "../../environments/config-loader.service";
import {Observable} from "rxjs";

declare const gapi: any;

Expand All @@ -15,6 +16,12 @@ export class AuthService {
public authToken: string;
public userId: string;
public userEmail: string;
public userDomain: string;
public forcedLogoutDomains: string[];
private logoutTimer: number;
private warningTimer: number;
public logoutInterval: number;
readonly WARNING_INTERVAL = 10000;

private initAuth(scopes: string[]): Promise<any> {
const clientId = this.configLoader.getEnvironmentConfigSynchronous()['clientId'];
Expand All @@ -34,11 +41,17 @@ export class AuthService {
this.authToken = user.getAuthResponse().access_token;
this.userId = user.getId();
this.userEmail = user.getBasicProfile().getEmail();
this.userDomain = user.getHostedDomain();
this.authenticated.next(true);

if (this.forcedLogoutDomains && this.forcedLogoutDomains.includes(this.userDomain)) {
this.setUpEventListeners();
}
} else {
this.authToken = undefined;
this.userId = undefined;
this.userEmail = undefined;
this.userDomain = undefined;
this.authenticated.next(false);
}
}
Expand All @@ -50,6 +63,12 @@ export class AuthService {
if (!capabilities.authentication || !capabilities.authentication.isRequired) {
return;
}

if (capabilities.authentication.forcedLogoutDomains && capabilities.authentication.forcedLogoutTime && capabilities.authentication.forcedLogoutTime > (this.WARNING_INTERVAL * 2)) {
this.forcedLogoutDomains = capabilities.authentication.forcedLogoutDomains;
this.logoutInterval = capabilities.authentication.forcedLogoutTime;
}

this.initAuthPromise = new Promise<void>( (resolve, reject) => {
gapi.load('client:auth2', {
callback: () => this.initAuth(capabilities.authentication.scopes)
Expand Down Expand Up @@ -83,20 +102,66 @@ export class AuthService {
})
}

public signIn(): Promise<any> {
public signIn(): Promise<void> {
return new Promise<void>( (resolve, reject) => {
gapi.auth2.getAuthInstance().signIn()
.then(user => resolve(user))
.catch(error => reject(error))
});
}

public signOut(): Promise<any> {
const auth2 = gapi.auth2.getAuthInstance();
return auth2.signOut();
public signOut(): Promise<void> {
return new Promise<void>( (resolve, reject) => {
gapi.auth2.getAuthInstance().signOut()
.then(user => resolve(user))
.catch(error => reject(error))
});
}

private revokeToken(): Promise<void> {
return new Promise<void>( (resolve, reject) => {
gapi.auth2.getAuthInstance().disconnect()
.then(user => resolve(user))
.catch(error => reject(error))
});
}

private handleError(error): void {
this.snackBar.open('An error occurred: ' + error);
}

private setUpEventListeners(): void {
const mouseWheelStream = Observable.fromEvent(window, "mousewheel");
mouseWheelStream.subscribe(() => this.resetTimers());

const mouseDownStream = Observable.fromEvent(window, "mousedown");
mouseDownStream.subscribe(() => this.resetTimers());

const mouseMoveStream = Observable.fromEvent(window, "mousemove");
mouseMoveStream.subscribe(() => this.resetTimers());

const keyDownStream = Observable.fromEvent(window, "keydown");
keyDownStream.subscribe(() => this.resetTimers());

const keyUpStream = Observable.fromEvent(window, "keyup");
keyUpStream.subscribe(() => this.resetTimers());

this.resetTimers();
}

public resetTimers(): void {
window.clearTimeout(this.logoutTimer);
window.clearTimeout(this.warningTimer);
this.snackBar.dismiss();

this.warningTimer = window.setTimeout(() => {
this.snackBar.open('You are about to be logged out due to inactivity');
}, this.logoutInterval - this.WARNING_INTERVAL);

this.logoutTimer = window.setTimeout(() => {
this.revokeToken().then(() => {
window.location.reload();
});
}, this.logoutInterval);
}
}
6 changes: 4 additions & 2 deletions ui/src/app/sign-in/sign-in.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component, OnInit, ViewContainerRef} from '@angular/core';
import {MatSnackBar, MatSnackBarConfig} from '@angular/material'
import {MatSnackBar} from '@angular/material'
import {ActivatedRoute, Router} from '@angular/router';

import {AuthService} from '../core/auth.service';
Expand All @@ -20,7 +20,9 @@ export class SignInComponent implements OnInit {
let returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.authService.authenticated.subscribe( (authenticated) => {
if (authenticated) {
this.router.navigateByUrl(returnUrl);
this.router.navigateByUrl(returnUrl).then(() => {
this.authService.resetTimers();
});
}
});
}
Expand Down

0 comments on commit 977c5be

Please sign in to comment.