diff --git a/modules/api/src/main/resources/application.conf b/modules/api/src/main/resources/application.conf index a90174f31..a49031e13 100644 --- a/modules/api/src/main/resources/application.conf +++ b/modules/api/src/main/resources/application.conf @@ -136,12 +136,6 @@ vinyldns { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "BIND Delete Zone Request", "type": "object", - "required": ["zoneName"], - "properties": { - "zoneName": { - "type": "string" - } - }, "additionalProperties": false } """ @@ -219,34 +213,16 @@ vinyldns { "additionalProperties": false } """ - delete-zone = """ - { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "PowerDNS Delete Zone", - "type": "object", - "required": ["name"], - "properties": { - "name": { "type": "string" } - }, - "additionalProperties": false - } - """ update-zone = """ { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "PowerDNS Update Zone", "type": "object", - "required": ["kind", "nameservers"], "properties": { "kind": { "type": "string", "enum": ["Native", "Master"] }, - "nameservers": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "pattern": "^[a-zA-Z0-9.-]+\\.$" } - }, "masters": { "type": "array", "items": { "type": "string" } @@ -265,11 +241,6 @@ vinyldns { "nameservers": "{{nameservers}}" } """ - delete-zone = """ - { - "name": "{{zoneName}}" - } - """ update-zone = """ { "name": "{{zoneName}}", diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala index 8d4776786..38d73f5a4 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala @@ -175,28 +175,7 @@ class ZoneService( 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( - operation: String, request: ZoneGenerationInput, auth: AuthPrincipal ): Result[ZoneGenerationResponse] = { @@ -204,22 +183,22 @@ class ZoneService( // Validate input providerConfig <- validateProvider(request.provider, dnsProviderApiConnection.providers).toResult _ <- validateZoneName(request.zoneName).toResult - _ <- schemaValidationResult(providerConfig, operation, request.providerParams) + _ <- schemaValidationResult(providerConfig, "create-zone", request.providerParams) _ <- logger.info(s"Request providerParams: ${request.providerParams}").toResult // Build request and endpoint - endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints(operation), request) - requestJsonOpt = buildGenerateZoneRequestJson(providerConfig.requestTemplates.get(operation), request) + endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints("create-zone"), request) + requestJsonOpt = buildGenerateZoneRequestJson(providerConfig.requestTemplates.get("create-zone"), request) // Authorization and existence checks _ <- canChangeZone(auth, request.zoneName, request.groupId).toResult - _ <- existenceCheck(operation, request.zoneName) + _ <- generateZoneDoesNotExist(request.zoneName).toResult // Send request _ <- logger.info(s"Request: provider=${request.provider}, path=$endpoint, request=$requestJsonOpt").toResult dnsProviderConn <- createConnection(endpoint).toResult - dnsConnResponse <- createDnsZoneService(providerConfig.apiKey, operation, requestJsonOpt, dnsProviderConn).toResult + dnsConnResponse <- createDnsZoneService(providerConfig.apiKey, "create-zone", requestJsonOpt, dnsProviderConn).toResult // Process response responseCode = dnsConnResponse.getResponseCode @@ -240,7 +219,107 @@ class ZoneService( ) _ <- logger.info(s"response: $zoneGenerateResponse").toResult - _ <- executeRepoAction(operation, request, zoneGenerateResponse) + zoneToGenerate = GenerateZone(request) + _ <- generateZoneRepository.save(zoneToGenerate.copy(response = Some(zoneGenerateResponse))).toResult[GenerateZone] + + } yield zoneGenerateResponse + } + + def handleUpdateGeneratedZoneRequest( + request: ZoneGenerationInput, + auth: AuthPrincipal + ): Result[ZoneGenerationResponse] = { + for { + existingGeneratedZone <- getGenerateZoneByName(request.zoneName, auth) + _ <- canChangeZone(auth, existingGeneratedZone.zoneName, existingGeneratedZone.groupId).toResult + + // Validate input + providerConfig <- validateProvider(request.provider, dnsProviderApiConnection.providers).toResult + _ <- validateZoneName(request.zoneName).toResult + _ <- schemaValidationResult(providerConfig, "update-zone", request.providerParams) + + _ <- logger.info(s"Request providerParams: ${request.providerParams}").toResult + + // Build request and endpoint + endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints("update-zone"), request) + requestJsonOpt = buildGenerateZoneRequestJson(providerConfig.requestTemplates.get("update-zone"), request) + + // Send request + _ <- logger.info(s"Request: provider=${request.provider}, path=$endpoint, request=$requestJsonOpt").toResult + dnsProviderConn <- createConnection(endpoint).toResult + dnsConnResponse <- createDnsZoneService(providerConfig.apiKey, "update-zone", requestJsonOpt, dnsProviderConn).toResult + + // Process response + responseCode = dnsConnResponse.getResponseCode + _ <- logger.info(s"response code: $responseCode").toResult + inputStream = if (responseCode >= 400) dnsConnResponse.getErrorStream else dnsConnResponse.getInputStream + responseMessage: String = Source.fromInputStream(inputStream, "UTF-8").mkString + _ <- isValidGenerateZoneConn(responseCode, responseMessage).toResult + + // Only parse JSON if the response is non-empty + responseJson = if (responseMessage.nonEmpty) parse(responseMessage) else JNothing + + // Create response object + zoneGenerateResponse = ZoneGenerationResponse( + provider = request.provider, + responseCode = responseCode, + status = dnsConnResponse.getResponseMessage, + message = responseJson + ) + _ <- logger.info(s"response: $zoneGenerateResponse").toResult + + updatedZone = existingGeneratedZone.copy( + email = request.email, + groupId = request.groupId, + providerParams = request.providerParams, + response = Some(zoneGenerateResponse) + ) + _ <- generateZoneRepository.save(updatedZone).toResult[GenerateZone] + + } yield zoneGenerateResponse + } + + def handleDeleteGeneratedZoneRequest( + generatedZoneId: String, + auth: AuthPrincipal + ): Result[ZoneGenerationResponse] = { + for { + generatedZone <- getGeneratedZoneOrFail(generatedZoneId) + _ <- canChangeZone(auth, generatedZone.zoneName, generatedZone.groupId).toResult + + providerConfig <- validateProvider(generatedZone.provider, dnsProviderApiConnection.providers).toResult + request = ZoneGenerationInput( + zoneName = generatedZone.zoneName, + provider = generatedZone.provider, + groupId = generatedZone.groupId, + email = generatedZone.email, + providerParams = generatedZone.providerParams + ) + endpoint = buildGenerateZoneEndpoint(providerConfig.endpoints("delete-zone"), request) + + dnsProviderConn <- createConnection(endpoint).toResult + dnsConnResponse <- createDnsZoneService(providerConfig.apiKey, "delete-zone", None, dnsProviderConn).toResult + + // Process response + responseCode = dnsConnResponse.getResponseCode + _ <- logger.info(s"response code: $responseCode").toResult + inputStream = if (responseCode >= 400) dnsConnResponse.getErrorStream else dnsConnResponse.getInputStream + responseMessage: String = Source.fromInputStream(inputStream, "UTF-8").mkString + _ <- isValidGenerateZoneConn(responseCode, responseMessage).toResult + + // Only parse JSON if the response is non-empty + responseJson = if (responseMessage.nonEmpty) parse(responseMessage) else JNothing + + // Create response object + zoneGenerateResponse = ZoneGenerationResponse( + provider = generatedZone.provider, + responseCode = responseCode, + status = dnsConnResponse.getResponseMessage, + message = responseJson + ) + _ <- logger.info(s"response: $zoneGenerateResponse").toResult + + _ <- generateZoneRepository.delete(generatedZone).toResult[GenerateZone] } yield zoneGenerateResponse } @@ -771,19 +850,19 @@ 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." - )) - } - } +// 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] = ensuring( @@ -826,6 +905,12 @@ class ZoneService( .orFail(ZoneNotFoundError(s"Zone with name $zoneName does not exists")) .toResult[GenerateZone] + def getGeneratedZoneOrFail(generatedZoneId: String): Result[GenerateZone] = + generateZoneRepository + .getGenerateZoneById(generatedZoneId) + .orFail(ZoneNotFoundError(s"Generated zone with id $generatedZoneId does not exists")) + .toResult[GenerateZone] + def validateZoneConnectionIfChanged(newZone: Zone, existingZone: Zone): Result[Unit] = if (newZone.connection != existingZone.connection || newZone.transferConnection != existingZone.transferConnection) { diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala index f4ae8ef47..0ab012ae8 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala @@ -16,8 +16,6 @@ package vinyldns.api.domain.zone -import cats.data.EitherT -import cats.effect.IO import vinyldns.api.Interfaces.Result import vinyldns.core.domain.auth.AuthPrincipal import vinyldns.core.domain.zone._ @@ -29,18 +27,34 @@ trait ZoneServiceAlgebra { auth: AuthPrincipal ): Result[ZoneCommandResult] - def handleGenerateZoneRequest(operation: String, request: ZoneGenerationInput, auth: AuthPrincipal): EitherT[IO, Throwable, ZoneGenerationResponse] + def handleGenerateZoneRequest( + request: ZoneGenerationInput, + auth: AuthPrincipal + ): Result[ZoneGenerationResponse] - def getGenerateZoneByName(zoneName: String, auth: AuthPrincipal): Result[GenerateZone] + def handleUpdateGeneratedZoneRequest( + request: ZoneGenerationInput, + auth: AuthPrincipal + ): Result[ZoneGenerationResponse] + + def handleDeleteGeneratedZoneRequest( + generatedZoneId: String, + auth: AuthPrincipal + ): Result[ZoneGenerationResponse] + + def getGenerateZoneByName( + zoneName: String, + auth: AuthPrincipal + ): Result[GenerateZone] def listGeneratedZones( - authPrincipal: AuthPrincipal, - nameFilter: Option[String], - startFrom: Option[String], - maxItems: Int, - searchByAdminGroup: Boolean, - ignoreAccess: Boolean - ): Result[ListGeneratedZonesResponse] + authPrincipal: AuthPrincipal, + nameFilter: Option[String], + startFrom: Option[String], + maxItems: Int, + searchByAdminGroup: Boolean, + ignoreAccess: Boolean + ): Result[ListGeneratedZonesResponse] def allowedDNSProviders(): Result[List[String]] @@ -69,12 +83,12 @@ trait ZoneServiceAlgebra { ): Result[ListZonesResponse] def listDeletedZones( - authPrincipal: AuthPrincipal, - nameFilter: Option[String], - startFrom: Option[String], - maxItems: Int, - ignoreAccess: Boolean - ): Result[ListDeletedZoneChangesResponse] + authPrincipal: AuthPrincipal, + nameFilter: Option[String], + startFrom: Option[String], + maxItems: Int, + ignoreAccess: Boolean + ): Result[ListDeletedZoneChangesResponse] def listZoneChanges( zoneId: String, @@ -98,8 +112,8 @@ trait ZoneServiceAlgebra { def getBackendIds(): Result[List[String]] def listFailedZoneChanges( - authPrincipal: AuthPrincipal, - startFrom: Int, - maxItems: Int - ): Result[ListFailedZoneChangesResponse] + authPrincipal: AuthPrincipal, + startFrom: Int, + maxItems: Int + ): Result[ListFailedZoneChangesResponse] } diff --git a/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala b/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala index 096b21e5f..c93a23d5d 100644 --- a/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala +++ b/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala @@ -146,28 +146,27 @@ class ZoneRoute( (post & monitor("Endpoint.generateZone")) { authenticateAndExecuteWithEntity[ZoneGenerationResponse, ZoneGenerationInput]( (authPrincipal, generateZone) => - zoneService.handleGenerateZoneRequest("create-zone", generateZone, authPrincipal) + zoneService.handleGenerateZoneRequest(generateZone, authPrincipal) ) { response => complete(StatusCodes.Accepted -> response) } } ~ - (delete & monitor("Endpoint.deleteGeneratedZone")) { - authenticateAndExecuteWithEntity[ZoneGenerationResponse, ZoneGenerationInput]( - (authPrincipal, generateZone) => - zoneService.handleGenerateZoneRequest("delete-zone", generateZone, authPrincipal) - ) { response => - complete(StatusCodes.Accepted, response) - } - } ~ (put & monitor("Endpoint.updateGeneratedZone")) { authenticateAndExecuteWithEntity[ZoneGenerationResponse, ZoneGenerationInput]( (authPrincipal, generateZone) => - zoneService.handleGenerateZoneRequest("update-zone", generateZone, authPrincipal) + zoneService.handleUpdateGeneratedZoneRequest(generateZone, authPrincipal) ) { response => complete(StatusCodes.Accepted, response) } } } ~ + path("zones" / "generate" / Segment) { id => + (delete & monitor("Endpoint.deleteGeneratedZone")) { + authenticateAndExecute(zoneService.handleDeleteGeneratedZoneRequest(id, _)) { response => + complete(StatusCodes.Accepted, response) + } + } + } ~ path("zones" / "generate" / "info") { (get & monitor("Endpoint.listGeneratedZones")) { parameters( diff --git a/modules/core/src/main/scala/vinyldns/core/domain/zone/GenerateZoneRepository.scala b/modules/core/src/main/scala/vinyldns/core/domain/zone/GenerateZoneRepository.scala index dcb34fd11..1204ed569 100644 --- a/modules/core/src/main/scala/vinyldns/core/domain/zone/GenerateZoneRepository.scala +++ b/modules/core/src/main/scala/vinyldns/core/domain/zone/GenerateZoneRepository.scala @@ -26,7 +26,9 @@ trait GenerateZoneRepository extends Repository { def getGenerateZoneByName(zoneName: String): IO[Option[GenerateZone]] - def deleteTx(generateZone: GenerateZone): IO[Unit] + def getGenerateZoneById(id: String): IO[Option[GenerateZone]] + + def delete(generateZone: GenerateZone): IO[GenerateZone] def listGenerateZones( authPrincipal: AuthPrincipal, diff --git a/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala b/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala index 7facfa3b5..0c494601e 100644 --- a/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala +++ b/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala @@ -224,7 +224,7 @@ trait ProtobufConversions { zgr.getProvider, zgr.getResponseCode.toInt, zgr.getStatus, - parse(zgr.getMessage) + if (Option(zgr.getMessage).exists(_.trim.nonEmpty)) parse(zgr.getMessage) else JNothing ) def fromPB(rd: VinylDNSProto.RecordData, rt: RecordType): RecordData = diff --git a/modules/mysql/src/main/resources/db/migration/V3.33__GenerateZone.sql b/modules/mysql/src/main/resources/db/migration/V3.33__GenerateZone.sql index 3d499e668..0fff4f054 100644 --- a/modules/mysql/src/main/resources/db/migration/V3.33__GenerateZone.sql +++ b/modules/mysql/src/main/resources/db/migration/V3.33__GenerateZone.sql @@ -8,6 +8,7 @@ Create the Generate Zone table CREATE TABLE generate_zone ( id CHAR(36) NOT NULL, name VARCHAR(256) NOT NULL, + provider VARCHAR(256) NOT NULL, admin_group_id CHAR(36) NOT NULL, response BLOB NOT NULL, data BLOB NOT NULL, diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGenerateZoneRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGenerateZoneRepository.scala index f2976d52f..d7995f461 100644 --- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGenerateZoneRepository.scala +++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGenerateZoneRepository.scala @@ -38,9 +38,10 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo */ private final val PUT_GENERATE_ZONE = sql""" - |INSERT INTO generate_zone(id, name, admin_group_id, response, data) - | VALUES ({id}, {name}, {adminGroupId}, {response}, {data}) ON DUPLICATE KEY + |INSERT INTO generate_zone(id, name, provider, admin_group_id, response, data) + | VALUES ({id}, {name}, {provider}, {adminGroupId}, {response}, {data}) ON DUPLICATE KEY | UPDATE name=VALUES(name), + | provider=VALUES(provider), | admin_group_id=VALUES(admin_group_id), | response=VALUES(response), | data=VALUES(data); @@ -61,6 +62,13 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo | WHERE name = ? """.stripMargin + private final val GET_GENERATED_ZONE_BY_ID = + sql""" + |SELECT data + | FROM generate_zone + | WHERE id = ? + """.stripMargin + private final val BASE_GENERATE_ZONE_SEARCH_SQL = """ |SELECT gz.data @@ -75,6 +83,7 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo .bindByName( 'id -> generateZone.id, 'name -> generateZone.zoneName, + 'provider -> generateZone.provider, 'adminGroupId -> generateZone.groupId, 'response -> toPB(generateZone.response.get).toByteArray, 'data -> toPB(generateZone).toByteArray @@ -96,11 +105,13 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo fromPB(VinylDNSProto.GenerateZone.parseFrom(res.bytes(columnIndex))) } - def deleteTx(generateZone: GenerateZone): IO[Unit] = + def delete(generateZone: GenerateZone): IO[GenerateZone] = monitor("repo.ZoneJDBC.generateZoneDelete") { IO { DB.localTx { implicit s => deleteGeneratedZone(generateZone) + + generateZone } } } @@ -108,6 +119,9 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo private def getGenerateZoneByNameInSession(zoneName: String)(implicit session: DBSession): Option[GenerateZone] = GET_GENERATED_ZONE_BY_NAME.bind(zoneName).map(extractGenerateZone(1)).first().apply() + private def getGenerateZoneByIdInSession(zoneId: String)(implicit session: DBSession): Option[GenerateZone] = + GET_GENERATED_ZONE_BY_ID.bind(zoneId).map(extractGenerateZone(1)).first().apply() + def getGenerateZoneByName(zoneName: String): IO[Option[GenerateZone]] = monitor("repo.ZoneJDBC.getGenerateZoneByName") { IO { @@ -117,6 +131,15 @@ class MySqlGenerateZoneRepository extends GenerateZoneRepository with ProtobufCo } } + def getGenerateZoneById(id: String): IO[Option[GenerateZone]] = + monitor("repo.ZoneJDBC.getGenerateZoneById") { + IO { + DB.readOnly { implicit s => + getGenerateZoneByIdInSession(id) + } + } + } + def listGenerateZones( authPrincipal: AuthPrincipal, zoneNameFilter: Option[String] = None,