2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-08-22 02:02:14 +00:00

update zone generation handler, repository methods

This commit is contained in:
nspadaccino 2025-05-29 13:35:35 -04:00
parent 244f6895f3
commit 477ae80a3a
No known key found for this signature in database
GPG Key ID: AB060C9A2C68918E
4 changed files with 98 additions and 81 deletions

View File

@ -34,7 +34,6 @@ import com.cronutils.parser.CronParser
import com.cronutils.model.CronType import com.cronutils.model.CronType
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import vinyldns.api.domain.membership.MembershipService import vinyldns.api.domain.membership.MembershipService
import vinyldns.core.Messages
import org.json4s._ import org.json4s._
import org.json4s.jackson.JsonMethods._ import org.json4s.jackson.JsonMethods._
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
@ -45,8 +44,6 @@ import scala.util.Try
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.io.{ByteArrayInputStream, InputStream, OutputStream}
import java.net.{HttpURLConnection, URL} import java.net.{HttpURLConnection, URL}
import scala.io.Source import scala.io.Source
@ -169,6 +166,35 @@ class ZoneService(
new URL(apiUrl).openConnection().asInstanceOf[HttpURLConnection] new URL(apiUrl).openConnection().asInstanceOf[HttpURLConnection]
} }
private def schemaValidationResult(
providerConfig: DnsProviderConfig,
operation: String,
params: Map[String, JValue]
): Result[Unit] = providerConfig.schemas.get(operation) match {
case Some(schema) => JsonSchemaValidator.validate(schema, params).toResult
case None => result(())
}
private def existenceCheck(operation: String, zoneName: String): Result[Unit] = operation match {
case "create-zone" => generateZoneDoesNotExist(zoneName).toResult
case "delete-zone" => generateZoneExists(zoneName).toResult
case "update-zone" => generateZoneExists(zoneName).toResult
case _ => Left(InvalidRequest(s"Unsupported operation: $operation")).toResult
}
private def executeRepoAction(
operation: String,
request: ZoneGenerationInput,
response: ZoneGenerationResponse
): Result[Unit] = {
val zoneToGenerate = GenerateZone(request)
operation match {
case "delete-zone" => generateZoneRepository.deleteTx(zoneToGenerate).toResult
case "create-zone" => generateZoneRepository.save(zoneToGenerate.copy(response = Some(response))).toResult
case "update-zone" => generateZoneRepository.save(zoneToGenerate.copy(response = Some(response))).toResult
case _ => Left(InvalidRequest(s"Unsupported operation: $operation")).toResult
}
}
def handleGenerateZoneRequest( def handleGenerateZoneRequest(
operation: String, operation: String,
request: ZoneGenerationInput, request: ZoneGenerationInput,
@ -178,45 +204,32 @@ class ZoneService(
// Validate input // Validate input
providerConfig <- validateProvider(request.provider, dnsProviderApiConnection.providers).toResult providerConfig <- validateProvider(request.provider, dnsProviderApiConnection.providers).toResult
_ <- validateZoneName(request.zoneName).toResult _ <- validateZoneName(request.zoneName).toResult
_ <- schemaValidationResult(providerConfig, operation, request.providerParams)
// JSON Schema validation for providerParams
_ <- JsonSchemaValidator
.validate(
providerConfig.schemas(operation),
request.providerParams
).toResult
_ <- logger.info(s"Request providerParams: ${request.providerParams}").toResult _ <- logger.info(s"Request providerParams: ${request.providerParams}").toResult
// Build JSON request // Build request and endpoint
generateZoneRequestJson = buildGenerateZoneRequestJson(
providerConfig.requestTemplates(operation),
request
)
// Authorization checks
_ <- canChangeZone(auth, request.zoneName, request.groupId).toResult
_ <- generateZoneDoesNotExist(request.zoneName).toResult
endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints(operation), request) endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints(operation), request)
requestJsonOpt = buildGenerateZoneRequestJson(providerConfig.requestTemplates.get(operation), request)
_ = logger.info(s"Request: provider=${request.provider}, path=${endpoint}, request=$generateZoneRequestJson").toResult // Authorization and existence checks
_ <- canChangeZone(auth, request.zoneName, request.groupId).toResult
_ <- existenceCheck(operation, request.zoneName)
// Send request
_ <- logger.info(s"Request: provider=${request.provider}, path=$endpoint, request=$requestJsonOpt").toResult
dnsProviderConn <- createConnection(endpoint).toResult dnsProviderConn <- createConnection(endpoint).toResult
dnsConnResponse <- createDnsZoneService( dnsConnResponse <- createDnsZoneService(providerConfig.apiKey, operation, requestJsonOpt, dnsProviderConn).toResult
endpoint,
providerConfig.apiKey,
generateZoneRequestJson,
dnsProviderConn
).toResult
// Process response // Process response
responseCode = dnsConnResponse.getResponseCode responseCode = dnsConnResponse.getResponseCode
_ <- logger.info(s"response code: $responseCode").toResult
inputStream = if (responseCode >= 400) dnsConnResponse.getErrorStream else dnsConnResponse.getInputStream inputStream = if (responseCode >= 400) dnsConnResponse.getErrorStream else dnsConnResponse.getInputStream
responseMessage: String = Source.fromInputStream(inputStream, "UTF-8").mkString responseMessage: String = Source.fromInputStream(inputStream, "UTF-8").mkString
_ <- isValidGenerateZoneConn(responseCode, responseMessage).toResult _ <- isValidGenerateZoneConn(responseCode, responseMessage).toResult
// Parse response // Only parse JSON if the response is non-empty
responseJson = parse(responseMessage) responseJson = if (responseMessage.nonEmpty) parse(responseMessage) else JNothing
// Create response object // Create response object
zoneGenerateResponse = ZoneGenerationResponse( zoneGenerateResponse = ZoneGenerationResponse(
@ -225,32 +238,28 @@ class ZoneService(
status = dnsConnResponse.getResponseMessage, status = dnsConnResponse.getResponseMessage,
message = responseJson message = responseJson
) )
_ <- logger.info(s"response: $zoneGenerateResponse").toResult
// Save to repository _ <- executeRepoAction(operation, request, zoneGenerateResponse)
zoneToGenerate = GenerateZone(request)
_ <- generateZoneRepository.save(zoneToGenerate.copy(response = Some(zoneGenerateResponse))).toResult[GenerateZone]
} yield { } yield zoneGenerateResponse
// Cleanup resources
Option(inputStream).foreach(_.close())
Option(dnsConnResponse).foreach(_.disconnect())
zoneGenerateResponse
}
} }
// Build a Generate Zone JSON request using template engine // Build a Generate Zone JSON request using template engine
private def buildGenerateZoneRequestJson( private def buildGenerateZoneRequestJson(
requestTemplate: String, maybeRequestTemplate: Option[String],
zoneGenerationInput: ZoneGenerationInput zoneGenerationInput: ZoneGenerationInput
): String = { ): Option[String] = {
val baseParams = Map( val baseParams = Map(
"zoneName" -> JString(zoneGenerationInput.zoneName), "zoneName" -> JString(zoneGenerationInput.zoneName),
"provider" -> JString(zoneGenerationInput.provider), "provider" -> JString(zoneGenerationInput.provider),
"groupId" -> JString(zoneGenerationInput.groupId), "groupId" -> JString(zoneGenerationInput.groupId),
"email" -> JString(zoneGenerationInput.email) "email" -> JString(zoneGenerationInput.email)
) )
TemplateEngine.renderTemplate(requestTemplate, baseParams ++ zoneGenerationInput.providerParams) maybeRequestTemplate.map { requestTemplate =>
TemplateEngine.renderTemplate(requestTemplate, baseParams ++ zoneGenerationInput.providerParams)
}
} }
private def buildGenerateZoneEndpoint( private def buildGenerateZoneEndpoint(
@ -354,36 +363,41 @@ class ZoneService(
} }
} }
def createDnsZoneService(dnsApiUrl: String, dnsApiKey: String, request: String, connection: HttpURLConnection): Either[Throwable, HttpURLConnection] = def createDnsZoneService(
{ dnsApiKey: String,
operation: String,
request: Option[String],
connection: HttpURLConnection
): Either[Throwable, HttpURLConnection] = {
try { try {
//val connection = new URL(dnsApiUrl).openConnection().asInstanceOf[HttpURLConnection] // Map operation to HTTP method
connection.setRequestMethod("POST") val method = operation match {
case "create-zone" => "POST"
case "update-zone" => "PUT"
case "delete-zone" => "DELETE"
case other => throw new IllegalArgumentException(s"Unsupported operation: $other")
}
connection.setRequestMethod(method)
connection.setRequestProperty("Content-Type", "application/json") connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("X-API-Key", dnsApiKey) connection.setRequestProperty("X-API-Key", dnsApiKey)
connection.setDoOutput(true)
val outputStream: OutputStream = connection.getOutputStream
outputStream.write(request.getBytes("UTF-8"))
outputStream.close()
// Only send a body if the HTTP method and request are appropriate
val methodsWithBody = Set("POST", "PUT", "PATCH")
if (methodsWithBody.contains(method) && request.isDefined) {
connection.setDoOutput(true)
val outputStream = connection.getOutputStream
try {
outputStream.write(request.get.getBytes("UTF-8"))
} finally {
outputStream.close()
}
}
Right(connection) Right(connection)
} catch { } catch {
case e: Exception => case e: Exception =>
val errorConnection = new HttpURLConnection(new URL(dnsApiUrl)) { Left(e)
private val errorJson = Messages.dnsProviderConnRefusedMessage(e, dnsApiUrl)
private val errorBytes = errorJson.getBytes("UTF-8")
private val errorByteStream = new ByteArrayInputStream(errorBytes)
override def disconnect(): Unit = {}
override def usingProxy(): Boolean = false
override def connect(): Unit = {}
override def getResponseCode: Int = 500
override def getErrorStream: InputStream = errorByteStream
}
Right(errorConnection)
} }
} }
@ -757,6 +771,20 @@ class ZoneService(
} }
} }
private def generateZoneExists(zoneName: String): Either[Throwable, Unit] = {
val existingZoneOpt: Option[GenerateZone] =
generateZoneRepository.getGenerateZoneByName(zoneName).unsafeRunSync()
existingZoneOpt match {
case Some(_) =>
Right(())
case None =>
Left(ZoneNotFoundError(
s"Zone with name $zoneName does not exist."
))
}
}
def canScheduleZoneSync(auth: AuthPrincipal): Either[Throwable, Unit] = def canScheduleZoneSync(auth: AuthPrincipal): Either[Throwable, Unit] =
ensuring( ensuring(
NotAuthorizedError(s"User '${auth.signedInUser.userName}' is not authorized to schedule zone sync in this zone.") NotAuthorizedError(s"User '${auth.signedInUser.userName}' is not authorized to schedule zone sync in this zone.")

View File

@ -24,7 +24,6 @@ import vinyldns.api.Interfaces.ensuring
import vinyldns.core.domain.membership.User import vinyldns.core.domain.membership.User
import vinyldns.core.domain.record.RecordType import vinyldns.core.domain.record.RecordType
import vinyldns.core.domain.zone.{ACLRule, Zone, ZoneACL, DnsProviderConfig} import vinyldns.core.domain.zone.{ACLRule, Zone, ZoneACL, DnsProviderConfig}
import org.json4s._
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
@ -105,17 +104,6 @@ class ZoneValidations(syncDelayMillis: Int) {
case None => Left(InvalidRequest(s"Unsupported DNS provider: $provider")) case None => Left(InvalidRequest(s"Unsupported DNS provider: $provider"))
} }
def validateRequiredFields(
requiredFields: List[String],
providedFields: Map[String, JValue],
providerName: String
): Either[Throwable, Unit] = {
val missing = requiredFields.filterNot(providedFields.contains)
ensuring(InvalidRequest(
s"Missing required fields for $providerName: ${missing.mkString(", ")}"
))(missing.isEmpty)
}
def validateZoneName(zoneName: String): Either[Throwable, Unit] = def validateZoneName(zoneName: String): Either[Throwable, Unit] =
ensuring(InvalidRequest(s"Invalid zone name: $zoneName")) { ensuring(InvalidRequest(s"Invalid zone name: $zoneName")) {
zoneName.matches("""^[a-zA-Z0-9.-]+\.$""") zoneName.matches("""^[a-zA-Z0-9.-]+\.$""")

View File

@ -26,6 +26,8 @@ trait GenerateZoneRepository extends Repository {
def getGenerateZoneByName(zoneName: String): IO[Option[GenerateZone]] def getGenerateZoneByName(zoneName: String): IO[Option[GenerateZone]]
def deleteTx(generateZone: GenerateZone): IO[Unit]
def listGenerateZones( def listGenerateZones(
authPrincipal: AuthPrincipal, authPrincipal: AuthPrincipal,
zoneNameFilter: Option[String] = None, zoneNameFilter: Option[String] = None,

View File

@ -82,8 +82,7 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo
.update() .update()
.apply() .apply()
} }
generateZone generateZone
} }
}} }}
@ -97,7 +96,7 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo
fromPB(VinylDNSProto.GenerateZone.parseFrom(res.bytes(columnIndex))) fromPB(VinylDNSProto.GenerateZone.parseFrom(res.bytes(columnIndex)))
} }
def deleteTx(generateZone: GenerateZone): IO[GenerateZone] = def deleteTx(generateZone: GenerateZone): IO[Unit] =
monitor("repo.ZoneJDBC.generateZoneDelete") { monitor("repo.ZoneJDBC.generateZoneDelete") {
IO { IO {
DB.localTx { implicit s => DB.localTx { implicit s =>