Skip to content

Commit

Permalink
fix : refactor functions and added logging
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperBatata committed Jan 29, 2025
1 parent f846f42 commit ef75108
Showing 1 changed file with 148 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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<String, String>,
val argument: Map<String, String>
)
Expand All @@ -38,94 +43,178 @@ 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()
}
}
}

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<String, Any>
): Result<Any> {

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<Unit> {
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<JsonObject> {
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<JsonObject>()["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<String, Any>
): Result<Any> {

return try {
logger.info { "Starting policy verification process" }
val config = parseConfig(args)
validatePolicyName(config.policyName)

val result = response.body<JsonObject>()["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)
}
}
}

Expand Down

0 comments on commit ef75108

Please sign in to comment.