proxy-wasm-rust-sdk icon indicating copy to clipboard operation
proxy-wasm-rust-sdk copied to clipboard

Suspending and re-dispatching HTTP requests which require external authorization

Open FrankTaylorLieder opened this issue 3 years ago • 1 comments

Hi,

Sorry for the long request, I am not sure if this will end up as a bug report, feature request or guidance, so I'm giving enough context on the problem.

I am looking to build an Envoy extension in Rust to enable external authorization of requests in sessions. Below is a simplified description of this authorization.

The first request in a session requires external authorization. However subsequent requests should be authorized locally (without an external call), for efficiency.

Further, we need to be able to support an asynchronous method to de-authorize a session.

In order to implement this set of flows I think I need the following:

EnovyPlugin
  • An HTTP Worker which handles each HTTP request. (called RP Worker above)
  • An Authz Callback Worker which handles the async session updates. (Out of scope for this.)
  • An Authz Service extension which maintains the session state and coordinates initial authz of each session.

The core problem I have is how the HTTP worker can pause a request and restart it when it gets the authorization response from the Authz Service extension (delivered using a queue).

(Note: I am new to Rust, so pardon any terminology slips!)

I'd like the following flow:

  1. HTTP Worker/Root Context handler creates the HTTP Context (in create_http_context()), capturing a copy in a Hashmap, indexed by context ID. (can't get this working due to ownership issues)

  2. HTTP Worker/HTTP Context: Intercepts request using on_http_request_headers(), sends a message to the Authz Service extension to validate the authz state of the session. (working)

  3. Authz Service: receive and process the authz request (either locally or by calling externally). Respond by sending a message to a queue (named in the authz request - owned by the Root Context). (working)

  4. HTTP Worker/Root Context: Use on_queue_ready() to detect the available response. It should then look up the suspended HTTP Context and dispatch it. (not working, as I can't get the hashmap from (1) working)

  5. HTTP Worker/HTTP Context: handle the authz response, either rejecting the HTTP request, or resuming it with resume_http_request(). (I assume this will work, untested)

The core problem is in step 1... capturing the HTTP Context for later re-dispatch.

At present create_http_context() is expecting a Box<dyn HttpContext> to be returned, so there is no scope of sharing access to this object.

So... options:

  1. Should the signature of create_http_context() be changed to use RefCell (or similar) instead of Box to enable implementations to capture and dynamically share the resulting HTTP Context?

  2. Is there another way for the Root Context on_queue_ready() handler to look up HTTP Contexts so we can dispatch them?

    The Dispatcher is already maintaining a table of context_id -> HTTP Context, can we get hold of this from the Root Context?

  3. Is there a completely different way to achieve this goal which I have missed?

Below is outline code for this... which does not work due to ownership issues.

Any input/suggestions on this would be gratefully received.

Thanks,

Frank.


...

struct AuthzWorkerRoot {
    _context_id: u32,
    http_ctxs: RefCell<HashMap<u32, Box<dyn ContinuableAuthzRequest>>>,
}

impl RootContext for AuthzWorkerRoot {
    fn get_type(&self) -> Option<ContextType> {
        Some(ContextType::HttpContext)
    }

    fn create_http_context(&self, context_id: u32) -> Option<Box<dyn HttpContext>> {
        let hc = Box::new(AuthzWorkerHttp { context_id: context_id }); // XXX RefCell or other?
        self.http_ctxs.borrow_mut().insert(context_id, hc);

        Some(hc) // Does not work due to borrow in the .insert above.
    }

    fn on_queue_ready(&mut self, qid: u32) {
        let rbs = match self.dequeue_shared_queue(qid) { ... handle de-queue of message ... }
        let msg = json::parse(str::from_utf8(&rbs).unwrap()).unwrap();
        
        match self.http_ctxs.borrow_mut().remove(msg.conext_id) {
            Some(hc): hc.borrow_mut().handle_authz_response(msg.result),
            None: ...
        }
    }
}

struct AuthzWorkerHttp {
    context_id: u32,
}

trait ContinuableAuthzRequest {
    fn handle_authz_response(&mut self, r: bool);
}

impl HttpContext for AuthzWorkerHttp {
    fn on_http_request_headers(&mut self, _: usize) -> Action {
        ... look up Authz Service message queue and prepare authz request message ...
        self.enqueue_shared_queue(service_queue, Some(json::stringify(msg).as_bytes()))
        ...
    }
}

impl ContinuableAuthzRequest for AuthzWorkerHttp {
    fn handle_authz_response(&mut self, r: bool) {
        if r {
            self.resume_http_request();
        } else {
            self.send_http_response(
                403,
                vec![("Powered-By", "AuthzWorkerHttp")],
                Some(b"Access denied by policy.\n"),
                );
        }
    }
}

FrankTaylorLieder avatar Nov 08 '21 17:11 FrankTaylorLieder

Hi. Can anyone comment on how I might suspend an HTTP request and re-dispatch it on response to a message received through a shared queue?

Many thanks.

FrankTaylorLieder avatar Nov 29 '21 11:11 FrankTaylorLieder