1 /*
<lambda>null2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.pm.test.verify.domain
18 
19 import android.content.UriRelativeFilter
20 import android.content.UriRelativeFilter.PATH
21 import android.content.UriRelativeFilterGroup
22 import android.content.UriRelativeFilterGroup.ACTION_ALLOW
23 import android.content.pm.verify.domain.DomainVerificationState
24 import android.os.PatternMatcher.PATTERN_LITERAL
25 import android.os.UserHandle
26 import android.util.ArrayMap
27 import android.util.SparseArray
28 import android.util.Xml
29 import com.android.modules.utils.TypedXmlPullParser
30 import com.android.modules.utils.TypedXmlSerializer
31 import com.android.server.pm.verify.domain.DomainVerificationPersistence
32 import com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState
33 import com.android.server.pm.verify.domain.models.DomainVerificationPkgState
34 import com.android.server.pm.verify.domain.models.DomainVerificationStateMap
35 import com.google.common.truth.Truth.assertThat
36 import com.google.common.truth.Truth.assertWithMessage
37 import org.junit.Rule
38 import org.junit.Test
39 import org.junit.rules.TemporaryFolder
40 import org.xmlpull.v1.XmlPullParser
41 import java.io.File
42 import java.nio.charset.StandardCharsets
43 import java.util.UUID
44 
45 class DomainVerificationPersistenceTest {
46 
47     companion object {
48         private val PKG_PREFIX = DomainVerificationPersistenceTest::class.java.`package`!!.name
49 
50         internal fun File.writeXml(block: (serializer: TypedXmlSerializer) -> Unit) = apply {
51             outputStream().use {
52                 // This must use the binary serializer the mirror the production behavior, as
53                 // there are slight differences with the string based one.
54                 Xml.newBinarySerializer()
55                     .apply {
56                         setOutput(it, StandardCharsets.UTF_8.name())
57                         startDocument(null, true)
58                         setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
59                         // Write a wrapping tag to ensure the domain verification settings didn't
60                         // close out the document, allowing other settings to be written
61                         startTag(null, "wrapper-tag")
62                     }
63                     .apply(block)
64                     .apply {
65                         startTag(null, "trailing-tag")
66                         endTag(null, "trailing-tag")
67                         endTag(null, "wrapper-tag")
68                     }
69                     .endDocument()
70             }
71         }
72 
73         internal fun <T> File.readXml(block: (parser: TypedXmlPullParser) -> T) =
74             inputStream().use {
75                 val parser = Xml.resolvePullParser(it)
76                 assertThat(parser.nextTag()).isEqualTo(XmlPullParser.START_TAG)
77                 assertThat(parser.name).isEqualTo("wrapper-tag")
78                 assertThat(parser.nextTag()).isEqualTo(XmlPullParser.START_TAG)
79                 block(parser).also {
80                     assertThat(parser.nextTag()).isEqualTo(XmlPullParser.START_TAG)
81                     assertThat(parser.name).isEqualTo("trailing-tag")
82                     assertThat(parser.nextTag()).isEqualTo(XmlPullParser.END_TAG)
83                     assertThat(parser.name).isEqualTo("trailing-tag")
84                     assertThat(parser.nextTag()).isEqualTo(XmlPullParser.END_TAG)
85                     assertThat(parser.name).isEqualTo("wrapper-tag")
86                 }
87             }
88     }
89 
90     @Rule
91     @JvmField
92     val tempFolder = TemporaryFolder()
93 
94     private fun mockWriteValues(
95         pkgNameToSignature: (String) -> String? = { null }
96     ): Triple<DomainVerificationStateMap<DomainVerificationPkgState>,
97             ArrayMap<String, DomainVerificationPkgState>,
98             ArrayMap<String, DomainVerificationPkgState>> {
99         val attached = DomainVerificationStateMap<DomainVerificationPkgState>().apply {
100             mockPkgState(0, pkgNameToSignature).let { put(it.packageName, it.id, it) }
101             mockPkgState(1, pkgNameToSignature).let { put(it.packageName, it.id, it) }
102         }
103         val pending = ArrayMap<String, DomainVerificationPkgState>().apply {
104             mockPkgState(2, pkgNameToSignature).let { put(it.packageName, it) }
105             mockPkgState(3, pkgNameToSignature).let { put(it.packageName, it) }
106         }
107         val restored = ArrayMap<String, DomainVerificationPkgState>().apply {
108             mockPkgState(4, pkgNameToSignature).let { put(it.packageName, it) }
109             mockPkgState(5, pkgNameToSignature).let { put(it.packageName, it) }
110         }
111 
112         return Triple(attached, pending, restored)
113     }
114 
115     @Test
116     fun writeAndReadBackNormal() {
117         val (attached, pending, restored) = mockWriteValues()
118         val file = tempFolder.newFile().writeXml {
119             DomainVerificationPersistence.writeToXml(it, attached, pending, restored,
120                     UserHandle.USER_ALL, null)
121         }
122 
123         val xml = file.readText()
124 
125         val (readActive, readRestored) = file.readXml {
126             DomainVerificationPersistence.readFromXml(it)
127         }
128 
129         assertWithMessage(xml).that(readActive.values)
130             .containsExactlyElementsIn(attached.values() + pending.values)
131         assertWithMessage(xml).that(readRestored.values).containsExactlyElementsIn(restored.values)
132     }
133 
134     @Test
135     fun writeAndReadBackWithSignature() {
136         val (attached, pending, restored) = mockWriteValues()
137         val file = tempFolder.newFile().writeXml {
138             DomainVerificationPersistence.writeToXml(it, attached, pending, restored,
139                     UserHandle.USER_ALL) {
140                 "SIGNATURE_$it"
141             }
142         }
143 
144         val (readActive, readRestored) = file.readXml {
145             DomainVerificationPersistence.readFromXml(it)
146         }
147 
148         // Assign the signatures to a fresh set of data structures, to ensure the previous write
149         // call did not use the signatures from the data structure. This is because the method is
150         // intended to optionally append signatures, regardless of if the existing data structures
151         // contain them or not.
152         val (attached2, pending2, restored2) = mockWriteValues { "SIGNATURE_$it" }
153 
154         assertThat(readActive.values)
155             .containsExactlyElementsIn(attached2.values() + pending2.values)
156         assertThat(readRestored.values).containsExactlyElementsIn(restored2.values)
157 
158         (readActive + readRestored).forEach { (_, value) ->
159             assertThat(value.backupSignatureHash).isEqualTo("SIGNATURE_${value.packageName}")
160         }
161     }
162 
163     @Test
164     fun writeStateSignatureIfFunctionReturnsNull() {
165         val (attached, pending, restored) = mockWriteValues { "SIGNATURE_$it" }
166         val file = tempFolder.newFile().writeXml {
167             DomainVerificationPersistence.writeToXml(it, attached, pending, restored,
168                     UserHandle.USER_ALL) { null }
169         }
170 
171         val (readActive, readRestored) = file.readXml {
172             DomainVerificationPersistence.readFromXml(it)
173         }
174 
175         assertThat(readActive.values)
176             .containsExactlyElementsIn(attached.values() + pending.values)
177         assertThat(readRestored.values).containsExactlyElementsIn(restored.values)
178 
179         (readActive + readRestored).forEach { (_, value) ->
180             assertThat(value.backupSignatureHash).isEqualTo("SIGNATURE_${value.packageName}")
181         }
182     }
183 
184     @Test
185     fun readMalformed() {
186         val stateZero = mockEmptyPkgState(0, pkgNameToSignature = { "ACTIVE" }).apply {
187             stateMap["example.com"] = DomainVerificationState.STATE_SUCCESS
188             stateMap["example.org"] = DomainVerificationState.STATE_FIRST_VERIFIER_DEFINED
189 
190             // A domain without a written state falls back to default
191             stateMap["missing-state.com"] = DomainVerificationState.STATE_NO_RESPONSE
192 
193             userStates[1] = DomainVerificationInternalUserState(1).apply {
194                 addHosts(setOf("example-user1.com", "example-user1.org"))
195                 isLinkHandlingAllowed = true
196             }
197         }
198         val stateOne = mockEmptyPkgState(1, pkgNameToSignature = { "RESTORED" }).apply {
199             // It's valid to have a user selection without any autoVerify domains
200             userStates[1] = DomainVerificationInternalUserState(1).apply {
201                 addHosts(setOf("example-user1.com", "example-user1.org"))
202                 isLinkHandlingAllowed = false
203             }
204         }
205 
206         // Also valid to have neither autoVerify domains nor any active user states
207         val stateTwo = mockEmptyPkgState(2, hasAutoVerifyDomains = false)
208 
209         // language=XML
210         val xml = """
211             <?xml?>
212             <domain-verifications>
213                 <active>
214                     <package-state
215                         packageName="${stateZero.packageName}"
216                         id="${stateZero.id}"
217                         >
218                         <state>
219                             <domain name="duplicate-takes-last.com" state="1"/>
220                         </state>
221                     </package-state>
222                     <package-state
223                         packageName="${stateZero.packageName}"
224                         id="${stateZero.id}"
225                         hasAutoVerifyDomains="true"
226                         signature="ACTIVE"
227                         >
228                         <state>
229                             <domain name="example.com" state="${
230                                 DomainVerificationState.STATE_SUCCESS}"/>
231                             <domain name="example.org" state="${
232                                 DomainVerificationState.STATE_FIRST_VERIFIER_DEFINED}"/>
233                             <not-domain name="not-domain.com" state="1"/>
234                             <domain name="missing-state.com"/>
235                         </state>
236                         <user-states>
237                             <user-state userId="1" allowLinkHandling="true">
238                                 <enabled-hosts>
239                                     <host name="example-user1.com"/>
240                                     <not-host name="not-host.com"/>
241                                     <host/>
242                                 </enabled-hosts>
243                                 <enabled-hosts>
244                                     <host name="example-user1.org"/>
245                                 </enabled-hosts>
246                                 <enabled-hosts/>
247                             </user-state>
248                             <user-state>
249                                 <enabled-hosts>
250                                     <host name="no-user-id.com"/>
251                                 </enabled-hosts>
252                             </user-state>
253                         </user-states>
254                     </package-state>
255                 </active>
256                 <not-active/>
257                 <restored>
258                     <package-state
259                         packageName="${stateOne.packageName}"
260                         id="${stateOne.id}"
261                         hasAutoVerifyDomains="true"
262                         signature="RESTORED"
263                         >
264                         <state/>
265                         <user-states>
266                             <user-state userId="1">
267                                 <enabled-hosts>
268                                     <host name="example-user1.com"/>
269                                     <host name="example-user1.org"/>
270                                 </enabled-hosts>
271                             </user-state>
272                         </user-states>
273                     </package-state>
274                     <package-state packageName="${stateTwo.packageName}"/>
275                     <package-state id="${stateTwo.id}"/>
276                     <package-state
277                         packageName="${stateTwo.packageName}"
278                         id="${stateTwo.id}"
279                         hasAutoVerifyDomains="false"
280                         >
281                         <state/>
282                         <user-states/>
283                     </package-state>
284                 </restore>
285                 <not-restored/>
286             </domain-verifications>
287         """.trimIndent()
288 
289         val (active, restored) = DomainVerificationPersistence
290             .readFromXml(Xml.resolvePullParser(xml.byteInputStream()))
291 
292         assertThat(active.values).containsExactly(stateZero)
293         assertThat(restored.values).containsExactly(stateOne, stateTwo)
294     }
295 
296     private fun mockEmptyPkgState(
297         id: Int,
298         hasAutoVerifyDomains: Boolean = true,
299         pkgNameToSignature: (String) -> String? = { null }
300     ): DomainVerificationPkgState {
301         val pkgName = pkgName(id)
302         val domainSetId = UUID(0L, id.toLong())
303         return DomainVerificationPkgState(
304             pkgName,
305             domainSetId,
306             hasAutoVerifyDomains,
307             ArrayMap(),
308             SparseArray(),
309             pkgNameToSignature(pkgName)
310         )
311     }
312 
313     private fun mockPkgState(id: Int, pkgNameToSignature: (String) -> String? = { null }) =
314         mockEmptyPkgState(id, pkgNameToSignature = pkgNameToSignature)
315             .apply {
316                 stateMap["$packageName.com"] = id
317                 userStates[id] = DomainVerificationInternalUserState(id).apply {
318                     addHosts(setOf("$packageName-user.com"))
319                     isLinkHandlingAllowed = true
320                 }
321                 val group = UriRelativeFilterGroup(ACTION_ALLOW)
322                 group.addUriRelativeFilter(UriRelativeFilter(PATH, PATTERN_LITERAL, "test"))
323                 uriRelativeFilterGroupMap.put("example.com", listOf(group))
324             }
325 
326     private fun pkgName(id: Int) = "$PKG_PREFIX.pkg$id"
327 }
328