Search code examples
rplumberapparmor

Combining R plumber and RAppArmor


I simply want to combine the packages plumber and RAppArmor in a way, that users can send code using the plumber-API to a server which is then safely evaluated with the help of profiles in RAppArmor. How can I make the following code work:

#* Evaluate the result
#* @post /eval_res
function(func){
  library("RAppArmor")
  data <- cbind(rnorm(100),rnorm(100))
  eval.secure(nrow(data),profile="r-user")
}

This code chunk is a simplified version of what is happening later. So far, it seems that any object specified in the function, e.g. data, cannot be passed to eval.secure given the user restrictions in r-user (standard profile of RAppArmor). I even tried allowing full access to /** for r-user by editing the profile, without success. Opening plumber on localhost and using curl with curl --data "func=function(x){return(nrow(x))}" "http://localhost:8000/eval_res" to get the result of eval.secure() results in an empty answer {}. The same code works without eval.secure() and correctly returns [100]. Can someone help me so I understand the problem better or even fix the code?

My current workaround is to save everything to a csv before eval.secure() and then read it within the eval.secure granting access to that folder in the r-user profile, but that is definitely not a convincing solution.

For those of you who are confused about the function: In a later step the option func will contain some code which will the be parsed and evaluated, but for this small example I thought it would only add unnecessary complexity.

Thanks in advance!


Solution

  • library("RAppArmor")
    
    #* Evaluate the result
    #* @post /eval_res
    function(func){
      data <- cbind(rnorm(100),rnorm(100))
      unix::eval_safe(nrow(data), profile="r-user")
    }
    

    So

    library("RAppArmor")
    
    #* Evaluate the result
    #* @post /eval_res
    function(func){
      data <- cbind(rnorm(100),rnorm(100))
      unix::eval_safe(eval(parse(text=func)), profile="r-user")
    }
    

    eval.secure just pass all parameters to unix::eval_safe. The reason why eval.secure does not work is because eval_safe expects to find your variables in its parent.frame(), which in the case of eval.secure is an empty function body.

    eval_safe use of parent.frame()

    function (expr, tmp = tempfile("fork"), std_out = stdout(), std_err = stderr(), 
        timeout = 0, priority = NULL, uid = NULL, gid = NULL, rlimits = NULL, 
        profile = NULL, device = pdf) 
    {
        orig_expr <- substitute(expr)
        out <- eval_fork(expr = tryCatch({
            if (length(priority)) 
                setpriority(priority)
            if (length(rlimits)) 
                set_rlimits(rlimits)
            if (length(gid)) 
                setgid(gid)
            if (length(uid)) 
                setuid(uid)
            if (length(profile)) 
                aa_change_profile(profile)
            if (length(device)) 
                options(device = device)
            graphics.off()
            options(menu.graphics = FALSE)
    ------> serialize(withVisible(eval(orig_expr, parent.frame())), 
                NULL)
        }, error = function(e) {
            old_class <- attr(e, "class")
            structure(e, class = c(old_class, "eval_fork_error"))
        }, finally = substitute(graphics.off())), tmp = tmp, timeout = timeout, 
            std_out = std_out, std_err = std_err)
        if (inherits(out, "eval_fork_error")) 
            base::stop(out)
        res <- unserialize(out)
        if (res$visible) 
            res$value
        else invisible(res$value)
    }
    
    # parent.frame() in eval_safe when using eval.secure
    function (...) 
    {
      # nothing here  
      unix::eval_safe(...)
    }
    
    # parent.frame() when using eval_safe directly
    function(func){
      data <- cbind(rnorm(100),rnorm(100))
      # Your stuff is here
      unix::eval_safe(nrow(data), profile="r-user")
    }