1 /*
2  * Copyright (C) 2022 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.libraries.pcc.chronicle.api.policy.builder
18 
19 import com.android.libraries.pcc.chronicle.api.DataTypeDescriptor
20 import com.android.libraries.pcc.chronicle.api.FieldType
21 import com.android.libraries.pcc.chronicle.api.Trigger
22 import com.android.libraries.pcc.chronicle.api.policy.FieldName
23 import com.android.libraries.pcc.chronicle.api.policy.Policy
24 import com.android.libraries.pcc.chronicle.api.policy.PolicyConfig
25 import com.android.libraries.pcc.chronicle.api.policy.PolicyField
26 import com.android.libraries.pcc.chronicle.api.policy.PolicyRetention
27 import com.android.libraries.pcc.chronicle.api.policy.PolicyTarget
28 import com.android.libraries.pcc.chronicle.api.policy.StorageMedium
29 import com.android.libraries.pcc.chronicle.api.policy.UsageType
30 import com.android.libraries.pcc.chronicle.api.policy.annotation.Annotation
31 import com.android.libraries.pcc.chronicle.api.policy.contextrules.All
32 import com.android.libraries.pcc.chronicle.api.policy.contextrules.PolicyContextRule
33 import java.time.Duration
34 
35 typealias UsageType = UsageType
36 
37 typealias StorageMedium = StorageMedium
38 
39 /**
40  * Builds a [Policy] with the supplied [name] and [egressType], using a [PolicyBuilder].
41  *
42  * Example:
43  *
44  * ```kotlin
45  * val myPolicy = policy("MyPolicy", "Analytics") {
46  *   description =
47  *     """
48  *     Policy describing valid usage of Foos and Bars when publishing statistics to cloud-based
49  *     analytics.
50  *     """.trimIndent()
51  *
52  *   allowedContexts = AllowAllContextsRule
53  *
54  *   config("AnalyticsServer") {
55  *     "url" to "https://mypolicyanalytics.com/stats"
56  *   }
57  *
58  *   target(fooDTD) {
59  *     maxAgeMillis = Duration.ofDays(5)
60  *
61  *     retention(StorageMedium.DISK, encryptionRequired = true)
62  *     retention(StorageMedium.RAM)
63  *
64  *     "age" to { rawUsage(UsageType.ANY) }
65  *     "parents" to {
66  *       "mother.firstName" to { rawUsage(UsageType.ANY) }
67  *       "father.firstName" to { rawUsage(UsageType.ANY) }
68  *     }
69  *     "address" to {
70  *       "latitude" to {
71  *         rawUsage(UsageType.JOIN)
72  *         conditionalUsage("citylevelAccuracy", UsageType.EGRESS)
73  *       }
74  *       "longitude" to {
75  *         rawUsage(UsageType.JOIN)
76  *         conditionalUsage("citylevelAccuracy", UsageType.EGRESS)
77  *       }
78  *     }
79  *   }
80  *
81  *   target(barDTD) {
82  *     maxAgeMillis = Duration.ofHours(6)
83  *
84  *     retention(StorageMedium.RAM)
85  *
86  *     "bestFriend.name" to { conditionalUsage("mangled", UsageType.ANY) }
87  *   }
88  * }
89  * ```
90  */
<lambda>null91 fun policy(name: String, egressType: String, block: PolicyBuilder.() -> Unit = {}): Policy =
92   PolicyBuilder(name, egressType).apply(block).build()
93 
94 /**
95  * Builds a [PolicyTarget] with the supplied [dataTypeDescriptor], using a [PolicyTargetBuilder].
96  *
97  * Providing `0` as [maxAgeMillis] implies that the target may not be held for any length of time.
98  *
99  * See [policy] for an example.
100  */
targetnull101 fun target(
102   dataTypeDescriptor: DataTypeDescriptor,
103   maxAge: Duration,
104   block: PolicyTargetBuilder.() -> Unit = {},
105 ): PolicyTarget =
106   PolicyTargetBuilder(dataTypeDescriptor, requireNotNull(dataTypeDescriptor.name), maxAge)
107     .apply(block)
108     .build()
109 
110 /** Builder of [Policy] instances. */
111 @DataDsl
112 class PolicyBuilder
113 internal constructor(
114   private val name: String,
115   private val egressType: String,
116 ) {
117   /** Human-readable description of the policy. */
118   var description: String = ""
119 
120   /**
121    * `allowedContext` can be used to define when a policy should be applied. PolicyContextRules can
122    * be expressed and combined with boolean logic, using `and`/`or`/`not` operators.
123    */
124   var allowedContext: PolicyContextRule = All
125 
126   // internal for tests only
127   internal val targets = mutableListOf<PolicyTarget>()
128   internal val configs = mutableMapOf<String, PolicyConfig>()
129 
130   /** Clones the PolicyBuilder. */
131   internal constructor(
132     policyBuilder: PolicyBuilder,
133   ) : this(policyBuilder.name, policyBuilder.egressType) {
<lambda>null134     this.apply {
135       description = policyBuilder.description
136       allowedContext = policyBuilder.allowedContext
137       // The values of these collections are immutable, so duplication the collections themselves is
138       // sufficient.
139       targets.addAll(policyBuilder.targets)
140       configs.putAll(policyBuilder.configs)
141     }
142   }
143 
targetnull144   fun target(
145     dataTypeDescriptor: DataTypeDescriptor,
146     maxAge: Duration,
147     block: PolicyTargetBuilder.() -> Unit,
148   ): PolicyTarget {
149     return PolicyTargetBuilder(dataTypeDescriptor, requireNotNull(dataTypeDescriptor.name), maxAge)
150       .apply(block)
151       .build()
152       .also(targets::add)
153   }
154 
155   /** Adds a [PolicyConfig] block to the [Policy] being built. */
confignull156   fun config(configName: String, block: PolicyConfigBuilder.() -> Unit): PolicyConfig =
157     PolicyConfigBuilder().apply(block).build().also { configs[configName] = it }
158 
159   /** Builds the [Policy]. */
buildnull160   fun build(): Policy {
161     return Policy(
162       name = name,
163       egressType = egressType,
164       description = description,
165       targets = targets,
166       configs = configs,
167       allowedContext = allowedContext
168     )
169   }
170 }
171 
172 /** Builder of [PolicyTarget] instances. */
173 @DataDsl
174 class PolicyTargetBuilder
175 internal constructor(
176   private val dataTypeDescriptor: DataTypeDescriptor,
177   private val schemaName: String,
178   /** The maximum allowable age of the entities being targeted. */
179   var maxAge: Duration,
180 ) {
181   private val retentions = mutableSetOf<PolicyRetention>()
182   private val fields = mutableSetOf<PolicyField>()
183   private val annotations = mutableSetOf<Annotation>()
184 
185   /**
186    * Adds a deletion requirement to a particular [field]. Use dots to delimit nested fields. For
187    * example: `target(FOO) { deletionTrigger(Trigger.PackageUninstalled, "packageName") }`
188    */
deletionTriggernull189   fun deletionTrigger(trigger: Trigger, field: String): PolicyTargetBuilder {
190     annotations.add(trigger.toAnnotation(field))
191     return this
192   }
193 
194   /** Adds a new [PolicyRetention] object to the [PolicyTarget] being built. */
retentionnull195   fun retention(medium: StorageMedium, encryptionRequired: Boolean = false): PolicyRetention =
196     PolicyRetention(medium, encryptionRequired).also(retentions::add)
197 
198   /**
199    * Adds a [PolicyField] to the [PolicyTarget] being built, with the receiving string as the
200    * dot-delimited access path of the field.
201    *
202    * Example:
203    *
204    * ```kotlin
205    * target(personDTD)) {
206    *   "name" { rawUsage(UsageType.ANY) }
207    *   "bestFriend.name" { rawUsage(UsageType.JOIN) }
208    * }
209    * ```
210    */
211   operator fun String.invoke(block: PolicyFieldBuilder.() -> Unit): PolicyField {
212     val entityDataTypeDescriptor =
213       PolicyFieldBuilder.validateAndGetDataTypeDescriptor(this, dataTypeDescriptor)
214     return PolicyFieldBuilder(entityDataTypeDescriptor, listOf(this))
215       .apply(block)
216       .build()
217       .also(fields::add)
218   }
219 
220   /** Builds the [PolicyTarget]. */
buildnull221   fun build(): PolicyTarget =
222     PolicyTarget(
223       schemaName = schemaName,
224       maxAgeMs = maxAge.toMillis(),
225       retentions = retentions.toList(),
226       fields = fields.toList(),
227       annotations = annotations.toList()
228     )
229 }
230 
231 /** Builder of [PolicyField] instances. */
232 @DataDsl
233 class PolicyFieldBuilder(
234   private val dataTypeDescriptor: DataTypeDescriptor?,
235   private val fieldPath: List<FieldName>
236 ) {
237   private val rawUsages = mutableSetOf<UsageType>()
238   private val redactedUsages = mutableMapOf<String, Set<UsageType>>()
239   private val subFields = mutableSetOf<PolicyField>()
240   private val annotations = mutableSetOf<Annotation>()
241 
242   /** Adds [UsageType]s as raw-usage affordances. */
243   fun rawUsage(vararg usageTypes: UsageType): PolicyFieldBuilder = apply {
244     rawUsages.addAll(usageTypes)
245   }
246 
247   /**
248    * Adds [UsageType]s as affordances, if and only if the [requiredLabel] is present on the field.
249    */
250   fun conditionalUsage(requiredLabel: String, vararg usageTypes: UsageType): PolicyFieldBuilder =
251     apply {
252       redactedUsages[requiredLabel] = (redactedUsages[requiredLabel] ?: emptySet()) + usageTypes
253     }
254 
255   fun ConditionalUsage.whenever(vararg usageTypes: UsageType): PolicyFieldBuilder {
256     conditionalUsage(this.serializedName, *usageTypes)
257     return this@PolicyFieldBuilder
258   }
259 
260   /** Adds a new sub-field to the [PolicyField] being built. */
261   operator fun String.invoke(block: PolicyFieldBuilder.() -> Unit): PolicyField {
262     val fieldDataTypeDescriptor =
263       requireNotNull(dataTypeDescriptor) { "Trying to lookup field '$this' in a non-entity type." }
264     val entityDataTypeDescriptor = validateAndGetDataTypeDescriptor(this, fieldDataTypeDescriptor)
265     return PolicyFieldBuilder(entityDataTypeDescriptor, fieldPath + listOf(this))
266       .apply(block)
267       .build()
268       .also(subFields::add)
269   }
270 
271   /** Builds the [PolicyField]. */
272   fun build(): PolicyField =
273     PolicyField(
274       fieldPath = fieldPath,
275       rawUsages = rawUsages,
276       redactedUsages = redactedUsages,
277       subfields = subFields.toList(),
278       annotations = annotations.toList()
279     )
280 
281   companion object {
282     /**
283      * Verifies that [fieldName] is a valid field in [dataTypeDescriptor] and returns the
284      * [DataTypeDescriptor] if [fieldName] is a field with another nested schema.
285      */
286     fun validateAndGetDataTypeDescriptor(
287       fieldName: String,
288       dataTypeDescriptor: DataTypeDescriptor
289     ): DataTypeDescriptor? {
290       // Only validate field if [schema] is not null.
291       val fieldType =
292         requireNotNull(dataTypeDescriptor.fields.get(fieldName)) {
293           "Field '$fieldName' not found in '${dataTypeDescriptor.name}'."
294         }
295       return fieldType.getDataTypeDescriptor(dataTypeDescriptor)
296     }
297 
298     /** Returns the underlying [DataTypeDescriptor] if available. */
299     private fun FieldType.getDataTypeDescriptor(
300       parentDataTypeDescriptor: DataTypeDescriptor
301     ): DataTypeDescriptor? {
302       return when (this) {
303         is FieldType.Array -> itemFieldType.getDataTypeDescriptor(parentDataTypeDescriptor)
304         is FieldType.List -> itemFieldType.getDataTypeDescriptor(parentDataTypeDescriptor)
305         is FieldType.Nested -> parentDataTypeDescriptor.innerTypes.find { it.name == this.name }
306         is FieldType.Nullable -> itemFieldType.getDataTypeDescriptor(parentDataTypeDescriptor)
307         FieldType.Boolean,
308         FieldType.Byte,
309         FieldType.ByteArray,
310         FieldType.Char,
311         FieldType.Double,
312         FieldType.Duration,
313         FieldType.Float,
314         FieldType.Instant,
315         FieldType.Integer,
316         FieldType.Long,
317         FieldType.Short,
318         FieldType.String,
319         is FieldType.Enum,
320         is FieldType.Opaque,
321         is FieldType.Reference,
322         is FieldType.Tuple -> null
323       }
324     }
325   }
326 }
327 
328 /** Builder of [PolicyConfig] maps. */
329 @DataDsl
330 class PolicyConfigBuilder {
331   private val backingMap = mutableMapOf<String, String>()
332 
333   /** Adds a key-value pair to the [PolicyConfig] being built. */
tonull334   infix fun String.to(value: String) {
335     backingMap[this] = value
336   }
337 
338   /** Builds the [PolicyConfig]. */
buildnull339   fun build(): PolicyConfig = backingMap
340 }
341