From 67880310a10011a0ddc5a17095479ea5492a9a35 Mon Sep 17 00:00:00 2001 From: Mahmood Ali Date: Fri, 3 Apr 2020 11:18:54 -0400 Subject: [PATCH 01/11] backend: support WS authentication handshake in alloc/exec The javascript Websocket API doesn't support setting custom headers (e.g. `X-Nomad-Token`). This change adds support for having an authentication handshake message: clients can set `ws_handshake` URL query parameter to true and send a single handshake message with auth token first before any other mssage. This is a backward compatible change: it does not affect nomad CLI path, as it doesn't set `ws_handshake` parameter. --- command/agent/alloc_endpoint.go | 38 ++++++++++++++++++++ command/agent/alloc_endpoint_test.go | 54 ++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/command/agent/alloc_endpoint.go b/command/agent/alloc_endpoint.go index 839022a54..950e6a80c 100644 --- a/command/agent/alloc_endpoint.go +++ b/command/agent/alloc_endpoint.go @@ -398,9 +398,47 @@ func (s *HTTPServer) allocExec(allocID string, resp http.ResponseWriter, req *ht return nil, fmt.Errorf("failed to upgrade connection: %v", err) } + if err := readWsHandshake(conn.ReadJSON, req, &args.QueryOptions); err != nil { + conn.WriteMessage(websocket.CloseMessage, + websocket.FormatCloseMessage(toWsCode(400), err.Error())) + return nil, err + } + return s.execStreamImpl(conn, &args) } +func readWsHandshake(readFn func(interface{}) error, req *http.Request, q *structs.QueryOptions) error { + + // avoid handshake if request doesn't require one + if hv := req.URL.Query().Get("ws_handshake"); hv == "" { + return nil + } else if h, err := strconv.ParseBool(hv); err != nil { + return fmt.Errorf("ws_handshake value is not a boolean: %v", err) + } else if !h { + return nil + } + + fmt.Println("HERE") + + var h wsHandshakeMessage + err := readFn(&h) + if err != nil { + return err + } + + if h.Version != 1 { + return fmt.Errorf("unexpected handshake value: %v", h.Version) + } + + q.AuthToken = h.AuthToken + return nil +} + +type wsHandshakeMessage struct { + Version int `json:"version"` + AuthToken string `json:"auth_token"` +} + func (s *HTTPServer) execStreamImpl(ws *websocket.Conn, args *cstructs.AllocExecRequest) (interface{}, error) { allocID := args.AllocID method := "Allocations.Exec" diff --git a/command/agent/alloc_endpoint_test.go b/command/agent/alloc_endpoint_test.go index c8092b000..4bdf4fee0 100644 --- a/command/agent/alloc_endpoint_test.go +++ b/command/agent/alloc_endpoint_test.go @@ -959,3 +959,57 @@ func TestHTTP_AllocAllGC_ACL(t *testing.T) { } }) } + +func TestHTTP_ReadWsHandshake(t *testing.T) { + cases := []struct { + name string + token string + handshake bool + }{ + { + name: "plain compatible mode", + token: "", + handshake: false, + }, + { + name: "handshake unauthenticated", + token: "", + handshake: true, + }, + { + name: "handshake authenticated", + token: "mysupersecret", + handshake: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + + called := false + readFn := func(h interface{}) error { + called = true + if !c.handshake { + return fmt.Errorf("should not be called") + } + + hm := h.(*wsHandshakeMessage) + hm.Version = 1 + hm.AuthToken = c.token + return nil + } + + req := httptest.NewRequest("PUT", "/target", nil) + if c.handshake { + req.URL.RawQuery = "ws_handshake=true" + } + + var q structs.QueryOptions + + err := readWsHandshake(readFn, req, &q) + require.NoError(t, err) + require.Equal(t, c.token, q.AuthToken) + require.Equal(t, c.handshake, called) + }) + } +} From cad5261ec14cce76e62a29637ac325572c932994 Mon Sep 17 00:00:00 2001 From: Mahmood Ali Date: Fri, 3 Apr 2020 11:22:22 -0400 Subject: [PATCH 02/11] ui: send authentication ws handshake Have the UI send the authentication websocket handshake message. --- ui/app/controllers/exec.js | 3 +- ui/app/services/sockets.js | 2 +- .../classes/exec-socket-xterm-adapter.js | 8 +- ui/tests/acceptance/exec-test.js | 86 ++++++++++++++++++- .../util/exec-socket-xterm-adapter-test.js | 66 +++++++++++++- 5 files changed, 158 insertions(+), 7 deletions(-) diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index c3aeee927..9de523987 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -14,6 +14,7 @@ const ANSI_WHITE = '\x1b[0m'; export default Controller.extend({ sockets: service(), system: service(), + token: service(), queryParams: ['allocation'], @@ -78,6 +79,6 @@ export default Controller.extend({ this.set('command', command); this.socket = this.sockets.getTaskStateSocket(this.taskState, command); - new ExecSocketXtermAdapter(this.terminal, this.socket); + new ExecSocketXtermAdapter(this.terminal, this.socket, this.token.secret); }, }); diff --git a/ui/app/services/sockets.js b/ui/app/services/sockets.js index 3d18a5e39..b629aa077 100644 --- a/ui/app/services/sockets.js +++ b/ui/app/services/sockets.js @@ -30,7 +30,7 @@ export default Service.extend({ return new WebSocket( `${protocol}//${prefix}/client/allocation/${taskState.allocation.id}` + - `/exec?task=${taskState.name}&tty=true` + + `/exec?task=${taskState.name}&tty=true&ws_handshake=true` + `&command=${encodeURIComponent(`["${command}"]`)}` ); } diff --git a/ui/app/utils/classes/exec-socket-xterm-adapter.js b/ui/app/utils/classes/exec-socket-xterm-adapter.js index 82141150c..c4a3dc2d9 100644 --- a/ui/app/utils/classes/exec-socket-xterm-adapter.js +++ b/ui/app/utils/classes/exec-socket-xterm-adapter.js @@ -4,11 +4,13 @@ import base64js from 'base64-js'; import { TextDecoderLite, TextEncoderLite } from 'text-encoder-lite'; export default class ExecSocketXtermAdapter { - constructor(terminal, socket) { + constructor(terminal, socket, token) { this.terminal = terminal; this.socket = socket; + this.token = token; socket.onopen = () => { + this.sendWsHandshake(); this.sendTtySize(); terminal.onData(data => { @@ -43,6 +45,10 @@ export default class ExecSocketXtermAdapter { ); } + sendWsHandshake() { + this.socket.send(JSON.stringify({ version: 1, auth_token: this.token || '' })); + } + handleData(data) { this.socket.send(JSON.stringify({ stdin: { data: encodeString(data) } })); } diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index d05020bf4..552f60241 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -2,6 +2,7 @@ import { module, test } from 'qunit'; import { currentURL, settled } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; +import Tokens from 'nomad-ui/tests/pages/settings/tokens'; import Service from '@ember/service'; import Exec from 'nomad-ui/tests/pages/exec'; @@ -10,7 +11,8 @@ module('Acceptance | exec', function(hooks) { setupMirage(hooks); hooks.beforeEach(async function() { - window.localStorage.removeItem('nomadExecCommand'); + localStorage.clear(); + sessionStorage.clear(); server.create('agent'); server.create('node'); @@ -260,6 +262,88 @@ module('Acceptance | exec', function(hooks) { await settled(); assert.deepEqual(mockSocket.sent, [ + '{"version":1,"auth_token":""}', + `{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`, + '{"stdin":{"data":"DQ=="}}', + ]); + + await mockSocket.onclose(); + await settled(); + + assert.equal( + window.execTerminal.buffer + .getLine(6) + .translateToString() + .trim(), + 'The connection has closed.' + ); + }); + test('running the command opens the socket and authenticates', async function(assert) { + let managementToken = server.create('token'); + + const { secretId } = managementToken; + + await Tokens.visit(); + await Tokens.secret(secretId).submit(); + + let mockSocket = new MockSocket(); + let mockSockets = Service.extend({ + getTaskStateSocket(taskState, command) { + assert.equal(taskState.name, task.name); + assert.equal(taskState.allocation.id, allocation.id); + + assert.equal(command, '/bin/bash'); + + assert.step('Socket built'); + + return mockSocket; + }, + }); + + this.owner.register('service:sockets', mockSockets); + + let taskGroup = this.job.task_groups.models[0]; + let task = taskGroup.tasks.models[0]; + let allocations = this.server.db.allocations.where({ + jobId: this.job.id, + taskGroup: taskGroup.name, + }); + let allocation = allocations[allocations.length - 1]; + + await Exec.visitTask({ + job: this.job.id, + task_group: taskGroup.name, + task_name: task.name, + allocation: allocation.id.split('-')[0], + }); + + await settled(); + + await Exec.terminal.pressEnter(); + await settled(); + mockSocket.onopen(); + + assert.verifySteps(['Socket built']); + + mockSocket.onmessage({ + data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}', + }); + + await settled(); + + assert.equal( + window.execTerminal.buffer + .getLine(5) + .translateToString() + .trim(), + 'sh-3.2 🥳$' + ); + + await Exec.terminal.pressEnter(); + await settled(); + + assert.deepEqual(mockSocket.sent, [ + `{"version":1,"auth_token":"${secretId}"}`, `{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`, '{"stdin":{"data":"DQ=="}}', ]); diff --git a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js index 7ecbf8eb0..c303d98ec 100644 --- a/ui/tests/integration/util/exec-socket-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-socket-xterm-adapter-test.js @@ -8,6 +8,66 @@ import { Terminal } from 'xterm-vendor'; module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { setupRenderingTest(hooks); + test('initiating socket sends authentication handshake', async function(assert) { + let done = assert.async(); + + let terminal = new Terminal(); + this.set('terminal', terminal); + + await render(hbs` + {{exec-terminal terminal=terminal}} + `); + + await settled(); + + let firstMessage = true; + let mockSocket = new Object({ + send(message) { + if (firstMessage) { + firstMessage = false; + assert.deepEqual(message, JSON.stringify({ version: 1, auth_token: 'mysecrettoken' })); + done(); + } + }, + }); + + new ExecSocketXtermAdapter(terminal, mockSocket, 'mysecrettoken'); + + mockSocket.onopen(); + + await settled(); + }); + + test('initiating socket sends authentication handshake even if unauthenticated', async function(assert) { + let done = assert.async(); + + let terminal = new Terminal(); + this.set('terminal', terminal); + + await render(hbs` + {{exec-terminal terminal=terminal}} + `); + + await settled(); + + let firstMessage = true; + let mockSocket = new Object({ + send(message) { + if (firstMessage) { + firstMessage = false; + assert.deepEqual(message, JSON.stringify({ version: 1, auth_token: '' })); + done(); + } + }, + }); + + new ExecSocketXtermAdapter(terminal, mockSocket, null); + + mockSocket.onopen(); + + await settled(); + }); + test('resizing the window passes a resize message through the socket', async function(assert) { let done = assert.async(); @@ -30,7 +90,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { }, }); - new ExecSocketXtermAdapter(terminal, mockSocket); + new ExecSocketXtermAdapter(terminal, mockSocket, ''); window.dispatchEvent(new Event('resize')); @@ -53,7 +113,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { send() {}, }); - new ExecSocketXtermAdapter(terminal, mockSocket); + new ExecSocketXtermAdapter(terminal, mockSocket, ''); mockSocket.onmessage({ data: '{"stdout":{"exited":"true"}}', @@ -76,7 +136,7 @@ module('Integration | Utility | exec-socket-xterm-adapter', function(hooks) { send() {}, }); - new ExecSocketXtermAdapter(terminal, mockSocket); + new ExecSocketXtermAdapter(terminal, mockSocket, ''); mockSocket.onmessage({ data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}', From b7999b31fb4c339eba80ce7b2fa1bbf2cdcef213 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 3 Apr 2020 12:21:44 -0500 Subject: [PATCH 03/11] Add space --- ui/tests/acceptance/exec-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 552f60241..8c565a264 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -278,6 +278,7 @@ module('Acceptance | exec', function(hooks) { 'The connection has closed.' ); }); + test('running the command opens the socket and authenticates', async function(assert) { let managementToken = server.create('token'); From 4fccaaa2e39e226c954dbd1ead4ff42fea1d7772 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 3 Apr 2020 12:26:25 -0500 Subject: [PATCH 04/11] Change to setting token directly Most tests bypass setting the token via the UI, instead choosing to set it in localStorage directly, because the acceptance tests for the token UI are sufficient to exercise that part of the UI, so this speeds up the test a bit. --- ui/tests/acceptance/exec-test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 8c565a264..ac5e31eff 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -2,7 +2,6 @@ import { module, test } from 'qunit'; import { currentURL, settled } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; -import Tokens from 'nomad-ui/tests/pages/settings/tokens'; import Service from '@ember/service'; import Exec from 'nomad-ui/tests/pages/exec'; @@ -284,8 +283,7 @@ module('Acceptance | exec', function(hooks) { const { secretId } = managementToken; - await Tokens.visit(); - await Tokens.secret(secretId).submit(); + window.localStorage.nomadTokenSecret = secretId; let mockSocket = new MockSocket(); let mockSockets = Service.extend({ From f1d9e878a7718bf87fce19a7e82e2b7de078d772 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 3 Apr 2020 12:27:03 -0500 Subject: [PATCH 05/11] Remove intermediate storage variable --- ui/tests/acceptance/exec-test.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index ac5e31eff..34b8385ba 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -279,10 +279,7 @@ module('Acceptance | exec', function(hooks) { }); test('running the command opens the socket and authenticates', async function(assert) { - let managementToken = server.create('token'); - - const { secretId } = managementToken; - + const { secretId } = server.create('token'); window.localStorage.nomadTokenSecret = secretId; let mockSocket = new MockSocket(); From 4fa139a2fd55e63e39d0590ca9cd873a7c12fcdc Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 3 Apr 2020 12:35:51 -0500 Subject: [PATCH 06/11] Remove redundant assertions from token exec test This only needs to check that the token is sent, the rest of the assertions were covered by the previous test. --- ui/tests/acceptance/exec-test.js | 33 ++------------------------------ 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 34b8385ba..5acf2a733 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -278,7 +278,7 @@ module('Acceptance | exec', function(hooks) { ); }); - test('running the command opens the socket and authenticates', async function(assert) { + test('the opening message includes the token if it exists', async function(assert) { const { secretId } = server.create('token'); window.localStorage.nomadTokenSecret = secretId; @@ -321,39 +321,10 @@ module('Acceptance | exec', function(hooks) { assert.verifySteps(['Socket built']); - mockSocket.onmessage({ - data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}', - }); - - await settled(); - - assert.equal( - window.execTerminal.buffer - .getLine(5) - .translateToString() - .trim(), - 'sh-3.2 🥳$' - ); - await Exec.terminal.pressEnter(); await settled(); - assert.deepEqual(mockSocket.sent, [ - `{"version":1,"auth_token":"${secretId}"}`, - `{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`, - '{"stdin":{"data":"DQ=="}}', - ]); - - await mockSocket.onclose(); - await settled(); - - assert.equal( - window.execTerminal.buffer - .getLine(6) - .translateToString() - .trim(), - 'The connection has closed.' - ); + assert.equal(mockSocket.sent[0], `{"version":1,"auth_token":"${secretId}"}`); }); test('only one socket is opened after switching between tasks', async function(assert) { From 2afe4441b4368f545922ee51b3bd7319fa5cc3b7 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 3 Apr 2020 12:52:39 -0500 Subject: [PATCH 07/11] Remove redundant assertions These are more things that are already covered elsewhere. --- ui/tests/acceptance/exec-test.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 5acf2a733..30183dfd3 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -284,12 +284,7 @@ module('Acceptance | exec', function(hooks) { let mockSocket = new MockSocket(); let mockSockets = Service.extend({ - getTaskStateSocket(taskState, command) { - assert.equal(taskState.name, task.name); - assert.equal(taskState.allocation.id, allocation.id); - - assert.equal(command, '/bin/bash'); - + getTaskStateSocket() { assert.step('Socket built'); return mockSocket; From dd1b2dc37dff6cb42759846bd968a0b0051e284a Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 3 Apr 2020 12:53:57 -0500 Subject: [PATCH 08/11] Remove redundant pause --- ui/tests/acceptance/exec-test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 30183dfd3..5fd0b513e 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -308,8 +308,6 @@ module('Acceptance | exec', function(hooks) { allocation: allocation.id.split('-')[0], }); - await settled(); - await Exec.terminal.pressEnter(); await settled(); mockSocket.onopen(); From c75657774ae57e749e91ea5af504735d6201deae Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 3 Apr 2020 12:54:47 -0500 Subject: [PATCH 09/11] Remove redundant step assertion --- ui/tests/acceptance/exec-test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 5fd0b513e..29266916a 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -285,8 +285,6 @@ module('Acceptance | exec', function(hooks) { let mockSocket = new MockSocket(); let mockSockets = Service.extend({ getTaskStateSocket() { - assert.step('Socket built'); - return mockSocket; }, }); @@ -312,8 +310,6 @@ module('Acceptance | exec', function(hooks) { await settled(); mockSocket.onopen(); - assert.verifySteps(['Socket built']); - await Exec.terminal.pressEnter(); await settled(); From 5562abd7bf6cc696742c1ffc56f5535c7ddfeac1 Mon Sep 17 00:00:00 2001 From: Mahmood Ali Date: Fri, 3 Apr 2020 14:20:31 -0400 Subject: [PATCH 10/11] fixup! backend: support WS authentication handshake in alloc/exec --- command/agent/alloc_endpoint.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/command/agent/alloc_endpoint.go b/command/agent/alloc_endpoint.go index 950e6a80c..42d2d761d 100644 --- a/command/agent/alloc_endpoint.go +++ b/command/agent/alloc_endpoint.go @@ -407,9 +407,11 @@ func (s *HTTPServer) allocExec(allocID string, resp http.ResponseWriter, req *ht return s.execStreamImpl(conn, &args) } +// readWsHandshake reads the websocket handshake message and sets +// query authentication token, if request requires a handshake func readWsHandshake(readFn func(interface{}) error, req *http.Request, q *structs.QueryOptions) error { - // avoid handshake if request doesn't require one + // Avoid handshake if request doesn't require one if hv := req.URL.Query().Get("ws_handshake"); hv == "" { return nil } else if h, err := strconv.ParseBool(hv); err != nil { @@ -418,15 +420,14 @@ func readWsHandshake(readFn func(interface{}) error, req *http.Request, q *struc return nil } - fmt.Println("HERE") - var h wsHandshakeMessage err := readFn(&h) if err != nil { return err } - if h.Version != 1 { + supportedWSHandshakeVersion := 1 + if h.Version != supportedWSHandshakeVersion { return fmt.Errorf("unexpected handshake value: %v", h.Version) } From 4a92a27db774e32d179a183e43c0d9317ee9e87b Mon Sep 17 00:00:00 2001 From: Mahmood Ali Date: Fri, 3 Apr 2020 14:31:19 -0400 Subject: [PATCH 11/11] ui: explicit reference to window.localStorage --- ui/tests/acceptance/exec-test.js | 8 ++++---- ui/tests/acceptance/token-test.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index 29266916a..dcda0a6fa 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -10,8 +10,8 @@ module('Acceptance | exec', function(hooks) { setupMirage(hooks); hooks.beforeEach(async function() { - localStorage.clear(); - sessionStorage.clear(); + window.localStorage.clear(); + window.sessionStorage.clear(); server.create('agent'); server.create('node'); @@ -347,7 +347,7 @@ module('Acceptance | exec', function(hooks) { let mockSockets = Service.extend({ getTaskStateSocket(taskState, command) { assert.equal(command, '/sh'); - localStorage.getItem('nomadExecCommand', JSON.stringify('/sh')); + window.localStorage.getItem('nomadExecCommand', JSON.stringify('/sh')); assert.step('Socket built'); @@ -407,7 +407,7 @@ module('Acceptance | exec', function(hooks) { }); test('a persisted customised command is recalled', async function(assert) { - localStorage.setItem('nomadExecCommand', JSON.stringify('/bin/sh')); + window.localStorage.setItem('nomadExecCommand', JSON.stringify('/bin/sh')); let taskGroup = this.job.task_groups.models[0]; let task = taskGroup.tasks.models[0]; diff --git a/ui/tests/acceptance/token-test.js b/ui/tests/acceptance/token-test.js index 19e4d3870..c66c8f8e7 100644 --- a/ui/tests/acceptance/token-test.js +++ b/ui/tests/acceptance/token-test.js @@ -17,8 +17,8 @@ module('Acceptance | tokens', function(hooks) { setupMirage(hooks); hooks.beforeEach(function() { - localStorage.clear(); - sessionStorage.clear(); + window.localStorage.clear(); + window.sessionStorage.clear(); server.create('agent'); node = server.create('node');