Skip to content

Commit 981e54b

Browse files
committed
daemon: record the author of each configuration transaction
The rollback log said what changed and when, but not who made the change, so the identities the northbound authenticates were used to admit a request and then discarded. Record an author alongside each transaction and return it from ListTransactions. A commit arriving over a Unix socket is attributed to the peer's user, taken from the socket credentials rather than from anything the client sends, while a remote one is attributed to the user it authenticated as, qualified by the address it came from. Signed-off-by: Renato Westphal <renatowestphal@gmail.com>
1 parent 1396b8a commit 981e54b

5 files changed

Lines changed: 75 additions & 11 deletions

File tree

holo-daemon/src/northbound/client/api.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ pub mod client {
7474
#[derive(Debug)]
7575
pub struct CommitRequest {
7676
pub config: CommitConfiguration,
77+
pub author: String,
7778
pub comment: String,
7879
pub confirmed_timeout: u32,
7980
pub responder: Responder<Result<CommitResponse>>,

holo-daemon/src/northbound/client/gnmi.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ impl proto::GNmi for GNmiService {
174174
grpc_request: Request<proto::SetRequest>,
175175
) -> Result<Response<proto::SetResponse>, Status> {
176176
let yang_ctx = YANG_CTX.get().unwrap();
177+
let author = grpc::request_author(&grpc_request);
177178
let grpc_request = grpc_request.into_inner();
178179
trace_span!("northbound").in_scope(|| {
179180
trace_span!("client", name = "gnmi").in_scope(|| {
@@ -235,6 +236,7 @@ impl proto::GNmi for GNmiService {
235236
// Convert and relay gNMI request to the northbound.
236237
let nb_request = api::client::CommitRequest {
237238
config: api::CommitConfiguration::Replace(candidate),
239+
author,
238240
comment: Default::default(),
239241
confirmed_timeout: 0,
240242
responder: responder_tx,

holo-daemon/src/northbound/client/grpc.rs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ use holo_utils::auth::Users;
1717
use holo_utils::task::Task;
1818
use holo_yang::{YANG_CTX, YANG_FEATURES};
1919
use nix::sys::stat::{Mode, fchmod};
20+
use nix::unistd::{Uid, User};
2021
use tokio::net::UnixListener;
2122
use tokio::sync::mpsc::Sender;
2223
use tokio::sync::{mpsc, oneshot, watch};
2324
use tokio_stream::wrappers::{UnboundedReceiverStream, UnixListenerStream};
2425
use tonic::metadata::MetadataMap;
2526
use tonic::service::interceptor::InterceptedService;
26-
use tonic::transport::server::Router;
27+
use tonic::transport::server::{Router, UdsConnectInfo};
2728
use tonic::transport::{Identity, Server, ServerTlsConfig};
2829
use tonic::{Request, Response, Status};
2930
use tracing::{trace, trace_span};
@@ -53,6 +54,10 @@ pub(crate) struct Authenticator {
5354
unix: bool,
5455
}
5556

57+
// User name a request was authenticated with.
58+
#[derive(Clone, Debug)]
59+
struct AuthenticatedUser(String);
60+
5661
// Where a server accepts connections.
5762
#[derive(Debug)]
5863
pub(crate) enum Listener {
@@ -298,6 +303,7 @@ impl proto::Northbound for NorthboundService {
298303
&self,
299304
grpc_request: Request<proto::CommitRequest>,
300305
) -> Result<Response<proto::CommitResponse>, Status> {
306+
let author = request_author(&grpc_request);
301307
let grpc_request = grpc_request.into_inner();
302308
trace_span!("northbound").in_scope(|| {
303309
trace_span!("client", name = "grpc").in_scope(|| {
@@ -335,6 +341,7 @@ impl proto::Northbound for NorthboundService {
335341
let nb_request =
336342
api::client::Request::Commit(api::client::CommitRequest {
337343
config,
344+
author,
338345
comment: grpc_request.comment,
339346
confirmed_timeout: grpc_request.confirmed_timeout,
340347
responder: responder_tx,
@@ -426,8 +433,9 @@ impl proto::Northbound for NorthboundService {
426433
nb_response.transactions.into_iter().map(|transaction| {
427434
Ok(proto::ListTransactionsResponse {
428435
id: transaction.id,
429-
comment: transaction.comment,
430436
date: transaction.date.to_string(),
437+
author: transaction.author,
438+
comment: transaction.comment,
431439
})
432440
});
433441

@@ -534,19 +542,24 @@ impl Authenticator {
534542
// user.
535543
pub(crate) fn intercept(
536544
&self,
537-
request: Request<()>,
545+
mut request: Request<()>,
538546
) -> Result<Request<()>, Status> {
539-
self.authenticate(request.metadata())?;
547+
if let Some(user) = self.authenticate(request.metadata())? {
548+
request.extensions_mut().insert(AuthenticatedUser(user));
549+
}
540550

541551
Ok(request)
542552
}
543553

544-
fn authenticate(&self, metadata: &MetadataMap) -> Result<(), Status> {
554+
fn authenticate(
555+
&self,
556+
metadata: &MetadataMap,
557+
) -> Result<Option<String>, Status> {
545558
// The socket's file permissions already decide who may connect, and
546559
// the peer's identity comes from the kernel, so no password is asked
547560
// for.
548561
if self.unix {
549-
return Ok(());
562+
return Ok(None);
550563
}
551564

552565
let Some((username, password)) = credentials(metadata) else {
@@ -563,7 +576,7 @@ impl Authenticator {
563576
return Err(Status::unauthenticated("invalid credentials"));
564577
}
565578

566-
Ok(())
579+
Ok(Some(username.to_owned()))
567580
}
568581
}
569582

@@ -812,6 +825,40 @@ fn unix_listener(path: &FsPath) -> std::io::Result<UnixListenerStream> {
812825

813826
// ===== global functions =====
814827

828+
// Identifies who issued a request.
829+
//
830+
// A Unix socket carries the peer's credentials, so its user comes from the
831+
// kernel and is prefixed with "unix:". A remote request is attributed to the
832+
// user it authenticated as, qualified by the address it came from.
833+
pub(crate) fn request_author<T>(request: &Request<T>) -> String {
834+
const UNKNOWN: &str = "unknown";
835+
836+
if let Some(info) = request.extensions().get::<UdsConnectInfo>() {
837+
let user = match info.peer_cred {
838+
Some(cred) => {
839+
let uid = Uid::from_raw(cred.uid());
840+
User::from_uid(uid)
841+
.ok()
842+
.flatten()
843+
.map(|user| user.name)
844+
.unwrap_or_else(|| format!("uid:{uid}"))
845+
}
846+
None => UNKNOWN.to_owned(),
847+
};
848+
return format!("unix:{user}");
849+
}
850+
851+
let user = request
852+
.extensions()
853+
.get::<AuthenticatedUser>()
854+
.map(|user| user.0.as_str())
855+
.unwrap_or(UNKNOWN);
856+
match request.remote_addr() {
857+
Some(address) => format!("{user}@{}", address.ip()),
858+
None => user.to_owned(),
859+
}
860+
}
861+
815862
// Sets up the listener and the server for the given address.
816863
//
817864
// An address starting with a slash is taken as the path of a Unix socket,

holo-daemon/src/northbound/core.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ pub struct Transaction {
7676
#[serde(with = "chrono::serde::ts_seconds")]
7777
pub date: DateTime<Utc>,
7878

79+
// User that committed the transaction.
80+
pub author: String,
81+
7982
// Optional comment for the transaction.
8083
pub comment: String,
8184

@@ -199,6 +202,7 @@ impl Northbound {
199202
let response = self
200203
.process_client_commit(
201204
request.config,
205+
request.author,
202206
request.comment,
203207
request.confirmed_timeout,
204208
)
@@ -270,6 +274,7 @@ impl Northbound {
270274
async fn process_client_commit(
271275
&mut self,
272276
config: capi::CommitConfiguration,
277+
author: String,
273278
comment: String,
274279
confirmed_timeout: u32,
275280
) -> Result<capi::client::CommitResponse> {
@@ -296,7 +301,7 @@ impl Northbound {
296301

297302
// Create configuration transaction.
298303
let transaction_id = self
299-
.create_transaction(candidate, comment, confirmed_timeout)
304+
.create_transaction(candidate, author, comment, confirmed_timeout)
300305
.await?;
301306
Ok(capi::client::CommitResponse { transaction_id })
302307
}
@@ -375,7 +380,12 @@ impl Northbound {
375380
let comment = "Confirmed commit rollback".to_owned();
376381
let rollback = self.confirmed_commit.rollback.take().unwrap();
377382
if let Err(error) = self
378-
.create_transaction(rollback.configuration, comment, 0)
383+
.create_transaction(
384+
rollback.configuration,
385+
String::new(),
386+
comment,
387+
0,
388+
)
379389
.await
380390
{
381391
error!(%error, "failed to rollback to previous configuration");
@@ -390,6 +400,7 @@ impl Northbound {
390400
async fn create_transaction(
391401
&mut self,
392402
candidate: DataTree<'static>,
403+
author: String,
393404
comment: String,
394405
confirmed_timeout: u32,
395406
) -> Result<u32> {
@@ -461,7 +472,7 @@ impl Northbound {
461472
// Create transaction structure.
462473
let candidate = Arc::try_unwrap(candidate).unwrap();
463474
let mut transaction =
464-
Transaction::new(Utc::now(), comment, candidate);
475+
Transaction::new(Utc::now(), author, comment, candidate);
465476

466477
// Record transaction.
467478
let mut db = self.db.lock().unwrap();

proto/holo.proto

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,11 @@ message ListTransactionsResponse {
244244
// Date and time the transaction was committed.
245245
string date = 2;
246246

247+
// User that committed the transaction.
248+
string author = 3;
249+
247250
// Comment assigned to the transaction.
248-
string comment = 3;
251+
string comment = 4;
249252
}
250253

251254
//

0 commit comments

Comments
 (0)