Skip to content

Instantly share code, notes, and snippets.

@InfoSec812
Last active December 30, 2016 18:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save InfoSec812/e95b3fd4a0ec4b83aefecbf151ec06d9 to your computer and use it in GitHub Desktop.
Save InfoSec812/e95b3fd4a0ec4b83aefecbf151ec06d9 to your computer and use it in GitHub Desktop.
Groovy Verticle using Closures and currying to eliminate duplicate logic
package com.sungardas.cc.edison.verticles
import static com.sungardas.cc.edison.support.Queries.*
import static com.sungardas.cc.vertx.HTTPUtil.*
import static groovy.json.JsonOutput.toJson
import static io.netty.handler.codec.http.HttpResponseStatus.*
import static io.vertx.core.http.HttpMethod.GET
import static io.vertx.core.http.HttpMethod.POST
import com.sungardas.cc.vertx.handlers.AuthHandler
import groovy.transform.PackageScope
import io.netty.handler.codec.http.HttpResponseStatus
import io.vertx.core.AsyncResult
import io.vertx.core.Future
import io.vertx.core.Handler
import io.vertx.core.json.DecodeException
import io.vertx.core.json.JsonArray
import io.vertx.core.json.JsonObject
import io.vertx.core.logging.Logger
import io.vertx.core.logging.LoggerFactory
import io.vertx.groovy.core.CompositeFuture
import io.vertx.groovy.core.Context
import io.vertx.groovy.core.Vertx
import io.vertx.groovy.core.buffer.Buffer
import io.vertx.groovy.core.eventbus.Message
import io.vertx.groovy.ext.web.Router
import io.vertx.groovy.ext.web.RoutingContext
import io.vertx.lang.groovy.GroovyVerticle
import java.util.stream.Collectors
/**
* REST API Verticle
*/
class REST extends GroovyVerticle {
private static final Logger LOG = LoggerFactory.getLogger(REST)
public static final String QUERY_ADDR = 'cc.v5.database.query'
@PackageScope
Map config
/**
* Default constructor with default parameters. Defined this way for easier unit testing
* @param vertx The {@link Vertx} instance to be used
* @param context The {@link Context} instance to be used
*/
REST(final Vertx vertx=null, final Context context=null) {
super()
this.vertx = vertx?:this.vertx
this.context = context?:this.context
}
@Override
void start(final Future<Void> startFuture) throws Exception {
config = context.config()
final Router rtr = Router.router(vertx)
// Map all unhandled exceptions to this exception handler
rtr.exceptionHandler(this.&exceptionHandler)
// All requests are passed through the auth hander
final AuthHandler authHandler = new AuthHandler(vertx, config)
rtr.route() .handler(authHandler.&handle)
// Create references to the transformer methods and rest handler so that they are short and readable
def (rest, dsTransform, vmTransform, diskTransform, simple) = [this.&rest, this.&dsTransform, this.&vmTransform,
this.&diskTransform, this.&simpleTranform]
// This is jumping through some extra hoops to reduce duplicate code and logic.
// Each handler is passed a Closure which is curried with the QUERY and the transformation method (and possibly
// auth requirements), thus just leaving the RoutingContext parameter to be passed when the handler calls the
// closure. Keeping in mind that the handler() expects a Closure which ONLY accepts a RoutingContext param.
// By currying in this manner, the code can be written so that most logic flows through the same path and only
// the portions which are different get special treatment (e.g. the transformation methods)
// In the `rest` handler, we use the transformation Closure to do any required manipulation of the SQL results
rtr.route(GET, '/v3/status*') .handler(this.&status)
rtr.route(GET, '/v3/ds/:id') .handler(rest.curry(DATASTORE, dsTransform))
rtr.route(GET, '/v3/cluster/:id') .handler(rest.curry(CLUSTER, simple))
rtr.route(GET, '/v3/network/:id') .handler(rest.curry(NETWORK, simple))
rtr.route(GET, '/v3/customer/:snt_code') .handler(rest.curry(CUSTOMER, simple))
rtr.route(GET, '/v3/vdc/:id') .handler(rest.curry(VDC, simple))
rtr.route(GET, '/v3/vm/:uuid') .handler(rest.curry(VIRTUALMACHINE, vmTransform))
rtr.route(GET, '/v3/vm/:uuid/hdd/:order') .handler(rest.curry(DISK, diskTransform))
rtr.route(GET, '/v3/vdc/:id/password') .handler(rest.curry(VDC_PASSWORD, simple).rcurry('admin'))
rtr.route(POST, '/v3/vm/lb/update') .handler(this.&lbUpdate)
rtr.route(POST, '/v3/vm/lb/clean/:vdc/:name').handler(this.&lbClean)
final serverOpts = [
logActivity: true,
host: config.listenAddr,
port: config.listenPort,
reuseAddress: true,
compressionSupported: true
]
if (config.ssl) {
serverOpts['ssl'] = true
serverOpts['pemKeyCertOptions'] = [keyPath: config.key, certPath: config.certificate]
}
LOG.debug('Finished deploying REST Verticle')
vertx.createHttpServer(serverOpts).requestHandler(rtr.&accept).listen(startFuture.completer())
}
/**
* Handles any uncaught exceptions in the Router processing
* @param throwable
*/
static void exceptionHandler(final Throwable throwable) {
LOG.error('Unexpected exception in router', throwable)
}
/**
* Handle HTTP(s) requests for simple REST API GET endpoints. All path params from the request are mapped to SQL
* where clause entries
* @param ctx The {@link RoutingContext} for the request
* @param query The SQL Query to be executed to retrieve the appropriate data
* @param handler A {@link Closure} to be called on the SQL result row in order to do an required transformations
* before sending the response to the HTTP client
* @param requireAuth Enable or disable authentication for the given route
* @param requiredRole If more than just authentication is required, allow the required role to be specified
*/
@PackageScope
void rest(final String query, final Closure transform, final RoutingContext ctx,
final Boolean requireAuth = true, final String requiredRole = null) {
if (isAuthorized(requireAuth, requiredRole, ctx)) {
final params = []
final conditions = ctx.pathParams() // Get path parameters Map
.entrySet() // Convert Map to List<Map.Entry>
.stream() // Iterate over path parameters
.peek({ params.add(it.value) }) // Add each parameter value to the parameters list
.map({ "`${it.key}`=?" }) // Add each parameter key to the WHERE clause
.collect(Collectors.toList()) // Collect results into a List<String>
.join(' AND ') // Join the items together with AND statements
final msg = new JsonObject([query: "${query} WHERE ${conditions}".toString(), params: params])
vertx.eventBus().send(QUERY_ADDR, msg, this.&respond.curry(ctx).rcurry(transform) as Handler)
} else {
unauthorized(ctx, new JsonObject([
error: 'Unauthorized',
details: "${requiredRole} role is required for LB update"
]))
}
}
/**
* Determine if a user is authorized
* @param requireAuth Enable/Disable auth checking
* @param requiredRole The name of the role required for this authorization
* @param ctx The {@link RoutingContext} which contains the user details like the roles
* @return {@code true} if authorized, otherwise {@link false}
*/
boolean isAuthorized(final boolean requireAuth, final String requiredRole, final RoutingContext ctx) {
def inRequiredRole = (requiredRole != null) && (ctx.get('user')?.roles[requiredRole])
return config.disableAuth || !requireAuth || inRequiredRole
}
/**
* A REST endpoint which will return the current status of the application
* @param ctx The {@link RoutingContext} for the current request
*/
@PackageScope
void status(final RoutingContext ctx) {
def authVal = ctx?.get('auth')
final authorized = (authVal !=null && authVal)
def msg = new JsonObject([auth: authorized])
vertx.eventBus().send('cc.v5.edison.status', msg, this.&statusResponseHandler.curry(ctx))
}
/**
* Sends the results from the Status Verticle back to the HTTP client
* @param ctx The {@link RoutingContext} for the current request
* @param res The response from the Status Verticle
*/
static void statusResponseHandler(final RoutingContext ctx, final AsyncResult<Message<Map>> res) {
HttpResponseStatus status = OK
JsonObject result
if (res.succeeded()) {
result = new JsonObject(res.result().body())
} else {
status = INTERNAL_SERVER_ERROR
result = new JsonObject(toJson(res.cause()))
}
final jsonBody = result.encodePrettily()
buildJsonResponse(ctx, status).end(jsonBody)
}
/**
* Map the SQL results of a DataStore query into an appropriate datastore object for serialization
* @param dsMap The SQL result row as a Map
* @return The map after having processed the DataStore specific fields into the correct format
*/
Map dsTransform(final Map dsMap) {
final prefacedSelf = "${config.baseUrl}${dsMap.self}".toString()
dsMap.self = prefacedSelf
final vdcList = Arrays.stream(((String) dsMap.vdc).split(','))
.map({ "${config.baseUrl}/v3/vdc/${it}".toString() })
.collect(Collectors.toList())
dsMap.vdc = vdcList
return dsMap
}
/**
* Map the SQL results of a VirtualMachine query into an appropriate vm object for serialization
* @param vmMap The SQL result row as a Map
* @return The map after having processed the VirtualMachine specific fields into the correct format
*/
Map vmTransform(final Map vmMap) {
final prefacedSelf = "${config.baseUrl}${vmMap.self}".toString()
vmMap.self = prefacedSelf
final ifString = "${vmMap.interfaces}".replaceAll(/,$/, '')
final interfaces = new JsonArray("[${ifString}]")
vmMap.interfaces = interfaces
return vmMap
}
/**
* Map the SQL results of a Disk query into an appropriate disk object for serialization
* @param diskMap The SQL result row as a Map
* @return The map after having processed the Disk specific fields into the correct format
*/
Map diskTransform(final Map diskMap) {
final prefacedSelf = "${config.baseUrl}${diskMap.self}".toString()
final datastore = "${config.baseUrl}${diskMap.datastore}".toString()
diskMap.self = prefacedSelf
diskMap.datastore = datastore
return diskMap
}
/**
* Map the SQL results of a document query into an appropriate object for serialization
* @param body The SQL result row as a Map
* @return The map after having processed the DataStore specific fields into the correct format
*/
Map simpleTranform(Map body) {
final prefacedSelf = "${config.baseUrl}${body.self}".toString()
body.self = prefacedSelf
return body
}
/**
* Handle simple DB query responses which require no special mapping
* @param ctx The {@link RoutingContext} for the request
* @param reply The database result message from the Database verticle
*/
@PackageScope
void respond(final RoutingContext ctx, final AsyncResult<Message<Map>> reply,
final Closure transform = this.&simpleTranform) {
if (reply.succeeded()) {
final body = reply.result().body()
if (body) {
ok(ctx, new JsonObject(transform.call(body) as Map))
} else {
notFound(ctx)
}
} else {
serverError(ctx, reply.cause())
}
}
/**
* Handle requests for LB update
* @param ctx The {@link RoutingContext} for the request
*/
@PackageScope
void lbUpdate(final RoutingContext ctx) {
if (ctx?.get('user')?.roles?.admin) {
ctx.request().bodyHandler({ final body -> lbUpdateBodyHandler(body, ctx) })
} else {
unauthorized(ctx, new JsonObject([error: 'Unauthorized', details: 'ADMIN role is required for LB update']))
}
}
/**
* Handles the body content once the entire POST body has been recieved.
* @param body A {@link Buffer} containing the POST body content
* @param ctx The {@link RoutingContext} for the request
*/
@PackageScope
void lbUpdateBodyHandler(final Buffer body, final RoutingContext ctx) {
try {
final update = body.toJsonObject()
final params = [
update.uuid,
update.oldName,
update.os,
update.template,
update.newName
]
final query = LB_UPDATE
final resultHandler = { final AsyncResult<Message> result -> handleLBUpdateResult(ctx, result, update) }
vertx.eventBus().send(QUERY_ADDR, new JsonObject([query: query, params: params]), resultHandler)
} catch (final DecodeException de) {
LOG.error('Unable to decode POST body a JSON', de)
badRequest(ctx, de)
}
}
/**
* Handle the DB response for an LB Update operation
* @param ctx The {@link RoutingContext} for the request
* @param res The {@link AsyncResult} which indicates if the process succeeded or failed.
* @param update The updated record as returned by the DB
*/
@PackageScope
void handleLBUpdateResult(final RoutingContext ctx, final AsyncResult res, final Map update) {
if (res.succeeded()) {
final body = [:]
body.putAll(update)
body.remove('oldName')
final prefacedSelf = "${config.baseUrl}${body.self}".toString()
body.self = prefacedSelf
buildJsonResponse(ctx, ACCEPTED).end(new JsonObject(body).encodePrettily())
} else {
LOG.error('Unable to update LB details: {}', res.cause(), new JsonObject(update).encodePrettily())
serverError(ctx, res.cause())
}
}
/**
* Handle a LB Cleanup request
* @param ctx The {@link RoutingContext} for the request
*/
@PackageScope
void lbClean(final RoutingContext ctx) {
if (ctx?.get('user')?.roles?.admin) {
final queries = [
CD_DELETE,
HDD_DELETE,
VM_DELETE
]
final futureList = []
queries.each {
final future = io.vertx.groovy.core.Future.future()
futureList.add(future)
final query = [query: it, params: [ctx.request().getParam('vdc'), ctx.request().getParam('name')]]
vertx.eventBus().send(QUERY_ADDR, query, future.completer())
}
CompositeFuture.join(futureList).setHandler({ final res -> handleLBCleanResult(ctx, res) })
} else {
unauthorized(ctx, new JsonObject([error: 'Unauthorized', details: 'ADMIN role is required for LB Clean']))
}
}
/**
* Handle the Async result from the LB Clean query
* @param ctx The {@link RoutingContext} for the request
* @param res The {@link AsyncResult} which indicates if the process succeeded or failed.
*/
static void handleLBCleanResult(RoutingContext ctx, AsyncResult res) {
if (res.succeeded()) {
buildJsonResponse(ctx, ACCEPTED).end()
} else {
serverError(ctx, res.cause())
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment