on xenial help sign in
Fix some security issues with the tablet greeter, which allowed the lockscreen to be bypassed. (LP: #1367715)
5e81d15Michael Terry
qml/Greeter/Greeter.qml+13−1
@@ -36,7 +36,7 @@ Showable {
36
36
37 property alias dragHandleWidth: dragHandle.width
37 property alias dragHandleWidth: dragHandle.width
38 property alias model: greeterContentLoader.model
38 property alias model: greeterContentLoader.model
39− property bool locked: shown && !LightDM.Greeter.promptless
39+ property bool locked: true
40
40
41 readonly property bool narrowMode: !multiUser && height > width
41 readonly property bool narrowMode: !multiUser && height > width
42 readonly property bool multiUser: LightDM.Users.count > 1
42 readonly property bool multiUser: LightDM.Users.count > 1
@@ -57,6 +57,18 @@ Showable {
57 }
57 }
58 }
58 }
59
59
60+ function tryToUnlock() {
61+ if (created) {
62+ greeterContentLoader.item.tryToUnlock()
63+ }
64+ }
65+
66+ function reset() {
67+ if (created) {
68+ greeterContentLoader.item.reset()
69+ }
70+ }
71+
60 onRequiredChanged: {
72 onRequiredChanged: {
61 // Reset hide animation to default once we're finished with it
73 // Reset hide animation to default once we're finished with it
62 if (required) {
74 if (required) {
qml/Greeter/GreeterContent.qml+12
@@ -29,6 +29,18 @@ Item {
29 signal selected(int uid)
29 signal selected(int uid)
30 signal unlocked(int uid)
30 signal unlocked(int uid)
31
31
32+ function tryToUnlock() {
33+ if (loginLoader.item) {
34+ loginLoader.item.tryToUnlock()
35+ }
36+ }
37+
38+ function reset() {
39+ if (loginLoader.item) {
40+ loginLoader.item.reset()
41+ }
42+ }
43+
32 Rectangle {
44 Rectangle {
33 // In case background fails to load
45 // In case background fails to load
34 id: backgroundBackup
46 id: backgroundBackup
qml/Greeter/LoginList.qml+19−6
@@ -35,6 +35,22 @@ Item {
35 signal selected(int uid)
35 signal selected(int uid)
36 signal unlocked(int uid)
36 signal unlocked(int uid)
37
37
38+ function tryToUnlock() {
39+ if (LightDM.Greeter.promptless) {
40+ if (LightDM.Greeter.authenticated) {
41+ root.unlocked(userList.currentIndex)
42+ } else {
43+ root.resetAuthentication()
44+ }
45+ } else {
46+ passwordInput.forceActiveFocus()
47+ }
48+ }
49+
50+ function reset() {
51+ root.resetAuthentication()
52+ }
53+
38 Keys.onEscapePressed: root.resetAuthentication()
54 Keys.onEscapePressed: root.resetAuthentication()
39
55
40 Rectangle {
56 Rectangle {
@@ -229,14 +245,11 @@ Item {
229 }
245 }
230
246
231 MouseArea {
247 MouseArea {
248+ id: passwordMouseArea
249+ objectName: "passwordMouseArea"
232 anchors.fill: passwordInput
250 anchors.fill: passwordInput
233 enabled: LightDM.Greeter.promptless
251 enabled: LightDM.Greeter.promptless
234− onClicked: {
252+ onClicked: root.tryToUnlock()
235− if (LightDM.Greeter.authenticated)
236− root.unlocked(userList.currentIndex);
237− else
238− root.resetAuthentication();
239− }
240 }
253 }
241
254
242 function resetAuthentication() {
255 function resetAuthentication() {
qml/Shell.qml+74−40
@@ -87,9 +87,9 @@ Item {
87 }
87 }
88 }
88 }
89
89
90− function setFakeActiveForApp(app) {
90+ function setLockedApp(app) {
91 if (shell.locked) {
91 if (shell.locked) {
92− greeter.fakeActiveForApp = app
92+ greeter.lockedApp = app
93 lockscreen.hide()
93 lockscreen.hide()
94 }
94 }
95 }
95 }
@@ -169,31 +169,37 @@ Item {
169 Connections {
169 Connections {
170 target: ApplicationManager
170 target: ApplicationManager
171 onFocusRequested: {
171 onFocusRequested: {
172+ if (greeter.narrowMode) {
172− if (appId === "dialer-app") {
173+ if (appId === "dialer-app") {
173− // Always let the dialer-app through. Either user asked
174+ // Always let the dialer-app through. Either user asked
174− // for an emergency call or accepted an incoming call.
175+ // for an emergency call or accepted an incoming call.
175− setFakeActiveForApp(appId)
176+ setLockedApp(appId)
176− } else if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== appId) {
177+ } else if (greeter.hasLockedApp && greeter.lockedApp !== appId) {
177− lockscreen.show();
178+ greeter.startUnlock()
179+ }
180+ greeter.hide();
181+ } else {
182+ if (LightDM.Greeter.active) {
183+ greeter.startUnlock()
184+ }
178 }
185 }
179− greeter.hide();
180 }
186 }
181
187
182 onFocusedApplicationIdChanged: {
188 onFocusedApplicationIdChanged: {
183− if (greeter.fakeActiveForApp !== "" && greeter.fakeActiveForApp !== ApplicationManager.focusedApplicationId) {
189+ if (greeter.hasLockedApp && greeter.lockedApp !== ApplicationManager.focusedApplicationId) {
184− lockscreen.show();
190+ greeter.startUnlock()
185 }
191 }
186 panel.indicators.hide();
192 panel.indicators.hide();
187 }
193 }
188
194
189 onApplicationAdded: {
195 onApplicationAdded: {
190 if (greeter.shown && appId != "unity8-dash") {
196 if (greeter.shown && appId != "unity8-dash") {
191− greeter.hide();
197+ greeter.startUnlock()
192 }
198 }
193− if (appId === "dialer-app") {
199+ if (greeter.narrowMode && appId === "dialer-app") {
194 // Always let the dialer-app through. Either user asked
200 // Always let the dialer-app through. Either user asked
195 // for an emergency call or accepted an incoming call.
201 // for an emergency call or accepted an incoming call.
196− setFakeActiveForApp(appId)
202+ setLockedApp(appId)
197 }
203 }
198 launcher.hide();
204 launcher.hide();
199 }
205 }
@@ -201,9 +207,19 @@ Item {
201
207
202 Loader {
208 Loader {
203 id: applicationsDisplayLoader
209 id: applicationsDisplayLoader
210+ objectName: "applicationsDisplayLoader"
204 anchors.fill: parent
211 anchors.fill: parent
205
212
213+ // When we have a locked app, we only want to show that one app.
214+ // FIXME: do this in a less traumatic way. We currently only allow
215+ // locked apps in phone mode (see FIXME in Lockscreen component in
216+ // this same file). When that changes, we need to do something
217+ // nicer here. But this code is currently just to prevent a
218+ // theoretical attack where user enters lockedApp mode, then makes
219+ // the screen larger (maybe connects to monitor) and tries to enter
220+ // tablet mode.
221+ property bool tabletMode: shell.sideStageEnabled && !greeter.hasLockedApp
206− source: shell.sideStageEnabled ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
222+ source: tabletMode ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
207
223
208 Binding {
224 Binding {
209 target: applicationsDisplayLoader.item
225 target: applicationsDisplayLoader.item
@@ -229,7 +245,7 @@ Item {
229 Binding {
245 Binding {
230 target: applicationsDisplayLoader.item
246 target: applicationsDisplayLoader.item
231 property: "spreadEnabled"
247 property: "spreadEnabled"
232− value: edgeDemo.stagesEnabled && greeter.fakeActiveForApp === "" // to support emergency dialer hack
248+ value: edgeDemo.stagesEnabled && !greeter.hasLockedApp
233 }
249 }
234 Binding {
250 Binding {
235 target: applicationsDisplayLoader.item
251 target: applicationsDisplayLoader.item
@@ -304,14 +320,14 @@ Item {
304 // and wider screens are tablets which don't. When we do allow this
320 // and wider screens are tablets which don't. When we do allow this
305 // on devices with a side stage and a SIM, work should be done to
321 // on devices with a side stage and a SIM, work should be done to
306 // ensure that the main stage is disabled while the dialer is present
322 // ensure that the main stage is disabled while the dialer is present
307− // in the side stage.
323+ // in the side stage. See the FIXME in the stage loader in this file.
308 showEmergencyCallButton: !shell.sideStageEnabled
324 showEmergencyCallButton: !shell.sideStageEnabled
309
325
310 onEntered: LightDM.Greeter.respond(passphrase);
326 onEntered: LightDM.Greeter.respond(passphrase);
311 onCancel: greeter.show()
327 onCancel: greeter.show()
312− onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter fake-active mode
328+ onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter locked-app mode
313
329
314− onShownChanged: if (shown) greeter.fakeActiveForApp = ""
330+ onShownChanged: if (shown) greeter.lockedApp = ""
315
331
316 Timer {
332 Timer {
317 id: forcedDelayTimer
333 id: forcedDelayTimer
@@ -360,11 +376,13 @@ Item {
360 }
376 }
361
377
362 onPromptlessChanged: {
378 onPromptlessChanged: {
379+ if (greeter.narrowMode) {
363− if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
380+ if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
364− lockscreen.hide()
381+ lockscreen.hide()
365− } else {
382+ } else {
366− lockscreen.reset();
383+ lockscreen.reset();
367− lockscreen.show();
384+ lockscreen.show();
385+ }
368 }
386 }
369 }
387 }
370
388
@@ -414,7 +432,7 @@ Item {
414 Binding {
432 Binding {
415 target: LightDM.Greeter
433 target: LightDM.Greeter
416 property: "active"
434 property: "active"
417− value: greeter.shown || lockscreen.shown || greeter.fakeActiveForApp != ""
435+ value: greeter.shown || lockscreen.shown || greeter.hasLockedApp
418 }
436 }
419
437
420 Rectangle {
438 Rectangle {
@@ -426,7 +444,8 @@ Item {
426 Item {
444 Item {
427 // Just a tiny wrapper to adjust greeter's x without messing with its own dragging
445 // Just a tiny wrapper to adjust greeter's x without messing with its own dragging
428 id: greeterWrapper
446 id: greeterWrapper
447+ objectName: "greeterWrapper"
429− x: launcher.progress
448+ x: greeter.narrowMode ? launcher.progress : 0
430 y: panel.panelHeight
449 y: panel.panelHeight
431 width: parent.width
450 width: parent.width
432 height: parent.height - panel.panelHeight
451 height: parent.height - panel.panelHeight
@@ -439,7 +458,7 @@ Item {
439 property bool fullyShown: showProgress === 1.0
458 property bool fullyShown: showProgress === 1.0
440 onFullyShownChanged: {
459 onFullyShownChanged: {
441 // Wait until the greeter is completely covering lockscreen before resetting it.
460 // Wait until the greeter is completely covering lockscreen before resetting it.
442− if (fullyShown && !LightDM.Greeter.authenticated) {
461+ if (greeter.narrowMode && fullyShown && !LightDM.Greeter.authenticated) {
443 lockscreen.reset();
462 lockscreen.reset();
444 lockscreen.show();
463 lockscreen.show();
445 }
464 }
@@ -448,7 +467,7 @@ Item {
448 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
467 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
449 onShowProgressChanged: {
468 onShowProgressChanged: {
450 if (showProgress === 0) {
469 if (showProgress === 0) {
451− if (LightDM.Greeter.authenticated) {
470+ if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
452 greeter.login()
471 greeter.login()
453 } else if (greeter.narrowMode) {
472 } else if (greeter.narrowMode) {
454 lockscreen.clear(false) // to reset focus if necessary
473 lockscreen.clear(false) // to reset focus if necessary
@@ -462,13 +481,16 @@ Item {
462
481
463 signal sessionStarted() // helpful for tests
482 signal sessionStarted() // helpful for tests
464
483
465− property string fakeActiveForApp: ""
484+ property string lockedApp: ""
485+ property bool hasLockedApp: lockedApp !== ""
466
486
467 available: true
487 available: true
468 hides: [launcher, panel.indicators]
488 hides: [launcher, panel.indicators]
469 shown: true
489 shown: true
470 loadContent: required || lockscreen.required // keeps content in memory for quick show()
490 loadContent: required || lockscreen.required // keeps content in memory for quick show()
471
491
492+ locked: shell.locked
493+
472 defaultBackground: shell.background
494 defaultBackground: shell.background
473
495
474 width: parent.width
496 width: parent.width
@@ -476,6 +498,18 @@ Item {
476
498
477 dragHandleWidth: shell.edgeSize
499 dragHandleWidth: shell.edgeSize
478
500
501+ function startUnlock() {
502+ if (narrowMode) {
503+ if (!LightDM.Greeter.authenticated) {
504+ lockscreen.show()
505+ }
506+ hide()
507+ } else {
508+ show()
509+ tryToUnlock()
510+ }
511+ }
512+
479 function login() {
513 function login() {
480 enabled = false;
514 enabled = false;
481 if (LightDM.Greeter.startSessionSync()) {
515 if (LightDM.Greeter.startSessionSync()) {
@@ -491,8 +525,10 @@ Item {
491 if (shown) {
525 if (shown) {
492 if (greeter.narrowMode) {
526 if (greeter.narrowMode) {
493 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
527 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
528+ } else {
529+ reset()
494 }
530 }
495− greeter.fakeActiveForApp = "";
531+ greeter.lockedApp = "";
496 greeter.forceActiveFocus();
532 greeter.forceActiveFocus();
497 }
533 }
498 }
534 }
@@ -537,10 +573,7 @@ Item {
537 }
573 }
538
574
539 if (LightDM.Greeter.active) {
575 if (LightDM.Greeter.active) {
540− if (!LightDM.Greeter.authenticated) {
541− lockscreen.show()
542− }
543− greeter.hide()
576+ greeter.startUnlock()
544 }
577 }
545
578
546 var animate = !LightDM.Greeter.active && !stages.shown
579 var animate = !LightDM.Greeter.active && !stages.shown
@@ -549,7 +582,8 @@ Item {
549 }
582 }
550
583
551 function showDash() {
584 function showDash() {
552− if (greeter.fakeActiveForApp !== "") { // just in case user gets here
585+ if (greeter.hasLockedApp || // just in case user gets here
586+ (!greeter.narrowMode && shell.locked)) {
553 return
587 return
554 }
588 }
555
589
@@ -576,7 +610,7 @@ Item {
576 anchors.fill: parent //because this draws indicator menus
610 anchors.fill: parent //because this draws indicator menus
577 indicators {
611 indicators {
578 hides: [launcher]
612 hides: [launcher]
579− available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && greeter.fakeActiveForApp === ""
613+ available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && !greeter.hasLockedApp
580 contentEnabled: edgeDemo.panelContentEnabled
614 contentEnabled: edgeDemo.panelContentEnabled
581 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
615 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
582 panelHeight: units.gu(3)
616 panelHeight: units.gu(3)
@@ -587,7 +621,7 @@ Item {
587 ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
621 ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
588
622
589 fullscreenMode: (topmostApplicationIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
623 fullscreenMode: (topmostApplicationIsFullscreen && !LightDM.Greeter.active && launcher.progress == 0)
590− || greeter.fakeActiveForApp !== ""
624+ || greeter.hasLockedApp
591 }
625 }
592
626
593 Launcher {
627 Launcher {
@@ -600,7 +634,7 @@ Item {
600 anchors.bottom: parent.bottom
634 anchors.bottom: parent.bottom
601 width: parent.width
635 width: parent.width
602 dragAreaWidth: shell.edgeSize
636 dragAreaWidth: shell.edgeSize
603− available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && greeter.fakeActiveForApp === ""
637+ available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && !greeter.hasLockedApp
604
638
605 onShowDashHome: showHome()
639 onShowDashHome: showHome()
606 onDash: showDash()
640 onDash: showDash()
@@ -610,8 +644,8 @@ Item {
610 }
644 }
611 }
645 }
612 onLauncherApplicationSelected: {
646 onLauncherApplicationSelected: {
613− if (greeter.fakeActiveForApp !== "") {
647+ if (greeter.hasLockedApp) {
614− lockscreen.show()
648+ greeter.startUnlock()
615 }
649 }
616 if (!edgeDemo.running)
650 if (!edgeDemo.running)
617 shell.activateApplication(appId)
651 shell.activateApplication(appId)
tests/qmltests/CMakeLists.txt+1
@@ -21,6 +21,7 @@ set(qmltest_DEFAULT_PROPERTIES ENVIRONMENT "LC_ALL=C")
21
21
22add_qml_test(. Shell ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single")
22add_qml_test(. Shell ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single")
23add_qml_test(. ShellWithPin ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single-pin")
23add_qml_test(. ShellWithPin ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/single-pin")
24+add_qml_test(. TabletShell ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/LightDM/full")
24add_qml_test(Components Background)
25add_qml_test(Components Background)
25add_qml_test(Components Carousel)
26add_qml_test(Components Carousel)
26add_qml_test(Components DraggingArea)
27add_qml_test(Components DraggingArea)
tests/qmltests/Greeter/tst_MultiGreeter.qml+2−2
@@ -29,6 +29,7 @@ Item {
29 Greeter {
29 Greeter {
30 id: greeter
30 id: greeter
31 anchors.fill: parent
31 anchors.fill: parent
32+ locked: !LightDM.Greeter.authenticated
32 }
33 }
33
34
34 Component {
35 Component {
@@ -115,8 +116,7 @@ Item {
115 var waitForSignal = data.uid != 0 && userList.currentIndex != data.uid
116 var waitForSignal = data.uid != 0 && userList.currentIndex != data.uid
116 select_index(data.uid)
117 select_index(data.uid)
117 tryCompare(userList, "currentIndex", data.uid)
118 tryCompare(userList, "currentIndex", data.uid)
118− tryCompare(greeter, "locked", data.tag !== "no-password" &&
119+ tryCompare(greeter, "locked", data.tag !== "no-password")
119− data.tag !== "auth-error")
120 if (waitForSignal) {
120 if (waitForSignal) {
121 selectionSpy.wait()
121 selectionSpy.wait()
122 tryCompare(selectionSpy, "count", 1)
122 tryCompare(selectionSpy, "count", 1)
tests/qmltests/tst_Shell.qml+1
@@ -484,6 +484,7 @@ Row {
484 tryCompare(greeter, "showProgress", 0)
484 tryCompare(greeter, "showProgress", 0)
485 waitForRendering(greeter);
485 waitForRendering(greeter);
486 LightDM.Greeter.showGreeter()
486 LightDM.Greeter.showGreeter()
487+ waitForRendering(greeter)
487 tryCompare(greeter, "showProgress", 1)
488 tryCompare(greeter, "showProgress", 1)
488 LightDM.Greeter.hideGreeter()
489 LightDM.Greeter.hideGreeter()
489 tryCompare(greeter, "showProgress", 0)
490 tryCompare(greeter, "showProgress", 0)
tests/qmltests/tst_ShellWithPin.qml+106−25
@@ -27,10 +27,9 @@ import Powerd 0.1
27
27
28import "../../qml"
28import "../../qml"
29
29
30−Item {
30+Row {
31 id: root
31 id: root
32− width: shell.width + units.gu(20)
32+ spacing: 0
33− height: shell.height
34
33
35 QtObject {
34 QtObject {
36 id: applicationArguments
35 id: applicationArguments
@@ -48,21 +47,55 @@ Item {
48 }
47 }
49 }
48 }
50
49
51− Shell {
50+ Loader {
52− id: shell
51+ id: shellLoader
53− maxFailedLogins: maxRetriesTextField.text
52+
54− }
55− Column {
56− anchors { top: parent.top; right: parent.right; bottom: parent.bottom; margins:units.gu(1) }
57− width: units.gu(18)
53+ width: units.gu(40)
54+ height: units.gu(71)
58
55
59− Label {
56+ property bool itemDestroyed: false
60− text: "Max retries:"
57+ sourceComponent: Component {
61− color: "black"
58+ Shell {
59+ Component.onDestruction: {
60+ shellLoader.itemDestroyed = true
61+ }
62+ maxFailedLogins: maxRetriesTextField.text
63+ }
62 }
64 }
65+ }
66+
67+ Rectangle {
68+ color: "white"
69+ width: units.gu(30)
70+ height: shellLoader.height
71+
72+ Column {
73+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
74+ spacing: units.gu(1)
75+ Row {
76+ anchors { left: parent.left; right: parent.right }
77+ Button {
78+ text: "Show Greeter"
79+ onClicked: {
80+ if (shellLoader.status !== Loader.Ready)
81+ return
82+
83+ var greeter = testCase.findChild(shellLoader.item, "greeter")
84+ if (!greeter.shown) {
85+ greeter.show()
86+ }
87+ }
88+ }
89+ }
90+
91+ Label {
92+ text: "Max retries:"
93+ color: "black"
94+ }
63− TextField {
95+ TextField {
64− id: maxRetriesTextField
96+ id: maxRetriesTextField
65− text: "-1"
97+ text: "-1"
98+ }
66 }
99 }
67 }
100 }
68
101
@@ -81,12 +114,10 @@ Item {
81 name: "ShellWithPin"
114 name: "ShellWithPin"
82 when: windowShown
115 when: windowShown
83
116
84− function initTestCase() {
117+ property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
85−
86− sessionSpy.target = findChild(shell, "greeter")
87− }
88
118
89 function init() {
119 function init() {
120+ sessionSpy.target = findChild(shell, "greeter")
90 swipeAwayGreeter()
121 swipeAwayGreeter()
91 shell.failedLoginsDelayAttempts = -1
122 shell.failedLoginsDelayAttempts = -1
92 maxRetriesTextField.text = "-1"
123 maxRetriesTextField.text = "-1"
@@ -95,12 +126,28 @@ Item {
95 }
126 }
96
127
97 function cleanup() {
128 function cleanup() {
98− LightDM.Greeter.showGreeter()
129+ shellLoader.itemDestroyed = false
99− var greeter = findChild(shell, "greeter")
130+
100− tryCompare(greeter, "showProgress", 1)
131+ shellLoader.active = false
132+
133+ tryCompare(shellLoader, "status", Loader.Null)
134+ tryCompare(shellLoader, "item", null)
135+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
136+ // item might still be alive. So if we set Loader.active back to true
137+ // again right now we will get the very same Shell instance back. So no reload
138+ // actually took place. Likely because Loader waits until the next event loop
139+ // iteration to do its work. So to ensure the reload, we will wait until the
140+ // Shell instance gets destroyed.
141+ tryCompare(shellLoader, "itemDestroyed", true)
101
142
102 // kill all (fake) running apps
143 // kill all (fake) running apps
103 killApps()
144 killApps()
145+
146+ // reload our test subject to get it in a fresh state once again
147+ shellLoader.active = true
148+
149+ tryCompare(shellLoader, "status", Loader.Ready)
150+ removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item)
104 }
151 }
105
152
106 function killApps() {
153 function killApps() {
@@ -113,6 +160,7 @@ Item {
113
160
114 function swipeAwayGreeter() {
161 function swipeAwayGreeter() {
115 var greeter = findChild(shell, "greeter");
162 var greeter = findChild(shell, "greeter");
163+ waitForRendering(greeter)
116 tryCompare(greeter, "showProgress", 1);
164 tryCompare(greeter, "showProgress", 1);
117
165
118 var touchX = shell.width - (shell.edgeSize / 2);
166 var touchX = shell.width - (shell.edgeSize / 2);
@@ -168,7 +216,8 @@ Item {
168
216
169 mouseClick(emergencyButton, units.gu(1), units.gu(1))
217 mouseClick(emergencyButton, units.gu(1), units.gu(1))
170
218
171− tryCompare(greeter, "fakeActiveForApp", "dialer-app")
219+ tryCompare(greeter, "lockedApp", "dialer-app")
220+ tryCompare(greeter, "hasLockedApp", true)
172 tryCompare(lockscreen, "shown", false)
221 tryCompare(lockscreen, "shown", false)
173 tryCompare(panel, "fullscreenMode", true)
222 tryCompare(panel, "fullscreenMode", true)
174 tryCompare(indicators, "available", false)
223 tryCompare(indicators, "available", false)
@@ -180,7 +229,8 @@ Item {
180 LightDM.Greeter.showGreeter()
229 LightDM.Greeter.showGreeter()
181
230
182 tryCompare(greeter, "shown", true)
231 tryCompare(greeter, "shown", true)
183− tryCompare(greeter, "fakeActiveForApp", "")
232+ tryCompare(greeter, "lockedApp", "")
233+ tryCompare(greeter, "hasLockedApp", false)
184 tryCompare(lockscreen, "shown", true)
234 tryCompare(lockscreen, "shown", true)
185 tryCompare(panel, "fullscreenMode", false)
235 tryCompare(panel, "fullscreenMode", false)
186 tryCompare(indicators, "available", true)
236 tryCompare(indicators, "available", true)
@@ -253,5 +303,36 @@ Item {
253 enterPin("1111")
303 enterPin("1111")
254 tryCompare(resetSpy, "count", 1)
304 tryCompare(resetSpy, "count", 1)
255 }
305 }
306+
307+ function test_emergencyDialerLockOut() {
308+ // This is a theoretical attack on the lockscreen: Enter emergency
309+ // dialer mode on a phone, then plug into a larger screen,
310+ // switching to a tablet interface. This would in theory move the
311+ // dialer to a side stage and give access to other apps. So just
312+ // confirm that such an attack doesn't work.
313+
314+ var applicationsDisplayLoader = findChild(shell, "applicationsDisplayLoader")
315+
316+ // We start in phone mode
317+ tryCompare(shell, "sideStageEnabled", false)
318+ tryCompare(applicationsDisplayLoader, "tabletMode", false)
319+
320+ var app = ApplicationManager.startApplication("dialer-app")
321+
322+ var greeter = findChild(shell, "greeter")
323+ tryCompare(greeter, "showProgress", 0)
324+ tryCompare(greeter, "hasLockedApp", true)
325+
326+ // OK, we're in. Now try (but fail) to switch to tablet mode
327+ shell.tablet = true
328+ tryCompare(shell, "sideStageEnabled", true)
329+ tryCompare(applicationsDisplayLoader, "tabletMode", false)
330+
331+ // And when we kill the app, we go back to locked tablet mode
332+ killApps()
333+ tryCompare(greeter, "showProgress", 1)
334+ tryCompare(shell, "sideStageEnabled", true)
335+ tryCompare(applicationsDisplayLoader, "tabletMode", true)
336+ }
256 }
337 }
257}
338}
tests/qmltests/tst_TabletShell.qmlnew file+289
@@ -0,0 +1,289 @@
1+/*
2+ * Copyright (C) 2013,2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtTest 1.0
19+import GSettings 1.0
20+import LightDM 0.1 as LightDM
21+import Ubuntu.Components 1.1
22+import Ubuntu.Telephony 0.1 as Telephony
23+import Unity.Application 0.1
24+import Unity.Connectivity 0.1
25+import Unity.Test 0.1 as UT
26+import Powerd 0.1
27+
28+import "../../qml"
29+
30+Row {
31+ id: root
32+ spacing: 0
33+
34+ QtObject {
35+ id: applicationArguments
36+
37+ function hasGeometry() {
38+ return false
39+ }
40+
41+ function width() {
42+ return 0
43+ }
44+
45+ function height() {
46+ return 0
47+ }
48+ }
49+
50+ Loader {
51+ id: shellLoader
52+
53+ width: units.gu(100)
54+ height: units.gu(80)
55+
56+ property bool itemDestroyed: false
57+ sourceComponent: Component {
58+ Shell {
59+ Component.onDestruction: {
60+ shellLoader.itemDestroyed = true
61+ }
62+ }
63+ }
64+ }
65+
66+ Rectangle {
67+ color: "white"
68+ width: units.gu(20)
69+ height: shellLoader.height
70+
71+ Column {
72+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
73+ spacing: units.gu(1)
74+ Row {
75+ anchors { left: parent.left; right: parent.right }
76+ Button {
77+ text: "Show Greeter"
78+ onClicked: {
79+ if (shellLoader.status !== Loader.Ready)
80+ return
81+
82+ var greeter = testCase.findChild(shellLoader.item, "greeter")
83+ if (!greeter.shown) {
84+ greeter.show()
85+ }
86+ }
87+ }
88+ }
89+ }
90+ }
91+
92+ SignalSpy {
93+ id: sessionSpy
94+ signalName: "sessionStarted"
95+ }
96+
97+ SignalSpy {
98+ id: dashCommunicatorSpy
99+ signalName: "setCurrentScopeCalled"
100+ }
101+
102+ SignalSpy {
103+ id: unlockAllModemsSpy
104+ target: Connectivity
105+ signalName: "unlockingAllModems"
106+ }
107+
108+ Telephony.CallEntry {
109+ id: phoneCall
110+ phoneNumber: "+447812221111"
111+ }
112+
113+ UT.UnityTestCase {
114+ id: testCase
115+ name: "TabletShell"
116+ when: windowShown
117+
118+ property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
119+
120+ function init() {
121+ sessionSpy.clear()
122+ sessionSpy.target = findChild(shell, "greeter")
123+ dashCommunicatorSpy.target = findInvisibleChild(shell, "dashCommunicator")
124+ }
125+
126+ function cleanup() {
127+ shellLoader.itemDestroyed = false
128+
129+ shellLoader.active = false
130+
131+ tryCompare(shellLoader, "status", Loader.Null)
132+ tryCompare(shellLoader, "item", null)
133+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
134+ // item might still be alive. So if we set Loader.active back to true
135+ // again right now we will get the very same Shell instance back. So no reload
136+ // actually took place. Likely because Loader waits until the next event loop
137+ // iteration to do its work. So to ensure the reload, we will wait until the
138+ // Shell instance gets destroyed.
139+ tryCompare(shellLoader, "itemDestroyed", true)
140+
141+ // kill all (fake) running apps
142+ killApps()
143+
144+ unlockAllModemsSpy.clear()
145+
146+ // reload our test subject to get it in a fresh state once again
147+ shellLoader.active = true
148+
149+ tryCompare(shellLoader, "status", Loader.Ready)
150+ removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item)
151+ }
152+
153+ function killApps() {
154+ while (ApplicationManager.count > 1) {
155+ var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
156+ ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId)
157+ }
158+ compare(ApplicationManager.count, 1)
159+ }
160+
161+ function selectIndex(i) {
162+ // We could be anywhere in list; find target index to know which direction
163+ var greeter = findChild(shell, "greeter")
164+ var userlist = findChild(greeter, "userList")
165+ if (userlist.currentIndex == i)
166+ keyClick(Qt.Key_Escape) // Reset state if we're not moving
167+ while (userlist.currentIndex != i) {
168+ var next = userlist.currentIndex + 1
169+ if (userlist.currentIndex > i) {
170+ next = userlist.currentIndex - 1
171+ }
172+ var account = findChild(greeter, "username"+next)
173+ mouseClick(account, 1, 1)
174+ tryCompare(userlist, "currentIndex", next)
175+ tryCompare(userlist, "movingInternally", false)
176+ }
177+ }
178+
179+ function selectUser(name) {
180+ // Find index of user with the right name
181+ var greeter = findChild(shell, "greeter")
182+ for (var i = 0; i < greeter.model.count; i++) {
183+ if (greeter.model.data(i, LightDM.UserRoles.NameRole) == name) {
184+ break
185+ }
186+ }
187+ if (i == greeter.model.count) {
188+ fail("Didn't find name")
189+ return -1
190+ }
191+ selectIndex(i)
192+ return i
193+ }
194+
195+ function clickPasswordInput(isButton) {
196+ var greeter = findChild(shell, "greeter")
197+ tryCompare(greeter, "showProgress", 1)
198+
199+ var passwordMouseArea = findChild(shell, "passwordMouseArea")
200+ tryCompare(passwordMouseArea, "enabled", isButton)
201+
202+ var passwordInput = findChild(shell, "passwordInput")
203+ mouseClick(passwordInput, passwordInput.width / 2, passwordInput.height / 2)
204+ }
205+
206+ function confirmLoggedIn(loggedIn) {
207+ var greeterWrapper = findChild(shell, "greeterWrapper")
208+ tryCompare(greeterWrapper, "showProgress", loggedIn ? 0 : 1)
209+ tryCompare(sessionSpy, "count", loggedIn ? 1 : 0)
210+ }
211+
212+ function swipeFromLeftEdge(swipeLength) {
213+ var touchStartX = 2
214+ var touchStartY = shell.height / 2
215+ touchFlick(shell, touchStartX, touchStartY, swipeLength, touchStartY)
216+ }
217+
218+ function test_noLockscreen() {
219+ selectUser("has-password")
220+ var lockscreen = findChild(shell, "lockscreen")
221+ tryCompare(lockscreen, "shown", false)
222+ }
223+
224+ function test_showAndHideGreeterDBusCalls() {
225+ var greeter = findChild(shell, "greeter")
226+ LightDM.Greeter.hideGreeter()
227+ tryCompare(greeter, "showProgress", 0)
228+ LightDM.Greeter.showGreeter()
229+ tryCompare(greeter, "showProgress", 1)
230+ }
231+
232+ function test_login_data() {
233+ return [
234+ {tag: "auth error", user: "auth-error", loggedIn: false, password: ""},
235+ {tag: "with password", user: "has-password", loggedIn: true, password: "password"},
236+ {tag: "without password", user: "no-password", loggedIn: true, password: ""},
237+ ]
238+ }
239+
240+ function test_login(data) {
241+ selectUser(data.user)
242+
243+ clickPasswordInput(data.password === "")
244+
245+ if (data.password !== "") {
246+ typeString(data.password)
247+ keyClick(Qt.Key_Enter)
248+ }
249+
250+ confirmLoggedIn(data.loggedIn)
251+ }
252+
253+ function test_appLaunchDuringGreeter_data() {
254+ return [
255+ {tag: "auth error", user: "auth-error", loggedIn: false, passwordFocus: false},
256+ {tag: "without password", user: "no-password", loggedIn: true, passwordFocus: false},
257+ {tag: "with password", user: "has-password", loggedIn: false, passwordFocus: true},
258+ ]
259+ }
260+
261+ function test_appLaunchDuringGreeter(data) {
262+ selectUser(data.user)
263+
264+ var greeter = findChild(shell, "greeter")
265+ var app = ApplicationManager.startApplication("dialer-app")
266+
267+ confirmLoggedIn(data.loggedIn)
268+
269+ if (data.passwordFocus) {
270+ var passwordInput = findChild(greeter, "passwordInput")
271+ tryCompare(passwordInput, "focus", true)
272+ }
273+ }
274+
275+ function test_leftEdgeDrag_data() {
276+ return [
277+ {tag: "without password", user: "no-password", loggedIn: true},
278+ {tag: "with password", user: "has-password", loggedIn: false},
279+ ]
280+ }
281+
282+ function test_leftEdgeDrag(data) {
283+ selectUser(data.user)
284+ swipeFromLeftEdge(shell.width * 0.75)
285+ wait(500) // to give time to handle dash() signal from Launcher
286+ confirmLoggedIn(data.loggedIn)
287+ }
288+ }
289+}