Merge lp:~mterry/unity8/tablet-security into lp:unity8
- tablet-security
- Merge into trunk
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 |
Related bugs: |
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-
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
PS Jenkins bot (ps-jenkins) wrote : | # |
MichaĆ Sawicz (saviq) wrote : | # |
Think we could get some tests confirming these behaviours?
Michael Terry (mterry) wrote : | # |
Yeah, fair. Will work on that.
Michael Terry (mterry) wrote : | # |
OK, tests added, review away.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1266
http://
Executed test runs:
FAILURE: http://
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1267. By Michael Terry
-
Make ShellWithPin tests more reliable by waiting for rendering
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1267
http://
Executed test runs:
FAILURE: http://
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1268. By Michael Terry
-
Make ShellWithPin tests more reliable
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1268
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Albert Astals Cid (aacid) wrote : | # |
Text conflict in qml/Shell.qml
1 conflicts encountered.
- 1269. By Michael Terry
-
Merge from trunk
Michael Terry (mterry) wrote : | # |
Merged from trunk.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1269
http://
Executed test runs:
UNSTABLE: http://
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1270. By Michael Terry
-
Merge from trunk
- 1271. By Michael Terry
-
Fix tests
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1270
http://
Executed test runs:
UNSTABLE: http://
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1271
http://
Executed test runs:
UNSTABLE: http://
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1272. By Michael Terry
-
Fix MultiGreeter qmluitest
Albert Astals Cid (aacid) wrote : | # |
Text conflict in qml/Shell.qml
1 conflicts encountered.
- 1273. By Michael Terry
-
Merge from trunk
Michael Terry (mterry) wrote : | # |
Merged from trunk
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1272
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1273
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
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
Michael Terry (mterry) wrote : | # |
Replied inline to your comment, will look at tryTabletShell.
- 1274. By Michael Terry
-
Merge from trunk
- 1275. By Michael Terry
-
Prevent dash logins in tablet mode
Michael Terry (mterry) wrote : | # |
OK, fixed the left-drag-unlock for locked users! Whoops, that was the result of a bad merge.
- 1276. By Michael Terry
-
Make password login test a little more reliable
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1276
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 1277. By Michael Terry
-
expand comments
- 1278. By Michael Terry
-
Add wait(500) to properly test leftEdgeDrag
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
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1277
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1277
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
None: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1278
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Preview Diff
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 | +} |
FAILED: Continuous integration, rev:1263 jenkins. qa.ubuntu. com/job/ unity8- ci/4272/ jenkins. qa.ubuntu. com/job/ generic- deb-autopilot- utopic- touch/4709 jenkins. qa.ubuntu. com/job/ unity-phablet- qmluitests- utopic/ 1273 jenkins. qa.ubuntu. com/job/ unity8- utopic- amd64-ci/ 1366 jenkins. qa.ubuntu. com/job/ unity8- utopic- i386-ci/ 1366 jenkins. qa.ubuntu. com/job/ generic- deb-autopilot- runner- mako/4482 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- utopic- armhf/5961 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- utopic- armhf/5961/ artifact/ work/output/ *zip*/output. zip s-jenkins. ubuntu- ci:8080/ job/touch- flash-device/ 13044
http://
Executed test runs:
UNSTABLE: http://
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/unity8- ci/4272/ rebuild
http://