1 /* 2 * Copyright 2016, 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 package com.android.managedprovisioning.parser; 17 18 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DISCLAIMER_CONTENT; 19 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DISCLAIMER_HEADER; 20 import static com.android.managedprovisioning.common.StoreUtils.DIR_PROVISIONING_PARAMS_FILE_CACHE; 21 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.net.Uri; 26 import android.os.Bundle; 27 import android.os.Parcelable; 28 import androidx.annotation.Nullable; 29 import android.os.Binder; 30 31 import android.text.TextUtils; 32 import com.android.managedprovisioning.common.ProvisionLogger; 33 import com.android.managedprovisioning.common.StoreUtils; 34 import com.android.managedprovisioning.model.DisclaimersParam; 35 import com.android.managedprovisioning.model.DisclaimersParam.Disclaimer; 36 import java.io.File; 37 import java.util.ArrayList; 38 import java.util.List; 39 import java.util.Objects; 40 41 /** 42 * Parser for {@link EXTRA_PROVISIONING_DISCLAIMERS} into {@link DisclaimersParam} 43 * It also saves the disclaimer content into files 44 */ 45 public class DisclaimersParserImpl implements DisclaimerParser { 46 private static final int MAX_LENGTH = 3; 47 private static final String SCHEME_ANDROID_RESOURCE = "android.resource"; 48 private static final String SCHEME_CONTENT = "content"; 49 50 51 private final Context mContext; 52 private final long mProvisioningId; 53 private final File mDisclaimerDir; 54 DisclaimersParserImpl(Context context, long provisioningId)55 public DisclaimersParserImpl(Context context, long provisioningId) { 56 mContext = context; 57 mProvisioningId = provisioningId; 58 mDisclaimerDir = new File(mContext.getFilesDir(), DIR_PROVISIONING_PARAMS_FILE_CACHE); 59 } 60 61 @Nullable parse(Parcelable[] parcelables)62 public DisclaimersParam parse(Parcelable[] parcelables) throws ClassCastException { 63 if (parcelables == null) { 64 return null; 65 } 66 67 List<Disclaimer> disclaimers = new ArrayList<>(MAX_LENGTH); 68 for (int i = 0; i < parcelables.length; i++) { 69 // maximum 3 disclaimers are accepted in the EXTRA_PROVISIONING_DISCLAIMERS API 70 if (disclaimers.size() >= MAX_LENGTH) { 71 break; 72 } 73 final Bundle disclaimerBundle = (Bundle) parcelables[i]; 74 final String header = disclaimerBundle.getString(EXTRA_PROVISIONING_DISCLAIMER_HEADER); 75 final Uri uri = disclaimerBundle.getParcelable(EXTRA_PROVISIONING_DISCLAIMER_CONTENT); 76 if (TextUtils.isEmpty(header)) { 77 ProvisionLogger.logw("Empty disclaimer header in " + i + " element"); 78 continue; 79 } 80 81 if (uri == null) { 82 ProvisionLogger.logw("Null disclaimer content uri in " + i + " element"); 83 continue; 84 } 85 try { 86 validateUriSchemeAndPermission(uri); 87 } catch (SecurityException e) { 88 ProvisionLogger.loge( 89 "Skipping disclaimer in " 90 + i 91 + " element due to URI validation failure: " 92 + e.getMessage(), 93 e); 94 continue; 95 } 96 File disclaimerFile = saveDisclaimerContentIntoFile(uri, i); 97 98 if (disclaimerFile == null) { 99 ProvisionLogger.logw("Failed to copy disclaimer uri in " + i + " element"); 100 continue; 101 } 102 103 disclaimers.add(new Disclaimer(header, disclaimerFile.getPath())); 104 } 105 return disclaimers.isEmpty() ? null : new DisclaimersParam.Builder() 106 .setDisclaimers(disclaimers.toArray(new Disclaimer[disclaimers.size()])).build(); 107 } 108 109 /** 110 * Validates a {@link Uri} extra pointing to disclaimer content. 111 * 112 * <p>It checks that the URI scheme is one of {@code content} ({@link 113 * android.content.ContentResolver#SCHEME_CONTENT}) or {@code 114 * android.resource} ({@link 115 * android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}). If a {@code 116 * content:} URI is passed, it also checks that the caller has grant read 117 * permission ({@link Intent#FLAG_GRANT_READ_URI_PERMISSION}). 118 * 119 * @throws SecurityException if the URI scheme is invalid or the caller 120 * does not have permission to access the URI. 121 */ validateUriSchemeAndPermission(Uri uri)122 private void validateUriSchemeAndPermission(Uri uri) throws SecurityException { 123 ProvisionLogger.logd("validateUriSchemeAndPermission: " + uri); 124 String scheme = uri.getScheme(); 125 if (!Objects.equals(scheme, SCHEME_ANDROID_RESOURCE) 126 && !Objects.equals(scheme, SCHEME_CONTENT)) { 127 String errorMessage = "Invalid URI scheme: " + scheme; 128 throw new SecurityException(errorMessage); 129 } 130 if (Objects.equals(scheme, SCHEME_CONTENT)) { 131 int permissionCheck = 132 mContext.checkUriPermission( 133 uri, 134 Binder.getCallingPid(), 135 Binder.getCallingUid(), 136 Intent.FLAG_GRANT_READ_URI_PERMISSION); 137 138 if (permissionCheck != PackageManager.PERMISSION_GRANTED) { 139 String errorMessage = "Caller does not have permission to access" 140 + " disclaimer URI: " + uri; 141 throw new SecurityException(errorMessage); 142 } 143 } 144 } 145 146 /** 147 * @return {@link File} if the uri content is saved into the file successfully. Otherwise, 148 * return null. 149 */ saveDisclaimerContentIntoFile(Uri uri, int index)150 private File saveDisclaimerContentIntoFile(Uri uri, int index) { 151 if (!mDisclaimerDir.exists()) { 152 mDisclaimerDir.mkdirs(); 153 } 154 155 String filename = "disclaimer_content_" + mProvisioningId + "_" + index + ".txt"; 156 File outputFile = new File(mDisclaimerDir, filename); 157 158 boolean success = StoreUtils.copyUriIntoFile(mContext.getContentResolver(), uri, 159 outputFile); 160 return success ? outputFile : null; 161 } 162 } 163