From ef75108ccc1a1baf7361a5deade927d0ce878b56 Mon Sep 17 00:00:00 2001 From: SuperBatata Date: Wed, 29 Jan 2025 16:37:39 +0100 Subject: [PATCH] fix : refactor functions and added logging --- .../walt/policies/policies/DynamicPolicy.kt | 207 +++++++++++++----- 1 file changed, 148 insertions(+), 59 deletions(-) diff --git a/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/DynamicPolicy.kt b/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/DynamicPolicy.kt index d58407120..8d1a21f27 100644 --- a/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/DynamicPolicy.kt +++ b/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/DynamicPolicy.kt @@ -4,6 +4,7 @@ import id.walt.credentials.utils.VCFormat import id.walt.crypto.utils.JsonUtils.toJsonObject import id.walt.policies.CredentialDataValidatorPolicy import id.walt.policies.DynamicPolicyException +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.plugins.contentnegotiation.* @@ -20,8 +21,12 @@ import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport +private val logger = KotlinLogging.logger {} @Serializable -data class PolicyArgs( +data class DynamicPolicyConfig( + val opaServer: String = "http://localhost:8181", + val policyQuery: String = "vc/verification", + val policyName: String, val rules: Map, val argument: Map ) @@ -38,6 +43,9 @@ class DynamicPolicy : CredentialDataValidatorPolicy() { override val supportedVCFormats = setOf(VCFormat.jwt_vc, VCFormat.jwt_vc_json, VCFormat.ldp_vc) companion object { + private const val MAX_REGO_CODE_SIZE = 1_000_000 // 1MB limit + private const val MAX_POLICY_NAME_LENGTH = 64 + private val http = HttpClient { install(ContentNegotiation) { json() @@ -45,87 +53,168 @@ class DynamicPolicy : CredentialDataValidatorPolicy() { } } - fun cleanCode(input: String): String { - // Replace \r\n with \n to normalize line endings - val normalized = input.replace("\r\n", "\n") - - // Split the string into lines - val lines = normalized.split("\n") + private fun cleanCode(input: String): String { + return input.replace("\r\n", "\n") + .split("\n").joinToString("\n") { it.trim() } + } - // Remove any leading or trailing whitespace from each line - val cleanedLines = lines.map { it.trim() } + private fun validatePolicyName(policyName: String) { + require(policyName.matches(Regex("^[a-zA-Z0-9_-]+$"))) { + "Policy name contains invalid characters. Only alphanumeric characters, underscores, and hyphens are allowed." + } + require(policyName.length <= MAX_POLICY_NAME_LENGTH) { + "Policy name exceeds maximum length of $MAX_POLICY_NAME_LENGTH characters" + } + } - // Join the lines back together with proper line endings - return cleanedLines.joinToString("\n") + private fun validateRegoCode(regoCode: String) { + require(regoCode.isNotEmpty()) { + "Rego code cannot be empty" + } + require(regoCode.length <= MAX_REGO_CODE_SIZE) { + "Rego code exceeds maximum allowed size of $MAX_REGO_CODE_SIZE bytes" + } } - @JvmBlocking - @JvmAsync - @JsPromise - @JsExport.Ignore - override suspend fun verify( - data: JsonObject, - args: Any?, - context: Map - ): Result { - val rules = (args as JsonObject)["rules"]?.jsonObject + + private fun parseConfig(args: Any?): DynamicPolicyConfig { + require(args is JsonObject) { "Args must be a JsonObject" } + + val rules = args["rules"]?.jsonObject ?: throw IllegalArgumentException("The 'rules' field is required.") - val opaServer = (args)["opa_server"]?.jsonPrimitive?.content - ?: "http://localhost:8181" - val policyQuery = (args)["policy_query"]?.jsonPrimitive?.content - ?: "vc/verification" - val argument = (args)["argument"]?.jsonObject - ?: throw IllegalArgumentException("The 'argument' field is required.") - val policyName = (args)["policy_name"]?.jsonPrimitive?.content + val policyName = args["policy_name"]?.jsonPrimitive?.content ?: throw IllegalArgumentException("The 'policy_name' field is required.") - val regoCode = rules["rego"] - ?: return Result.failure(Exception("The 'rego' code is required in the 'rules' field.")) + val argument = args["argument"]?.jsonObject + ?: throw IllegalArgumentException("The 'argument' field is required.") + + return DynamicPolicyConfig( + opaServer = args["opa_server"]?.jsonPrimitive?.content ?: "http://localhost:8181", + policyQuery = args["policy_query"]?.jsonPrimitive?.content ?: "vc/verification", + policyName = policyName, + rules = rules.mapValues { it.value.jsonPrimitive.content }, + argument = argument.mapValues { it.value.jsonPrimitive.content } + ) + } + - val cleanedRegoCode = """ - ${ - cleanCode(regoCode.jsonPrimitive.content) + private suspend fun getRegoCode(config: DynamicPolicyConfig): String { + val regoCode = config.rules["rego"] + val policyUrl = config.rules["policy_url"] + + return when { + policyUrl != null -> { + logger.info { "Fetching rego code from URL: $policyUrl" } + try { + val response = http.get(policyUrl) + cleanCode(response.bodyAsText()) + } catch (e: Exception) { + logger.error(e) { "Failed to fetch rego code from URL: $policyUrl" } + throw DynamicPolicyException("Failed to fetch rego code: ${e.message}") + } + } + + regoCode != null -> cleanCode(regoCode) + else -> throw IllegalArgumentException("Either 'rego' or 'policy_url' must be provided in rules") } - """.trimIndent() + } - // upload the policy to OPA - val upload: HttpResponse = http.put("$opaServer/v1/policies/$policyName") { - contentType(ContentType.Text.Plain) - setBody(cleanedRegoCode) + + private suspend fun uploadPolicy(opaServer: String, policyName: String, regoCode: String): Result { + return try { + logger.info { "Uploading policy to OPA server: $policyName" } + val response = http.put("$opaServer/v1/policies/$policyName") { + contentType(ContentType.Text.Plain) + setBody(regoCode) + } + if (!response.status.isSuccess()) { + logger.error { "Failed to upload policy: ${response.status}" } + Result.failure(DynamicPolicyException("Failed to upload policy: ${response.status}")) + } else { + Result.success(Unit) + } + } catch (e: Exception) { + logger.error(e) { "Failed to upload policy" } + Result.failure(DynamicPolicyException("Failed to upload policy: ${e.message}")) } + } - check(upload.status.isSuccess()) { - "Failed to upload the policy to OPA. Check the policy code (rego) and try again." + private suspend fun deletePolicy(opaServer: String, policyName: String) { + try { + logger.info { "Deleting policy from OPA server: $policyName" } + http.delete("$opaServer/v1/policies/$policyName") + } catch (e: Exception) { + logger.error(e) { "Failed to delete policy" } } + } + - val input = mapOf( - "parameter" to argument, - "credentialData" to data.toMap() - ).toJsonObject() + private suspend fun verifyPolicy( + config: DynamicPolicyConfig, + data: JsonObject + ): Result { + return try { + logger.info { "Verifying policy: ${config.policyName}" } + val input = mapOf( + "parameter" to config.argument, + "credentialData" to data.toMap() + ).toJsonObject() + + val response = http.post("${config.opaServer}/v1/data/${config.policyQuery}/${config.policyName}") { + contentType(ContentType.Application.Json) + setBody(mapOf("input" to input)) + } + val result = response.body()["result"]?.jsonObject + ?: throw DynamicPolicyException("Invalid response from OPA server") - // verify the policy - val response: HttpResponse = http.post("$opaServer/v1/data/$policyQuery/$policyName") { - contentType(ContentType.Application.Json) - setBody(mapOf("input" to input)) + Result.success(result) + } catch (e: Exception) { + logger.error(e) { "Policy verification failed" } + Result.failure(DynamicPolicyException("Policy verification failed: ${e.message}")) } + } + @JvmBlocking + @JvmAsync + @JsPromise + @JsExport.Ignore + override suspend fun verify( + data: JsonObject, + args: Any?, + context: Map + ): Result { + return try { + logger.info { "Starting policy verification process" } + val config = parseConfig(args) + validatePolicyName(config.policyName) - val result = response.body()["result"]?.jsonObject - ?: throw IllegalArgumentException("Something went wrong while verifying the policy.") + val regoCode = getRegoCode(config) + validateRegoCode(regoCode) - val allow = result["allow"] + uploadPolicy(config.opaServer, config.policyName, regoCode).getOrThrow() - // delete the policy from OPA - http.delete("$opaServer/v1/policies/$policyName") - return if (allow is JsonPrimitive && allow.booleanOrNull == true) { - Result.success(result) - } else { + verifyPolicy(config, data).map { result -> + val allow = result["allow"] + if (allow is JsonPrimitive && allow.booleanOrNull == true) { + result + } else { + throw DynamicPolicyException("The policy condition was not met for policy ${config.policyName}") + } + } + } catch (e: Exception) { + logger.error(e) { "Policy verification failed" } Result.failure( - DynamicPolicyException( - message = "The policy condition was not met for policy ${policyName}." - ) + when (e) { + is DynamicPolicyException -> e + else -> DynamicPolicyException("Policy verification failed: ${e.message}") + } ) + } finally { + runCatching { + val config = parseConfig(args) + deletePolicy(config.opaServer, config.policyName) + } } }