/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.lint.checks; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_NAME; import static com.android.SdkConstants.TAG_PERMISSION; import static com.android.tools.lint.checks.PermissionRequirement.REVOCABLE_PERMISSION_NAMES; import static com.android.tools.lint.checks.PermissionRequirement.isRevocableSystemPermission; import static com.android.tools.lint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.sdklib.AndroidVersion; import com.android.testutils.SdkTestCase; import com.android.tools.lint.checks.PermissionHolder.SetPermissionLookup; import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation; import com.android.utils.XmlUtils; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.io.Files; import junit.framework.TestCase; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import lombok.ast.BinaryOperator; public class PermissionRequirementTest extends TestCase { private static ResolvedAnnotation createAnnotation( @NonNull String name, @NonNull ResolvedAnnotation.Value... values) { ResolvedAnnotation annotation = mock(ResolvedAnnotation.class); when(annotation.getName()).thenReturn(name); when(annotation.getValues()).thenReturn(Arrays.asList(values)); for (ResolvedAnnotation.Value value : values) { when(annotation.getValue(value.name)).thenReturn(value.value); } return annotation; } public void testSingle() { ResolvedAnnotation.Value values = new ResolvedAnnotation.Value("value", "android.permission.ACCESS_FINE_LOCATION"); Set<String> emptySet = Collections.emptySet(); Set<String> fineSet = Collections.singleton("android.permission.ACCESS_FINE_LOCATION"); ResolvedAnnotation annotation = createAnnotation(PERMISSION_ANNOTATION, values); PermissionRequirement req = PermissionRequirement.create(null, annotation); assertTrue(req.isRevocable(new SetPermissionLookup(emptySet))); assertFalse(req.isSatisfied(new SetPermissionLookup(emptySet))); assertFalse(req.isSatisfied(new SetPermissionLookup(Collections.singleton("")))); assertTrue(req.isSatisfied(new SetPermissionLookup(fineSet))); assertEquals("android.permission.ACCESS_FINE_LOCATION", req.describeMissingPermissions(new SetPermissionLookup(emptySet))); assertEquals(fineSet, req.getMissingPermissions(new SetPermissionLookup(emptySet))); assertEquals(emptySet, req.getMissingPermissions(new SetPermissionLookup(fineSet))); assertEquals(fineSet, req.getRevocablePermissions(new SetPermissionLookup(emptySet))); assertNull(req.getOperator()); assertFalse(req.getChildren().iterator().hasNext()); } public void testAny() { ResolvedAnnotation.Value values = new ResolvedAnnotation.Value("anyOf", new String[]{"android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"}); Set<String> emptySet = Collections.emptySet(); Set<String> fineSet = Collections.singleton("android.permission.ACCESS_FINE_LOCATION"); Set<String> coarseSet = Collections.singleton("android.permission.ACCESS_COARSE_LOCATION"); Set<String> bothSet = Sets.newHashSet( "android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"); ResolvedAnnotation annotation = createAnnotation(PERMISSION_ANNOTATION, values); PermissionRequirement req = PermissionRequirement.create(null, annotation); assertTrue(req.isRevocable(new SetPermissionLookup(emptySet))); assertFalse(req.isSatisfied(new SetPermissionLookup(emptySet))); assertFalse(req.isSatisfied(new SetPermissionLookup(Collections.singleton("")))); assertTrue(req.isSatisfied(new SetPermissionLookup(fineSet))); assertTrue(req.isSatisfied(new SetPermissionLookup(coarseSet))); assertEquals( "android.permission.ACCESS_FINE_LOCATION or android.permission.ACCESS_COARSE_LOCATION", req.describeMissingPermissions(new SetPermissionLookup(emptySet))); assertEquals(bothSet, req.getMissingPermissions(new SetPermissionLookup(emptySet))); assertEquals(bothSet, req.getRevocablePermissions(new SetPermissionLookup(emptySet))); assertSame(BinaryOperator.LOGICAL_OR, req.getOperator()); } public void testAll() { ResolvedAnnotation.Value values = new ResolvedAnnotation.Value("allOf", new String[]{"android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"}); Set<String> emptySet = Collections.emptySet(); Set<String> fineSet = Collections.singleton("android.permission.ACCESS_FINE_LOCATION"); Set<String> coarseSet = Collections.singleton("android.permission.ACCESS_COARSE_LOCATION"); Set<String> bothSet = Sets.newHashSet( "android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"); ResolvedAnnotation annotation = createAnnotation(PERMISSION_ANNOTATION, values); PermissionRequirement req = PermissionRequirement.create(null, annotation); assertTrue(req.isRevocable(new SetPermissionLookup(emptySet))); assertFalse(req.isSatisfied(new SetPermissionLookup(emptySet))); assertFalse(req.isSatisfied(new SetPermissionLookup(Collections.singleton("")))); assertFalse(req.isSatisfied(new SetPermissionLookup(fineSet))); assertFalse(req.isSatisfied(new SetPermissionLookup(coarseSet))); assertTrue(req.isSatisfied(new SetPermissionLookup(bothSet))); assertEquals( "android.permission.ACCESS_FINE_LOCATION and android.permission.ACCESS_COARSE_LOCATION", req.describeMissingPermissions(new SetPermissionLookup(emptySet))); assertEquals(bothSet, req.getMissingPermissions(new SetPermissionLookup(emptySet))); assertEquals( "android.permission.ACCESS_COARSE_LOCATION", req.describeMissingPermissions(new SetPermissionLookup(fineSet))); assertEquals(coarseSet, req.getMissingPermissions(new SetPermissionLookup(fineSet))); assertEquals( "android.permission.ACCESS_FINE_LOCATION", req.describeMissingPermissions(new SetPermissionLookup(coarseSet))); assertEquals(fineSet, req.getMissingPermissions(new SetPermissionLookup(coarseSet))); assertEquals(bothSet, req.getRevocablePermissions(new SetPermissionLookup(emptySet))); assertSame(BinaryOperator.LOGICAL_AND, req.getOperator()); } public void testRevocable() { assertTrue(isRevocableSystemPermission("android.permission.ACCESS_FINE_LOCATION")); assertTrue(isRevocableSystemPermission("android.permission.ACCESS_COARSE_LOCATION")); assertFalse(isRevocableSystemPermission("android.permission.UNKNOWN_PERMISSION_NAME")); } public void testRevocable2() { assertTrue(new SetPermissionLookup(Collections.<String>emptySet(), Sets.newHashSet("my.permission1", "my.permission2")).isRevocable("my.permission2")); } public void testAppliesTo() { ResolvedAnnotation annotation; PermissionRequirement req; // No date range applies to permission annotation = createAnnotation(PERMISSION_ANNOTATION, new ResolvedAnnotation.Value("value", "android.permission.AUTHENTICATE_ACCOUNTS")); req = PermissionRequirement.create(null, annotation); assertTrue(req.appliesTo(getHolder(15, 1))); assertTrue(req.appliesTo(getHolder(15, 19))); assertTrue(req.appliesTo(getHolder(15, 23))); assertTrue(req.appliesTo(getHolder(22, 23))); assertTrue(req.appliesTo(getHolder(23, 23))); // Permission discontinued in API 23: annotation = createAnnotation(PERMISSION_ANNOTATION, new ResolvedAnnotation.Value("value", "android.permission.AUTHENTICATE_ACCOUNTS"), new ResolvedAnnotation.Value("apis", "..22")); req = PermissionRequirement.create(null, annotation); assertTrue(req.appliesTo(getHolder(15, 1))); assertTrue(req.appliesTo(getHolder(15, 19))); assertTrue(req.appliesTo(getHolder(15, 23))); assertTrue(req.appliesTo(getHolder(22, 23))); assertFalse(req.appliesTo(getHolder(23, 23))); // Permission requirement started in API 23 annotation = createAnnotation(PERMISSION_ANNOTATION, new ResolvedAnnotation.Value("value", "android.permission.AUTHENTICATE_ACCOUNTS"), new ResolvedAnnotation.Value("apis", "23..")); req = PermissionRequirement.create(null, annotation); assertFalse(req.appliesTo(getHolder(15, 1))); assertFalse(req.appliesTo(getHolder(1, 19))); assertFalse(req.appliesTo(getHolder(15, 22))); assertTrue(req.appliesTo(getHolder(22, 23))); assertTrue(req.appliesTo(getHolder(23, 30))); // Permission requirement applied from API 14 through API 18 annotation = createAnnotation(PERMISSION_ANNOTATION, new ResolvedAnnotation.Value("value", "android.permission.AUTHENTICATE_ACCOUNTS"), new ResolvedAnnotation.Value("apis", "14..18")); req = PermissionRequirement.create(null, annotation); assertFalse(req.appliesTo(getHolder(1, 5))); assertTrue(req.appliesTo(getHolder(15, 19))); } private static PermissionHolder getHolder(int min, int target) { return new PermissionHolder.SetPermissionLookup(Collections.<String>emptySet(), Collections.<String>emptySet(), new AndroidVersion(min, null), new AndroidVersion(target, null)); } public static void testDbUpToDate() throws Exception { List<String> expected = getDangerousPermissions(); if (expected == null) { return; } List<String> actual = Arrays.asList(REVOCABLE_PERMISSION_NAMES); if (!expected.equals(actual)) { System.out.println("Correct list of exceptions:"); for (String name : expected) { System.out.println(" \"" + name + "\","); } fail("List of revocable permissions has changed:\n" + // Make the diff show what it take to bring the actual results into the // expected results SdkTestCase.getDiff(Joiner.on('\n').join(actual), Joiner.on('\n').join(expected))); } } @Nullable private static List<String> getDangerousPermissions() throws IOException { Pattern pattern = Pattern.compile("dangerous"); String top = System.getenv("ANDROID_BUILD_TOP"); //$NON-NLS-1$ if (top == null) { top = "/Volumes/android/mnc-dev"; } // TODO: We should ship this file with the SDK! File file = new File(top, "frameworks/base/core/res/AndroidManifest.xml"); if (!file.exists()) { System.out.println("Set $ANDROID_BUILD_TOP to point to the git repository to check permissions"); return null; } boolean passedRuntimeHeader = false; boolean passedInstallHeader = false; String xml = Files.toString(file, Charsets.UTF_8); Document document = XmlUtils.parseDocumentSilently(xml, true); Set<String> revocable = Sets.newHashSet(); if (document != null && document.getDocumentElement() != null) { NodeList children = document.getDocumentElement().getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node child = children.item(i); short nodeType = child.getNodeType(); if (nodeType == Node.COMMENT_NODE) { String comment = child.getNodeValue(); if (comment.contains("RUNTIME PERMISSIONS")) { passedRuntimeHeader = true; } else if (comment.contains("INSTALLTIME PERMISSIONS")) passedInstallHeader = true; } else if (passedRuntimeHeader && !passedInstallHeader && nodeType == Node.ELEMENT_NODE && child.getNodeName().equals(TAG_PERMISSION)) { Element element = (Element) child; String protectionLevel = element.getAttributeNS(ANDROID_URI, "protectionLevel"); String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); if (!name.isEmpty() && pattern.matcher(protectionLevel).find()) { revocable.add(name); } } } } List<String> expected = Lists.newArrayList(revocable); Collections.sort(expected); return expected; } }