Merge lp:~mterry/unity8/tablet-security into lp:unity8

Proposed by Michael Terry
Status: Merged
Approved by: Michael Zanetti
Approved revision: 1278
Merged at revision: 1350
Proposed branch: lp:~mterry/unity8/tablet-security
Merge into: lp:unity8
Diff against target: 917 lines (+518/-75)
9 files modified
qml/Greeter/Greeter.qml (+13/-1)
qml/Greeter/GreeterContent.qml (+12/-0)
qml/Greeter/LoginList.qml (+19/-6)
qml/Shell.qml (+74/-40)
tests/qmltests/CMakeLists.txt (+1/-0)
tests/qmltests/Greeter/tst_MultiGreeter.qml (+2/-2)
tests/qmltests/tst_Shell.qml (+1/-0)
tests/qmltests/tst_ShellWithPin.qml (+107/-26)
tests/qmltests/tst_TabletShell.qml (+289/-0)
To merge this branch: bzr merge lp:~mterry/unity8/tablet-security
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Needs Fixing
Michael Zanetti (community) Approve
Unity Team Pending
Review via email: mp+234219@code.launchpad.net

Commit message

Fix some security issues with the tablet greeter, which allowed the lockscreen to be bypassed. (LP: #1367715)

Recent greeter fixes for phone mode did not fully take the tablet scenario into account. This branch does three main things:

1) Rename fakeActiveForApp because it is nothing but confusing. I renamed the variable itself to lockedApp and added a new variable hasLockedApp, which is infinitely clearer than fakeActiveForApp !== "", which is what we used to use. Since that is a very security-sensitive variable, I figured it's best to be clear.

2) Closed a security bug where if the user could plug in a phone showing the emergency dialer to a bigger screen, thus switching to tablet mode, they could get access to other apps.

3) Closed a security bug where the tablet lockscreen could be left-swiped away.

4) Closed a security bug where the tablet would unlock itself if you launched an app from the indicators or launcher. Now it just focuses the prompt field (much like how the phone shows the lockscreen pin pad).

Description of the change

Fix some security issues with the tablet greeter, which allowed the lockscreen to be bypassed. (LP: #1367715)

Recent greeter fixes for phone mode did not fully take the tablet scenario into account. This branch does four main things:

1) Rename fakeActiveForApp because it is nothing but confusing. I renamed the variable itself to lockedApp and added a new variable hasLockedApp, which is infinitely clearer than fakeActiveForApp !== "", which is what we used to use. Since that is a very security-sensitive variable, I figured it's best to be clear.

2) Closed a security bug where if the user could plug in a phone showing the emergency dialer to a bigger screen, thus switching to tablet mode, they could get access to other apps.

3) Closed a security bug where the tablet lockscreen could be left-swiped away.

4) Closed a security bug where the tablet would unlock itself if you launched an app from the indicators or launcher. Now it just focuses the prompt field (much like how the phone shows the lockscreen pin pad).

This whole branch has really brought home how much the lockscreen/greeter interactions need to be tied more closely together and abstracted. Having them strewn throughout Shell.qml is error-prone and adds a lot of logic to the "master file" that I'd rather have in isolated qml files.

Ideally we'd have one bundled "Greeter" object that would expose a clear API to Shell.qml and would just "do the right thing" regardless of tablet, phone-with-pin, phone-with-passphrase, and phone-with-swipe.

But that's a bigger project for another day.

== Checklist ==

 * Are there any related MPs required for this MP to build/function as expected? Please list.
 No

 * Did you perform an exploratory manual test run of your code change and any related functionality?
 Yes

 * Did you make sure that your branch does not contain spurious tags?
 Yes

 * If you changed the packaging (debian), did you subscribe the ubuntu-unity team to this MP?
 NA

 * If you changed the UI, has there been a design review?
 NA

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
MichaƂ Sawicz (saviq) wrote :

Think we could get some tests confirming these behaviours?

Revision history for this message
Michael Terry (mterry) wrote :

Yeah, fair. Will work on that.

Revision history for this message
Michael Terry (mterry) wrote :

OK, tests added, review away.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~mterry/unity8/tablet-security updated
1267. By Michael Terry

Make ShellWithPin tests more reliable by waiting for rendering

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~mterry/unity8/tablet-security updated
1268. By Michael Terry

Make ShellWithPin tests more reliable

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Albert Astals Cid (aacid) wrote :

Text conflict in qml/Shell.qml
1 conflicts encountered.

lp:~mterry/unity8/tablet-security updated
1269. By Michael Terry

Merge from trunk

Revision history for this message
Michael Terry (mterry) wrote :

Merged from trunk.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~mterry/unity8/tablet-security updated
1270. By Michael Terry

Merge from trunk

1271. By Michael Terry

Fix tests

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~mterry/unity8/tablet-security updated
1272. By Michael Terry

Fix MultiGreeter qmluitest

Revision history for this message
Albert Astals Cid (aacid) wrote :

Text conflict in qml/Shell.qml
1 conflicts encountered.

lp:~mterry/unity8/tablet-security updated
1273. By Michael Terry

Merge from trunk

Revision history for this message
Michael Terry (mterry) wrote :

Merged from trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Michael Zanetti (mzanetti) wrote :

hmm... running tryTabletShell I can still unlock it by doing a left edge swipe...

Also see one inline comment.

testing it on the phone seems to behave as expected

review: Needs Fixing
Revision history for this message
Michael Terry (mterry) wrote :

Replied inline to your comment, will look at tryTabletShell.

lp:~mterry/unity8/tablet-security updated
1274. By Michael Terry

Merge from trunk

1275. By Michael Terry

Prevent dash logins in tablet mode

Revision history for this message
Michael Terry (mterry) wrote :

OK, fixed the left-drag-unlock for locked users! Whoops, that was the result of a bad merge.

lp:~mterry/unity8/tablet-security updated
1276. By Michael Terry

Make password login test a little more reliable

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~mterry/unity8/tablet-security updated
1277. By Michael Terry

expand comments

1278. By Michael Terry

Add wait(500) to properly test leftEdgeDrag

Revision history for this message
Michael Zanetti (mzanetti) wrote :

 * Did you perform an exploratory manual test run of the code change and any related functionality?

yes

 * Did CI run pass? If not, please explain why.

seems dead currently :/

 * Did you make sure that the branch does not contain spurious tags?

yes

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'qml/Greeter/Greeter.qml'
2--- qml/Greeter/Greeter.qml 2014-09-15 16:41:54 +0000
3+++ qml/Greeter/Greeter.qml 2014-10-08 20:37:01 +0000
4@@ -36,7 +36,7 @@
5
6 property alias dragHandleWidth: dragHandle.width
7 property alias model: greeterContentLoader.model
8- property bool locked: shown && !LightDM.Greeter.promptless
9+ property bool locked: true
10
11 readonly property bool narrowMode: !multiUser && height > width
12 readonly property bool multiUser: LightDM.Users.count > 1
13@@ -57,6 +57,18 @@
14 }
15 }
16
17+ function tryToUnlock() {
18+ if (created) {
19+ greeterContentLoader.item.tryToUnlock()
20+ }
21+ }
22+
23+ function reset() {
24+ if (created) {
25+ greeterContentLoader.item.reset()
26+ }
27+ }
28+
29 onRequiredChanged: {
30 // Reset hide animation to default once we're finished with it
31 if (required) {
32
33=== modified file 'qml/Greeter/GreeterContent.qml'
34--- qml/Greeter/GreeterContent.qml 2014-09-25 18:30:32 +0000
35+++ qml/Greeter/GreeterContent.qml 2014-10-08 20:37:01 +0000
36@@ -29,6 +29,18 @@
37 signal selected(int uid)
38 signal unlocked(int uid)
39
40+ function tryToUnlock() {
41+ if (loginLoader.item) {
42+ loginLoader.item.tryToUnlock()
43+ }
44+ }
45+
46+ function reset() {
47+ if (loginLoader.item) {
48+ loginLoader.item.reset()
49+ }
50+ }
51+
52 Rectangle {
53 // In case background fails to load
54 id: backgroundBackup
55
56=== modified file 'qml/Greeter/LoginList.qml'
57--- qml/Greeter/LoginList.qml 2014-06-20 23:04:15 +0000
58+++ qml/Greeter/LoginList.qml 2014-10-08 20:37:01 +0000
59@@ -35,6 +35,22 @@
60 signal selected(int uid)
61 signal unlocked(int uid)
62
63+ function tryToUnlock() {
64+ if (LightDM.Greeter.promptless) {
65+ if (LightDM.Greeter.authenticated) {
66+ root.unlocked(userList.currentIndex)
67+ } else {
68+ root.resetAuthentication()
69+ }
70+ } else {
71+ passwordInput.forceActiveFocus()
72+ }
73+ }
74+
75+ function reset() {
76+ root.resetAuthentication()
77+ }
78+
79 Keys.onEscapePressed: root.resetAuthentication()
80
81 Rectangle {
82@@ -229,14 +245,11 @@
83 }
84
85 MouseArea {
86+ id: passwordMouseArea
87+ objectName: "passwordMouseArea"
88 anchors.fill: passwordInput
89 enabled: LightDM.Greeter.promptless
90- onClicked: {
91- if (LightDM.Greeter.authenticated)
92- root.unlocked(userList.currentIndex);
93- else
94- root.resetAuthentication();
95- }
96+ onClicked: root.tryToUnlock()
97 }
98
99 function resetAuthentication() {
100
101=== modified file 'qml/Shell.qml'
102--- qml/Shell.qml 2014-10-06 08:02:44 +0000
103+++ qml/Shell.qml 2014-10-08 20:37:01 +0000
104@@ -87,9 +87,9 @@
105 }
106 }
107
108- function setFakeActiveForApp(app) {
109+ function setLockedApp(app) {
110 if (shell.locked) {
111- greeter.fakeActiveForApp = app
112+ greeter.lockedApp = app
113 lockscreen.hide()
114 }
115 }
116@@ -169,31 +169,37 @@
117 Connections {
118 target: ApplicationManager
119 onFocusRequested: {
120- if (appId === "dialer-app") {
121- // Always let the dialer-app through. Either user asked
122- // for an emergency call or accepted an incoming call.
123- setFakeActiveForApp(appId)
124- } else if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== appId) {
125- lockscreen.show();
126+ if (greeter.narrowMode) {
127+ if (appId === "dialer-app") {
128+ // Always let the dialer-app through. Either user asked
129+ // for an emergency call or accepted an incoming call.
130+ setLockedApp(appId)
131+ } else if (greeter.hasLockedApp && greeter.lockedApp !== appId) {
132+ greeter.startUnlock()
133+ }
134+ greeter.hide();
135+ } else {
136+ if (LightDM.Greeter.active) {
137+ greeter.startUnlock()
138+ }
139 }
140- greeter.hide();
141 }
142
143 onFocusedApplicationIdChanged: {
144- if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== ApplicationManager.focusedApplicationId) {
145- lockscreen.show();
146+ if (greeter.hasLockedApp && greeter.lockedApp !== ApplicationManager.focusedApplicationId) {
147+ greeter.startUnlock()
148 }
149 panel.indicators.hide();
150 }
151
152 onApplicationAdded: {
153 if (greeter.shown && appId != "unity8-dash") {
154- greeter.hide();
155+ greeter.startUnlock()
156 }
157- if (appId === "dialer-app") {
158+ if (greeter.narrowMode && appId === "dialer-app") {
159 // Always let the dialer-app through. Either user asked
160 // for an emergency call or accepted an incoming call.
161- setFakeActiveForApp(appId)
162+ setLockedApp(appId)
163 }
164 launcher.hide();
165 }
166@@ -201,9 +207,19 @@
167
168 Loader {
169 id: applicationsDisplayLoader
170+ objectName: "applicationsDisplayLoader"
171 anchors.fill: parent
172
173- source: shell.sideStageEnabled ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
174+ // When we have a locked app, we only want to show that one app.
175+ // FIXME: do this in a less traumatic way. We currently only allow
176+ // locked apps in phone mode (see FIXME in Lockscreen component in
177+ // this same file). When that changes, we need to do something
178+ // nicer here. But this code is currently just to prevent a
179+ // theoretical attack where user enters lockedApp mode, then makes
180+ // the screen larger (maybe connects to monitor) and tries to enter
181+ // tablet mode.
182+ property bool tabletMode: shell.sideStageEnabled && !greeter.hasLockedApp
183+ source: tabletMode ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
184
185 Binding {
186 target: applicationsDisplayLoader.item
187@@ -229,7 +245,7 @@
188 Binding {
189 target: applicationsDisplayLoader.item
190 property: "spreadEnabled"
191- value: edgeDemo.stagesEnabled && greeter.fakeActiveForApp === "" // to support emergency dialer hack
192+ value: edgeDemo.stagesEnabled && !greeter.hasLockedApp
193 }
194 Binding {
195 target: applicationsDisplayLoader.item
196@@ -304,14 +320,14 @@
197 // and wider screens are tablets which don't. When we do allow this
198 // on devices with a side stage and a SIM, work should be done to
199 // ensure that the main stage is disabled while the dialer is present
200- // in the side stage.
201+ // in the side stage. See the FIXME in the stage loader in this file.
202 showEmergencyCallButton: !shell.sideStageEnabled
203
204 onEntered: LightDM.Greeter.respond(passphrase);
205 onCancel: greeter.show()
206- onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter fake-active mode
207+ onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter locked-app mode
208
209- onShownChanged: if (shown) greeter.fakeActiveForApp = ""
210+ onShownChanged: if (shown) greeter.lockedApp = ""
211
212 Timer {
213 id: forcedDelayTimer
214@@ -360,11 +376,13 @@
215 }
216
217 onPromptlessChanged: {
218- if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
219- lockscreen.hide()
220- } else {
221- lockscreen.reset();
222- lockscreen.show();
223+ if (greeter.narrowMode) {
224+ if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
225+ lockscreen.hide()
226+ } else {
227+ lockscreen.reset();
228+ lockscreen.show();
229+ }
230 }
231 }
232
233@@ -414,7 +432,7 @@
234 Binding {
235 target: LightDM.Greeter
236 property: "active"
237- value: greeter.shown || lockscreen.shown || greeter.fakeActiveForApp != ""
238+ value: greeter.shown || lockscreen.shown || greeter.hasLockedApp
239 }
240
241 Rectangle {
242@@ -426,7 +444,8 @@
243 Item {
244 // Just a tiny wrapper to adjust greeter's x without messing with its own dragging
245 id: greeterWrapper
246- x: launcher.progress
247+ objectName: "greeterWrapper"
248+ x: greeter.narrowMode ? launcher.progress : 0
249 y: panel.panelHeight
250 width: parent.width
251 height: parent.height - panel.panelHeight
252@@ -439,7 +458,7 @@
253 property bool fullyShown: showProgress === 1.0
254 onFullyShownChanged: {
255 // Wait until the greeter is completely covering lockscreen before resetting it.
256- if (fullyShown && !LightDM.Greeter.authenticated) {
257+ if (greeter.narrowMode && fullyShown && !LightDM.Greeter.authenticated) {
258 lockscreen.reset();
259 lockscreen.show();
260 }
261@@ -448,7 +467,7 @@
262 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
263 onShowProgressChanged: {
264 if (showProgress === 0) {
265- if (LightDM.Greeter.authenticated) {
266+ if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
267 greeter.login()
268 } else if (greeter.narrowMode) {
269 lockscreen.clear(false) // to reset focus if necessary
270@@ -462,13 +481,16 @@
271
272 signal sessionStarted() // helpful for tests
273
274- property string fakeActiveForApp: ""
275+ property string lockedApp: ""
276+ property bool hasLockedApp: lockedApp !== ""
277
278 available: true
279 hides: [launcher, panel.indicators]
280 shown: true
281 loadContent: required || lockscreen.required // keeps content in memory for quick show()
282
283+ locked: shell.locked
284+
285 defaultBackground: shell.background
286
287 width: parent.width
288@@ -476,6 +498,18 @@
289
290 dragHandleWidth: shell.edgeSize
291
292+ function startUnlock() {
293+ if (narrowMode) {
294+ if (!LightDM.Greeter.authenticated) {
295+ lockscreen.show()
296+ }
297+ hide()
298+ } else {
299+ show()
300+ tryToUnlock()
301+ }
302+ }
303+
304 function login() {
305 enabled = false;
306 if (LightDM.Greeter.startSessionSync()) {
307@@ -491,8 +525,10 @@
308 if (shown) {
309 if (greeter.narrowMode) {
310 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
311+ } else {
312+ reset()
313 }
314- greeter.fakeActiveForApp = "";
315+ greeter.lockedApp = "";
316 greeter.forceActiveFocus();
317 }
318 }
319@@ -537,10 +573,7 @@
320 }
321
322 if (LightDM.Greeter.active) {
323- if (!LightDM.Greeter.authenticated) {
324- lockscreen.show()
325- }
326- greeter.hide()
327+ greeter.startUnlock()
328 }
329
330 var animate = !LightDM.Greeter.active && !stages.shown
331@@ -549,7 +582,8 @@
332 }
333
334 function showDash() {
335- if (greeter.fakeActiveForApp !== "") { // just in case user gets here
336+ if (greeter.hasLockedApp || // just in case user gets here
337+ (!greeter.narrowMode && shell.locked)) {
338 return
339 }
340
341@@ -576,7 +610,7 @@
342 anchors.fill: parent //because this draws indicator menus
343 indicators {
344 hides: [launcher]
345- available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && greeter.fakeActiveForApp === ""
346+ available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && !greeter.hasLockedApp
347 contentEnabled: edgeDemo.panelContentEnabled
348 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
349 panelHeight: units.gu(3)
350@@ -587,7 +621,7 @@
351 ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
352
353 fullscreenMode: (topmostApplicationIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
354- || greeter.fakeActiveForApp !== ""
355+ || greeter.hasLockedApp
356 }
357
358 Launcher {
359@@ -600,7 +634,7 @@
360 anchors.bottom: parent.bottom
361 width: parent.width
362 dragAreaWidth: shell.edgeSize
363- available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && greeter.fakeActiveForApp === ""
364+ available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && !greeter.hasLockedApp
365
366 onShowDashHome: showHome()
367 onDash: showDash()
368@@ -610,8 +644,8 @@
369 }
370 }
371 onLauncherApplicationSelected: {
372- if (greeter.fakeActiveForApp !== "") {
373- lockscreen.show()
374+ if (greeter.hasLockedApp) {
375+ greeter.startUnlock()
376 }
377 if (!edgeDemo.running)
378 shell.activateApplication(appId)
379
380=== modified file 'tests/qmltests/CMakeLists.txt'
381--- tests/qmltests/CMakeLists.txt 2014-09-30 16:56:24 +0000
382+++ tests/qmltests/CMakeLists.txt 2014-10-08 20:37:01 +0000
383@@ -21,6 +21,7 @@
384
385 add_qml_test(. Shell ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single")
386 add_qml_test(. ShellWithPin ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single-pin")
387+add_qml_test(. TabletShell ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/full")
388 add_qml_test(Components Background)
389 add_qml_test(Components Carousel)
390 add_qml_test(Components DraggingArea)
391
392=== modified file 'tests/qmltests/Greeter/tst_MultiGreeter.qml'
393--- tests/qmltests/Greeter/tst_MultiGreeter.qml 2014-06-02 14:10:59 +0000
394+++ tests/qmltests/Greeter/tst_MultiGreeter.qml 2014-10-08 20:37:01 +0000
395@@ -29,6 +29,7 @@
396 Greeter {
397 id: greeter
398 anchors.fill: parent
399+ locked: !LightDM.Greeter.authenticated
400 }
401
402 Component {
403@@ -115,8 +116,7 @@
404 var waitForSignal = data.uid != 0 && userList.currentIndex != data.uid
405 select_index(data.uid)
406 tryCompare(userList, "currentIndex", data.uid)
407- tryCompare(greeter, "locked", data.tag !== "no-password" &&
408- data.tag !== "auth-error")
409+ tryCompare(greeter, "locked", data.tag !== "no-password")
410 if (waitForSignal) {
411 selectionSpy.wait()
412 tryCompare(selectionSpy, "count", 1)
413
414=== modified file 'tests/qmltests/tst_Shell.qml'
415--- tests/qmltests/tst_Shell.qml 2014-10-06 07:59:50 +0000
416+++ tests/qmltests/tst_Shell.qml 2014-10-08 20:37:01 +0000
417@@ -484,6 +484,7 @@
418 tryCompare(greeter, "showProgress", 0)
419 waitForRendering(greeter);
420 LightDM.Greeter.showGreeter()
421+ waitForRendering(greeter)
422 tryCompare(greeter, "showProgress", 1)
423 LightDM.Greeter.hideGreeter()
424 tryCompare(greeter, "showProgress", 0)
425
426=== modified file 'tests/qmltests/tst_ShellWithPin.qml'
427--- tests/qmltests/tst_ShellWithPin.qml 2014-09-15 16:41:54 +0000
428+++ tests/qmltests/tst_ShellWithPin.qml 2014-10-08 20:37:01 +0000
429@@ -27,10 +27,9 @@
430
431 import "../../qml"
432
433-Item {
434+Row {
435 id: root
436- width: shell.width + units.gu(20)
437- height: shell.height
438+ spacing: 0
439
440 QtObject {
441 id: applicationArguments
442@@ -48,21 +47,55 @@
443 }
444 }
445
446- Shell {
447- id: shell
448- maxFailedLogins: maxRetriesTextField.text
449+ Loader {
450+ id: shellLoader
451+
452+ width: units.gu(40)
453+ height: units.gu(71)
454+
455+ property bool itemDestroyed: false
456+ sourceComponent: Component {
457+ Shell {
458+ Component.onDestruction: {
459+ shellLoader.itemDestroyed = true
460+ }
461+ maxFailedLogins: maxRetriesTextField.text
462+ }
463+ }
464 }
465- Column {
466- anchors { top: parent.top; right: parent.right; bottom: parent.bottom; margins:units.gu(1) }
467- width: units.gu(18)
468-
469- Label {
470- text: "Max retries:"
471- color: "black"
472- }
473- TextField {
474- id: maxRetriesTextField
475- text: "-1"
476+
477+ Rectangle {
478+ color: "white"
479+ width: units.gu(30)
480+ height: shellLoader.height
481+
482+ Column {
483+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
484+ spacing: units.gu(1)
485+ Row {
486+ anchors { left: parent.left; right: parent.right }
487+ Button {
488+ text: "Show Greeter"
489+ onClicked: {
490+ if (shellLoader.status !== Loader.Ready)
491+ return
492+
493+ var greeter = testCase.findChild(shellLoader.item, "greeter")
494+ if (!greeter.shown) {
495+ greeter.show()
496+ }
497+ }
498+ }
499+ }
500+
501+ Label {
502+ text: "Max retries:"
503+ color: "black"
504+ }
505+ TextField {
506+ id: maxRetriesTextField
507+ text: "-1"
508+ }
509 }
510 }
511
512@@ -81,12 +114,10 @@
513 name: "ShellWithPin"
514 when: windowShown
515
516- function initTestCase() {
517+ property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
518
519+ function init() {
520 sessionSpy.target = findChild(shell, "greeter")
521- }
522-
523- function init() {
524 swipeAwayGreeter()
525 shell.failedLoginsDelayAttempts = -1
526 maxRetriesTextField.text = "-1"
527@@ -95,12 +126,28 @@
528 }
529
530 function cleanup() {
531- LightDM.Greeter.showGreeter()
532- var greeter = findChild(shell, "greeter")
533- tryCompare(greeter, "showProgress", 1)
534+ shellLoader.itemDestroyed = false
535+
536+ shellLoader.active = false
537+
538+ tryCompare(shellLoader, "status", Loader.Null)
539+ tryCompare(shellLoader, "item", null)
540+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
541+ // item might still be alive. So if we set Loader.active back to true
542+ // again right now we will get the very same Shell instance back. So no reload
543+ // actually took place. Likely because Loader waits until the next event loop
544+ // iteration to do its work. So to ensure the reload, we will wait until the
545+ // Shell instance gets destroyed.
546+ tryCompare(shellLoader, "itemDestroyed", true)
547
548 // kill all (fake) running apps
549 killApps()
550+
551+ // reload our test subject to get it in a fresh state once again
552+ shellLoader.active = true
553+
554+ tryCompare(shellLoader, "status", Loader.Ready)
555+ removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item)
556 }
557
558 function killApps() {
559@@ -113,6 +160,7 @@
560
561 function swipeAwayGreeter() {
562 var greeter = findChild(shell, "greeter");
563+ waitForRendering(greeter)
564 tryCompare(greeter, "showProgress", 1);
565
566 var touchX = shell.width - (shell.edgeSize / 2);
567@@ -168,7 +216,8 @@
568
569 mouseClick(emergencyButton, units.gu(1), units.gu(1))
570
571- tryCompare(greeter, "fakeActiveForApp", "dialer-app")
572+ tryCompare(greeter, "lockedApp", "dialer-app")
573+ tryCompare(greeter, "hasLockedApp", true)
574 tryCompare(lockscreen, "shown", false)
575 tryCompare(panel, "fullscreenMode", true)
576 tryCompare(indicators, "available", false)
577@@ -180,7 +229,8 @@
578 LightDM.Greeter.showGreeter()
579
580 tryCompare(greeter, "shown", true)
581- tryCompare(greeter, "fakeActiveForApp", "")
582+ tryCompare(greeter, "lockedApp", "")
583+ tryCompare(greeter, "hasLockedApp", false)
584 tryCompare(lockscreen, "shown", true)
585 tryCompare(panel, "fullscreenMode", false)
586 tryCompare(indicators, "available", true)
587@@ -253,5 +303,36 @@
588 enterPin("1111")
589 tryCompare(resetSpy, "count", 1)
590 }
591+
592+ function test_emergencyDialerLockOut() {
593+ // This is a theoretical attack on the lockscreen: Enter emergency
594+ // dialer mode on a phone, then plug into a larger screen,
595+ // switching to a tablet interface. This would in theory move the
596+ // dialer to a side stage and give access to other apps. So just
597+ // confirm that such an attack doesn't work.
598+
599+ var applicationsDisplayLoader = findChild(shell, "applicationsDisplayLoader")
600+
601+ // We start in phone mode
602+ tryCompare(shell, "sideStageEnabled", false)
603+ tryCompare(applicationsDisplayLoader, "tabletMode", false)
604+
605+ var app = ApplicationManager.startApplication("dialer-app")
606+
607+ var greeter = findChild(shell, "greeter")
608+ tryCompare(greeter, "showProgress", 0)
609+ tryCompare(greeter, "hasLockedApp", true)
610+
611+ // OK, we're in. Now try (but fail) to switch to tablet mode
612+ shell.tablet = true
613+ tryCompare(shell, "sideStageEnabled", true)
614+ tryCompare(applicationsDisplayLoader, "tabletMode", false)
615+
616+ // And when we kill the app, we go back to locked tablet mode
617+ killApps()
618+ tryCompare(greeter, "showProgress", 1)
619+ tryCompare(shell, "sideStageEnabled", true)
620+ tryCompare(applicationsDisplayLoader, "tabletMode", true)
621+ }
622 }
623 }
624
625=== added file 'tests/qmltests/tst_TabletShell.qml'
626--- tests/qmltests/tst_TabletShell.qml 1970-01-01 00:00:00 +0000
627+++ tests/qmltests/tst_TabletShell.qml 2014-10-08 20:37:01 +0000
628@@ -0,0 +1,289 @@
629+/*
630+ * Copyright (C) 2013,2014 Canonical, Ltd.
631+ *
632+ * This program is free software; you can redistribute it and/or modify
633+ * it under the terms of the GNU General Public License as published by
634+ * the Free Software Foundation; version 3.
635+ *
636+ * This program is distributed in the hope that it will be useful,
637+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
638+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
639+ * GNU General Public License for more details.
640+ *
641+ * You should have received a copy of the GNU General Public License
642+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
643+ */
644+
645+import QtQuick 2.0
646+import QtTest 1.0
647+import GSettings 1.0
648+import LightDM 0.1 as LightDM
649+import Ubuntu.Components 1.1
650+import Ubuntu.Telephony 0.1 as Telephony
651+import Unity.Application 0.1
652+import Unity.Connectivity 0.1
653+import Unity.Test 0.1 as UT
654+import Powerd 0.1
655+
656+import "../../qml"
657+
658+Row {
659+ id: root
660+ spacing: 0
661+
662+ QtObject {
663+ id: applicationArguments
664+
665+ function hasGeometry() {
666+ return false
667+ }
668+
669+ function width() {
670+ return 0
671+ }
672+
673+ function height() {
674+ return 0
675+ }
676+ }
677+
678+ Loader {
679+ id: shellLoader
680+
681+ width: units.gu(100)
682+ height: units.gu(80)
683+
684+ property bool itemDestroyed: false
685+ sourceComponent: Component {
686+ Shell {
687+ Component.onDestruction: {
688+ shellLoader.itemDestroyed = true
689+ }
690+ }
691+ }
692+ }
693+
694+ Rectangle {
695+ color: "white"
696+ width: units.gu(20)
697+ height: shellLoader.height
698+
699+ Column {
700+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
701+ spacing: units.gu(1)
702+ Row {
703+ anchors { left: parent.left; right: parent.right }
704+ Button {
705+ text: "Show Greeter"
706+ onClicked: {
707+ if (shellLoader.status !== Loader.Ready)
708+ return
709+
710+ var greeter = testCase.findChild(shellLoader.item, "greeter")
711+ if (!greeter.shown) {
712+ greeter.show()
713+ }
714+ }
715+ }
716+ }
717+ }
718+ }
719+
720+ SignalSpy {
721+ id: sessionSpy
722+ signalName: "sessionStarted"
723+ }
724+
725+ SignalSpy {
726+ id: dashCommunicatorSpy
727+ signalName: "setCurrentScopeCalled"
728+ }
729+
730+ SignalSpy {
731+ id: unlockAllModemsSpy
732+ target: Connectivity
733+ signalName: "unlockingAllModems"
734+ }
735+
736+ Telephony.CallEntry {
737+ id: phoneCall
738+ phoneNumber: "+447812221111"
739+ }
740+
741+ UT.UnityTestCase {
742+ id: testCase
743+ name: "TabletShell"
744+ when: windowShown
745+
746+ property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
747+
748+ function init() {
749+ sessionSpy.clear()
750+ sessionSpy.target = findChild(shell, "greeter")
751+ dashCommunicatorSpy.target = findInvisibleChild(shell, "dashCommunicator")
752+ }
753+
754+ function cleanup() {
755+ shellLoader.itemDestroyed = false
756+
757+ shellLoader.active = false
758+
759+ tryCompare(shellLoader, "status", Loader.Null)
760+ tryCompare(shellLoader, "item", null)
761+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
762+ // item might still be alive. So if we set Loader.active back to true
763+ // again right now we will get the very same Shell instance back. So no reload
764+ // actually took place. Likely because Loader waits until the next event loop
765+ // iteration to do its work. So to ensure the reload, we will wait until the
766+ // Shell instance gets destroyed.
767+ tryCompare(shellLoader, "itemDestroyed", true)
768+
769+ // kill all (fake) running apps
770+ killApps()
771+
772+ unlockAllModemsSpy.clear()
773+
774+ // reload our test subject to get it in a fresh state once again
775+ shellLoader.active = true
776+
777+ tryCompare(shellLoader, "status", Loader.Ready)
778+ removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item)
779+ }
780+
781+ function killApps() {
782+ while (ApplicationManager.count > 1) {
783+ var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
784+ ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId)
785+ }
786+ compare(ApplicationManager.count, 1)
787+ }
788+
789+ function selectIndex(i) {
790+ // We could be anywhere in list; find target index to know which direction
791+ var greeter = findChild(shell, "greeter")
792+ var userlist = findChild(greeter, "userList")
793+ if (userlist.currentIndex == i)
794+ keyClick(Qt.Key_Escape) // Reset state if we're not moving
795+ while (userlist.currentIndex != i) {
796+ var next = userlist.currentIndex + 1
797+ if (userlist.currentIndex > i) {
798+ next = userlist.currentIndex - 1
799+ }
800+ var account = findChild(greeter, "username"+next)
801+ mouseClick(account, 1, 1)
802+ tryCompare(userlist, "currentIndex", next)
803+ tryCompare(userlist, "movingInternally", false)
804+ }
805+ }
806+
807+ function selectUser(name) {
808+ // Find index of user with the right name
809+ var greeter = findChild(shell, "greeter")
810+ for (var i = 0; i < greeter.model.count; i++) {
811+ if (greeter.model.data(i, LightDM.UserRoles.NameRole) == name) {
812+ break
813+ }
814+ }
815+ if (i == greeter.model.count) {
816+ fail("Didn't find name")
817+ return -1
818+ }
819+ selectIndex(i)
820+ return i
821+ }
822+
823+ function clickPasswordInput(isButton) {
824+ var greeter = findChild(shell, "greeter")
825+ tryCompare(greeter, "showProgress", 1)
826+
827+ var passwordMouseArea = findChild(shell, "passwordMouseArea")
828+ tryCompare(passwordMouseArea, "enabled", isButton)
829+
830+ var passwordInput = findChild(shell, "passwordInput")
831+ mouseClick(passwordInput, passwordInput.width / 2, passwordInput.height / 2)
832+ }
833+
834+ function confirmLoggedIn(loggedIn) {
835+ var greeterWrapper = findChild(shell, "greeterWrapper")
836+ tryCompare(greeterWrapper, "showProgress", loggedIn ? 0 : 1)
837+ tryCompare(sessionSpy, "count", loggedIn ? 1 : 0)
838+ }
839+
840+ function swipeFromLeftEdge(swipeLength) {
841+ var touchStartX = 2
842+ var touchStartY = shell.height / 2
843+ touchFlick(shell, touchStartX, touchStartY, swipeLength, touchStartY)
844+ }
845+
846+ function test_noLockscreen() {
847+ selectUser("has-password")
848+ var lockscreen = findChild(shell, "lockscreen")
849+ tryCompare(lockscreen, "shown", false)
850+ }
851+
852+ function test_showAndHideGreeterDBusCalls() {
853+ var greeter = findChild(shell, "greeter")
854+ LightDM.Greeter.hideGreeter()
855+ tryCompare(greeter, "showProgress", 0)
856+ LightDM.Greeter.showGreeter()
857+ tryCompare(greeter, "showProgress", 1)
858+ }
859+
860+ function test_login_data() {
861+ return [
862+ {tag: "auth error", user: "auth-error", loggedIn: false, password: ""},
863+ {tag: "with password", user: "has-password", loggedIn: true, password: "password"},
864+ {tag: "without password", user: "no-password", loggedIn: true, password: ""},
865+ ]
866+ }
867+
868+ function test_login(data) {
869+ selectUser(data.user)
870+
871+ clickPasswordInput(data.password === "")
872+
873+ if (data.password !== "") {
874+ typeString(data.password)
875+ keyClick(Qt.Key_Enter)
876+ }
877+
878+ confirmLoggedIn(data.loggedIn)
879+ }
880+
881+ function test_appLaunchDuringGreeter_data() {
882+ return [
883+ {tag: "auth error", user: "auth-error", loggedIn: false, passwordFocus: false},
884+ {tag: "without password", user: "no-password", loggedIn: true, passwordFocus: false},
885+ {tag: "with password", user: "has-password", loggedIn: false, passwordFocus: true},
886+ ]
887+ }
888+
889+ function test_appLaunchDuringGreeter(data) {
890+ selectUser(data.user)
891+
892+ var greeter = findChild(shell, "greeter")
893+ var app = ApplicationManager.startApplication("dialer-app")
894+
895+ confirmLoggedIn(data.loggedIn)
896+
897+ if (data.passwordFocus) {
898+ var passwordInput = findChild(greeter, "passwordInput")
899+ tryCompare(passwordInput, "focus", true)
900+ }
901+ }
902+
903+ function test_leftEdgeDrag_data() {
904+ return [
905+ {tag: "without password", user: "no-password", loggedIn: true},
906+ {tag: "with password", user: "has-password", loggedIn: false},
907+ ]
908+ }
909+
910+ function test_leftEdgeDrag(data) {
911+ selectUser(data.user)
912+ swipeFromLeftEdge(shell.width * 0.75)
913+ wait(500) // to give time to handle dash() signal from Launcher
914+ confirmLoggedIn(data.loggedIn)
915+ }
916+ }
917+}

Subscribers

People subscribed via source and target branches