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