Merge branch 'rpc-auth-and-meta' into 'main'

rpc: authentication and basic handle manipulation

See merge request tpo/core/arti!1200
This commit is contained in:
Nick Mathewson 2023-05-24 14:53:29 +00:00
commit 3d4b9aa1b7
8 changed files with 340 additions and 114 deletions

View File

@ -1,12 +1,13 @@
//! RPC connection support, mainloop, and protocol implementation.
mod auth;
use std::{
collections::HashMap,
pin::Pin,
sync::{Arc, Mutex},
};
use arti_client::TorClient;
use asynchronous_codec::JsonCodecError;
use futures::{
channel::mpsc,
@ -17,7 +18,6 @@ use pin_project::pin_project;
use rpc::dispatch::BoxedUpdateSink;
use serde_json::error::Category as JsonErrorCategory;
use tor_async_utils::SinkExt as _;
use tor_rtcompat::PreferredRuntime;
use crate::{
cancel::{Cancel, CancelHandle},
@ -32,7 +32,7 @@ use tor_rpcbase as rpc;
///
/// Tracks information that persists from one request to another.
pub struct Connection {
/// The mutable state of this connection
/// The mutable state of this connection.
inner: Mutex<Inner>,
/// Lookup table to find the implementations for methods
@ -92,7 +92,7 @@ impl Connection {
}
/// Look up a given object by its object ID relative to this connection.
fn lookup_object(
pub(crate) fn lookup_object(
self: &Arc<Self>,
id: &rpc::ObjectId,
) -> Result<Arc<dyn rpc::Object>, rpc::LookupError> {
@ -376,108 +376,29 @@ impl rpc::Context for RequestContext {
.insert_weak(object)
.encode()
}
}
/// A simple temporary method to echo a reply.
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Echo {
/// A message to echo.
msg: String,
}
rpc::decl_method! { "arti:x-echo" => Echo}
impl rpc::Method for Echo {
type Output = Echo;
type Update = rpc::NoUpdates;
}
fn release_owned(&self, id: &rpc::ObjectId) -> Result<(), rpc::LookupError> {
let idx = crate::objmap::GenIdx::try_decode(id)?;
if !idx.is_strong() {
return Err(rpc::LookupError::WrongType(id.clone()));
}
/// Implementation for calling "echo" on a TorClient.
///
/// TODO RPC: Remove this. It shouldn't exist.
async fn echo_on_session(
_obj: Arc<TorClient<PreferredRuntime>>,
method: Box<Echo>,
_ctx: Box<dyn rpc::Context>,
) -> Result<Echo, rpc::RpcError> {
Ok(*method)
}
let removed = self
.conn
.inner
.lock()
.expect("Lock poisoned")
.objects
.remove(idx);
rpc::rpc_invoke_fn! {
echo_on_session(TorClient<PreferredRuntime>,Echo);
}
/// The authentication scheme as enumerated in the spec.
///
/// Conceptually, an authentication scheme answers the question "How can the
/// Arti process know you have permissions to use or administer it?"
///
/// TODO RPC: The only supported one for now is "inherent:unix_path"
#[derive(Debug, Copy, Clone, serde::Deserialize)]
enum AuthenticationScheme {
/// Inherent authority based on the ability to access an AF_UNIX address.
#[serde(rename = "inherent:unix_path")]
InherentUnixPath,
}
/// Method to implement basic authentication. Right now only "I connected to
/// you so I must have permission!" is supported.
#[derive(Debug, serde::Deserialize)]
struct Authenticate {
/// The authentication scheme as enumerated in the spec.
///
/// TODO RPC: The only supported one for now is "inherent:unix_path"
scheme: AuthenticationScheme,
}
/// A reply from the `Authenticate` method.
#[derive(Debug, serde::Serialize)]
struct AuthenticateReply {
/// An owned reference to a `TorClient` object.
client: Option<rpc::ObjectId>,
}
rpc::decl_method! {"auth:authenticate" => Authenticate}
impl rpc::Method for Authenticate {
type Output = AuthenticateReply;
type Update = rpc::NoUpdates;
}
/// An error during authentication.
#[derive(Debug, Clone, thiserror::Error, serde::Serialize)]
enum AuthenticationFailure {}
impl tor_error::HasKind for AuthenticationFailure {
fn kind(&self) -> tor_error::ErrorKind {
// TODO RPC not right.
tor_error::ErrorKind::LocalProtocolViolation
if removed.is_some() {
Ok(())
} else {
Err(rpc::LookupError::NoObject(id.clone()))
}
}
}
/// Invoke the "authenticate" method on a connection.
///
/// TODO RPC: This behavior is wrong; we'll need to fix it to be all
/// capabilities-like.
async fn authenticate_connection(
unauth: Arc<Connection>,
method: Box<Authenticate>,
ctx: Box<dyn rpc::Context>,
) -> Result<AuthenticateReply, rpc::RpcError> {
match method.scheme {
// For now, we only support AF_UNIX connections, and we assume that if
// you have permission to open such a connection to us, you have
// permission to use Arti. We will refine this later on!
AuthenticationScheme::InherentUnixPath => {}
}
let client = Arc::clone(&unauth.inner.lock().expect("Poisoned lock").client);
let client = Some(ctx.register_weak(client));
Ok(AuthenticateReply { client })
}
rpc::rpc_invoke_fn! {
authenticate_connection(Connection, Authenticate);
}
/// An error given when an RPC request is cancelled.
///
/// This is a separate type from [`crate::cancel::Cancelled`] since eventually

View File

@ -0,0 +1,183 @@
//! RPC commands and related functionality for authentication.
//!
//! In Arti's RPC system, authentication is a kind of method that can be invoked
//! on the special "connection" object, which gives you an RPC _session_ as a
//! result. The RPC session is the root for all other capabilities.
use std::sync::Arc;
use super::Connection;
use tor_rpcbase as rpc;
/*
TODO RPC: This is disabled because the design isn't really useful.
If we're going to provide something here, it should probably
contain a list of protocol elements/aspects, and it should be designed
to enable compatibility, with a clear view of what applications are
supposed to do about it.
/// Declare the get_rpc_protocol method.
mod get_rpc_protocol {
use super::Connection;
use std::sync::Arc;
use tor_rpcbase as rpc;
/// Method to inquire about the RPC protocol.
#[derive(Debug, serde::Deserialize)]
struct GetRpcProtocol {}
/// Reply to the [`GetRpcProtocol`] method
#[derive(Debug, serde::Serialize)]
struct GetProtocolReply {
/// The version of the RPC protocol that this server speaks.
// TODO RPC: Should this be a list?
version: RpcProtocolId,
}
/// Identifier for a version of this RPC meta-protocol.
#[derive(Debug, Copy, Clone, serde::Serialize)]
enum RpcProtocolId {
/// Alpha version of the protocol. Things might break between here and the
/// stable protocol.
///
/// TODO RPC: CHange this to v0.
#[serde(rename = "alpha")]
Alpha,
}
rpc::decl_method! {"auth:get_rpc_protocol" => GetRpcProtocol}
impl rpc::Method for GetRpcProtocol {
type Output = GetProtocolReply;
type Update = rpc::NoUpdates;
}
/// Describe which version of the RPC protocol our connection implements.
async fn conn_get_rpc_protocol(
_conn: Arc<Connection>,
_method: Box<GetRpcProtocol>,
_ctx: Box<dyn rpc::Context>,
) -> Result<GetProtocolReply, rpc::RpcError> {
Ok(GetProtocolReply {
version: RpcProtocolId::Alpha,
})
}
rpc::rpc_invoke_fn! {
conn_get_rpc_protocol(Connection, GetRpcProtocol);
}
}
*/
/// The authentication scheme as enumerated in the spec.
///
/// Conceptually, an authentication scheme answers the question "How can the
/// Arti process know you have permissions to use or administer it?"
///
/// TODO RPC: The only supported one for now is "inherent:unix_path"
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
enum AuthenticationScheme {
/// Inherent authority based on the ability to access an AF_UNIX address.
#[serde(rename = "inherent:unix_path")]
InherentUnixPath,
}
/// Method to ask which authentication methods are supported.
#[derive(Debug, serde::Deserialize)]
struct AuthQuery {}
/// A list of supported authentication schemes and their parameters.
#[derive(Debug, serde::Serialize)]
struct SupportedAuth {
/// A list of the supported authentication schemes.
///
/// TODO RPC: Actually, this should be able to contain strings _or_ maps,
/// where the maps are additional information about the parameters needed
/// for a particular scheme. But I think that's a change we can make later
/// once we have a scheme that takes parameters.
///
/// TODO RPC: Should we indicate which schemes get you additional privileges?
schemes: Vec<AuthenticationScheme>,
}
rpc::decl_method! {"auth:query" => AuthQuery}
impl rpc::Method for AuthQuery {
type Output = SupportedAuth;
type Update = rpc::NoUpdates;
}
/// Implement `auth:AuthQuery` on a connection.
async fn conn_authquery(
_conn: Arc<Connection>,
_query: Box<AuthQuery>,
_ctx: Box<dyn rpc::Context>,
) -> Result<SupportedAuth, rpc::RpcError> {
// Right now, every connection supports the same scheme.
Ok(SupportedAuth {
schemes: vec![AuthenticationScheme::InherentUnixPath],
})
}
rpc::rpc_invoke_fn! {
conn_authquery(Connection, AuthQuery);
}
/// Method to implement basic authentication. Right now only "I connected to
/// you so I must have permission!" is supported.
#[derive(Debug, serde::Deserialize)]
struct Authenticate {
/// The authentication scheme as enumerated in the spec.
///
/// TODO RPC: The only supported one for now is "inherent:unix_path"
scheme: AuthenticationScheme,
}
/// A reply from the `Authenticate` method.
#[derive(Debug, serde::Serialize)]
struct AuthenticateReply {
/// An owned reference to a `Session` object.
session: rpc::ObjectId,
}
rpc::decl_method! {"auth:authenticate" => Authenticate}
impl rpc::Method for Authenticate {
type Output = AuthenticateReply;
type Update = rpc::NoUpdates;
}
/// An error during authentication.
#[derive(Debug, Clone, thiserror::Error, serde::Serialize)]
enum AuthenticationFailure {}
impl tor_error::HasKind for AuthenticationFailure {
fn kind(&self) -> tor_error::ErrorKind {
// TODO RPC not right.
tor_error::ErrorKind::LocalProtocolViolation
}
}
/// Invoke the "authenticate" method on a connection.
///
/// TODO RPC: This behavior is wrong; we'll need to fix it to be all
/// capabilities-like.
async fn authenticate_connection(
unauth: Arc<Connection>,
method: Box<Authenticate>,
ctx: Box<dyn rpc::Context>,
) -> Result<AuthenticateReply, rpc::RpcError> {
match method.scheme {
// For now, we only support AF_UNIX connections, and we assume that if
// you have permission to open such a connection to us, you have
// permission to use Arti. We will refine this later on!
AuthenticationScheme::InherentUnixPath => {}
}
// TODO RPC: I'm actually not totally sure about the semantics of creating a
// new session object here, since it will _look_ separate from other
// sessions, but in fact they will all share the same object map.
//
// Perhaps we need to think more about the semantics of authenticating more
// then once on the same connection.
let client = unauth.inner.lock().expect("lock poisoned").client.clone();
let session = crate::session::Session::new(client);
let session = ctx.register_owned(session);
Ok(AuthenticateReply { session })
}
rpc::rpc_invoke_fn! {
authenticate_connection(Connection, Authenticate);
}

View File

@ -43,6 +43,7 @@ mod err;
mod mgr;
mod msgs;
mod objmap;
mod session;
mod streams;
pub use connection::{Connection, ConnectionError};

View File

@ -181,6 +181,11 @@ impl TaggedAddr {
/// analyze these object IDs, please contact the Arti developers instead and let
/// us give you a better way to do whatever you want.
impl GenIdx {
/// Return true if this is a strong (owning) reference.
pub(crate) fn is_strong(&self) -> bool {
matches!(self, GenIdx::Strong(_))
}
/// Encode `self` into an rpc::ObjectId that we can give to a client.
pub(crate) fn encode(self) -> rpc::ObjectId {
self.encode_with_rng(&mut rand::thread_rng())
@ -319,18 +324,19 @@ impl ObjMap {
}
}
/// Remove the entry at `idx`, if any.
pub(crate) fn remove(&mut self, idx: GenIdx) {
/// Remove and return the entry at `idx`, if any.
pub(crate) fn remove(&mut self, idx: GenIdx) -> Option<Arc<dyn rpc::Object>> {
match idx {
GenIdx::Weak(idx) => {
if let Some(entry) = self.weak_arena.remove(idx) {
let old_idx = self.reverse_map.remove(&entry.tagged_addr());
debug_assert_eq!(old_idx, Some(idx));
entry.obj.upgrade()
} else {
None
}
}
GenIdx::Strong(idx) => {
self.strong_arena.remove(idx);
}
GenIdx::Strong(idx) => self.strong_arena.remove(idx),
}
}

View File

@ -0,0 +1,92 @@
//! High-level APIs for an RPC session
//!
//! A "session" is created when a user authenticates on an RPC connection. It
//! is the root for all other RPC capabilities.
use std::sync::Arc;
use tor_rpcbase as rpc;
/// An authenticated RPC session.
pub(crate) struct Session {
/// An inner TorClient object that we use to implement remaining
/// functionality.
#[allow(unused)]
client: Arc<dyn rpc::Object>,
}
impl rpc::Object for Session {}
rpc::decl_object! {Session}
impl Session {
/// Create a new session object.
pub(crate) fn new(client: Arc<dyn rpc::Object>) -> Arc<Self> {
Arc::new(Self { client })
}
}
/// RPC method to release a single strong reference.
#[derive(Debug, serde::Deserialize)]
struct RpcRelease {
/// The object to release. Must be a strong reference.
///
/// TODO RPC: Releasing a weak reference is perilous and hard-to-define
/// based on how we have implemented our object ids. If you tell the objmap
/// to "release" a single name for a weak reference, you are releasing every
/// name for that weak reference, which may have surprising results.
///
/// This might be a sign of a design problem.
obj: rpc::ObjectId,
}
/// RPC method to release a single strong reference, creating a weak reference
/// in its place.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct RpcDowngrade {
/// The object to downgrade
obj: rpc::ObjectId,
}
rpc::decl_method! { "rpc:release" => RpcRelease}
impl rpc::Method for RpcRelease {
type Output = rpc::Nil;
type Update = rpc::NoUpdates;
}
/// Implementation for calling "release" on a Session.
async fn rpc_release(
_obj: Arc<Session>,
method: Box<RpcRelease>,
ctx: Box<dyn rpc::Context>,
) -> Result<rpc::Nil, rpc::RpcError> {
ctx.release_owned(&method.obj)?;
Ok(rpc::Nil::default())
}
rpc::rpc_invoke_fn! {
rpc_release(Session,RpcRelease);
}
/// A simple temporary method to echo a reply.
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Echo {
/// A message to echo.
msg: String,
}
rpc::decl_method! { "arti:x-echo" => Echo}
impl rpc::Method for Echo {
type Output = Echo;
type Update = rpc::NoUpdates;
}
/// Implementation for calling "echo" on a Session.
///
/// TODO RPC: Remove this. It shouldn't exist.
async fn echo_on_session(
_obj: Arc<Session>,
method: Box<Echo>,
_ctx: Box<dyn rpc::Context>,
) -> Result<Echo, rpc::RpcError> {
Ok(*method)
}
rpc::rpc_invoke_fn! {
echo_on_session(Session,Echo);
}

View File

@ -434,6 +434,10 @@ mod test {
fn register_weak(&self, _object: Arc<dyn crate::Object>) -> crate::ObjectId {
todo!()
}
fn release_owned(&self, _object: &crate::ObjectId) -> Result<(), crate::LookupError> {
todo!()
}
}
#[async_test]

View File

@ -80,9 +80,7 @@ pub trait Context: Send {
/// Look up an object by identity within this context.
fn lookup_object(&self, id: &ObjectId) -> Result<Arc<dyn Object>, LookupError>;
/// Find an owning reference to `object` within this context. If none can
/// be found, or if only a non-owning reference is found, make sure that
/// this context contains an owning reference to `object`.
/// Create an owning reference to `object` within this context.
///
/// Return an ObjectId for this object.
///
@ -90,9 +88,9 @@ pub trait Context: Send {
/// function depending on how we decide to name and specify things.
fn register_owned(&self, object: Arc<dyn Object>) -> ObjectId;
/// Find any owning reference to `object` within this context. If none can
/// be found, make sure that
/// this context contains a non-owning reference to `object`.
/// Make sure that
/// this context contains a non-owning reference to `object`,
/// creating one if necessary.
///
/// Return an ObjectId for this object.
///
@ -102,6 +100,13 @@ pub trait Context: Send {
/// TODO RPC: We may need to change the above semantics and the name of this
/// function depending on how we decide to name and specify things.
fn register_weak(&self, object: Arc<dyn Object>) -> ObjectId;
/// Drop an owning reference to the object called `object` within this context.
///
/// This will return an error if `object` is not an owning reference.
///
/// TODO RPC should this really return a LookupError?
fn release_owned(&self, object: &ObjectId) -> Result<(), LookupError>;
}
/// An error caused while trying to send an update to a method.
@ -148,3 +153,13 @@ pub trait ContextExt: Context {
}
}
impl<T: Context> ContextExt for T {}
/// A serializable empty object.
///
/// Used when we need to declare that a method returns nothing.
///
/// TODO RPC: Perhaps we can get () to serialize as {} and make this an alias
/// for ().
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
#[non_exhaustive]
pub struct Nil {}

View File

@ -507,7 +507,7 @@ in order to receive any other object IDs.
The pre-authentication methods available on a connection are:
auth:get_proto
auth:get_rpc_protocol
: Ask Arti which version of the protocol is in use.
auth:query
@ -776,9 +776,13 @@ The echo command will only work post-authentication.
Here is an example session:
```
>>> {"id": "abc", "obj": "connection", "method": "auth:get_rpc_protocol", "params": {}}
<<< {"id":"abc","result":{"version":"alpha"}}
>>> {"id": "abc", "obj": "connection", "method": "auth:query", "params": {}}
<<< {"id":"abc","result":{"schemes":["inherent:unix_path"]}}
>>> {"id": 3, "obj": "connection", "method": "auth:authenticate", "params": {"scheme": "inherent:unix_path"}}
<<< {"id":3,"result":{"client":"dTewFIaZKQV1N7AUhpkpBIrIT-t5Ztb8"}}
>>> {"id": 4, "obj": "dTewFIaZKQV1N7AUhpkpBIrIT-t5Ztb8", "method": "arti:x-echo", "params": {"msg": "Hello World"}}
<<< {"id":3,"result":{"session":"2yFi5qrMD9LbIWLmqswP0iTenRlVM_Au"}}
>>> {"id": 4, "obj": "2yFi5qrMD9LbIWLmqswP0iTenRlVM_Au", "method": "arti:x-echo", "params": {"msg": "Hello World"}}
<<< {"id":4,"result":{"msg":"Hello World"}}
```