Skip to content

Commit

Permalink
[feat] add an admin client
Browse files Browse the repository at this point in the history
  • Loading branch information
jbtrystram committed Mar 30, 2022
1 parent 806a2d6 commit 1f6565b
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 23 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ base64 = "0.13"
base64-serde = "0.6"
chrono = { version = "0.4", features = ["serde"] }
futures = "0.3"
indexmap = { version = "1", features = ["serde"] }
humantime-serde = "1"
log = "0.4"
serde = { version = "1", features = ["derive"] }
Expand Down
2 changes: 2 additions & 0 deletions src/admin/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
//! Application administration API.
pub mod v1;
156 changes: 156 additions & 0 deletions src/admin/v1/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use super::data::*;
use crate::error::ClientError;
use crate::openid::TokenProvider;
use crate::util::Client;
use std::fmt::Debug;
use tracing::instrument;
use url::Url;

/// A device registry client, backed by reqwest.
#[derive(Clone, Debug)]
pub struct AdminClient<TP>
where
TP: TokenProvider,
{
client: reqwest::Client,
api_url: Url,
token_provider: TP,
}

enum AdministrationOperation {
Transfer,
Accept,
Members,
}

type ClientResult<T> = Result<T, ClientError<reqwest::Error>>;

impl<TP> Client<TP> for AdminClient<TP>
where
TP: TokenProvider,
{
/// Create a new client instance.
fn new(client: reqwest::Client, api_url: Url, token_provider: TP) -> Self {
Self {
client,
api_url,
token_provider,
}
}

fn client(&self) -> &reqwest::Client {
&self.client
}

fn token_provider(&self) -> &TP {
&self.token_provider
}
}

impl<TP> AdminClient<TP>
where
TP: TokenProvider,
{
fn url(&self, application: &str, operation: AdministrationOperation) -> ClientResult<Url> {
let mut url = self.api_url.clone();

{
let mut path = url
.path_segments_mut()
.map_err(|_| ClientError::Request("Failed to get paths".into()))?;

path.extend(&["api", "admin", "v1alpha1", "apps"]);
if !application.is_empty() {
path.push(application);
}
match operation {
AdministrationOperation::Transfer => path.push("transfer-ownership"),
AdministrationOperation::Accept => path.push("accept-ownership"),
AdministrationOperation::Members => path.push("members"),
};
}

Ok(url)
}

/// Get the application members and their roles
#[instrument]
pub async fn get_members<A>(&self, application: A) -> ClientResult<Option<Members>>
where
A: AsRef<str> + Debug,
{
self.read(self.url(application.as_ref(), AdministrationOperation::Members)?)
.await
}

/// Update the application members and their roles
#[instrument]
pub async fn update_members<A>(&self, application: A, members: Members) -> ClientResult<bool>
where
A: AsRef<str> + Debug,
{
self.update(
self.url(application.as_ref(), AdministrationOperation::Members)?,
Some(members),
)
.await
}

/// Transfer the application ownership to another user
#[instrument]
pub async fn initiate_app_transfer<A, U>(
&self,
application: A,
username: U,
) -> ClientResult<bool>
where
A: AsRef<str> + Debug,
U: AsRef<str> + Debug,
{
let payload = TransferOwnership {
new_user: username.as_ref().to_string(),
};

self.update(
self.url(application.as_ref(), AdministrationOperation::Transfer)?,
Some(payload),
)
.await
}

/// Cancel the application ownership transfer
#[instrument]
pub async fn cancel_app_transfer<A>(&self, application: A) -> ClientResult<bool>
where
A: AsRef<str> + Debug,
{
self.delete(self.url(application.as_ref(), AdministrationOperation::Transfer)?)
.await
}

/// Accept the application ownership transfer
#[instrument]
pub async fn accept_app_transfer<A>(&self, application: A) -> ClientResult<bool>
where
A: AsRef<str> + Debug,
{
self.update(
self.url(application.as_ref(), AdministrationOperation::Accept)?,
None::<()>,
)
.await
}

/// Read the application ownership transfer state
#[instrument]
pub async fn read_app_transfer<A>(
&self,
application: A,
) -> ClientResult<Option<TransferOwnership>>
where
A: AsRef<str> + Debug,
{
self.read(self.url(application.as_ref(), AdministrationOperation::Transfer)?)
.await
}
}
45 changes: 45 additions & 0 deletions src/admin/v1/data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use core::fmt::{Display, Formatter};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransferOwnership {
pub new_user: String,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Members {
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_version: Option<String>,
#[serde(default)]
pub members: IndexMap<String, MemberEntry>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MemberEntry {
pub role: Role,
}

#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum Role {
/// Allow everything, including changing members
Admin,
/// Allow reading and writing, but not changing members.
Manager,
/// Allow reading only.
Reader,
}

impl Display for Role {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Admin => write!(f, "Administrator"),
Self::Manager => write!(f, "Manager"),
Self::Reader => write!(f, "Reader"),
}
}
}
7 changes: 7 additions & 0 deletions src/admin/v1/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#[cfg(feature = "reqwest")]
mod client;
mod data;

#[cfg(feature = "reqwest")]
pub use client::*;
pub use data::*;
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! A client for the Drogue IoT Cloud APIs.
pub mod admin;
pub mod core;
pub mod error;
pub mod meta;
Expand Down
4 changes: 2 additions & 2 deletions src/registry/v1/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ where
pub async fn update_app(&self, application: &Application) -> ClientResult<bool> {
self.update(
self.url(Some(application.metadata.name.as_str()), None)?,
application,
Some(application),
)
.await
}
Expand All @@ -194,7 +194,7 @@ where
Some(device.metadata.application.as_str()),
Some(device.metadata.name.as_str()),
)?,
device,
Some(device),
)
.await
}
Expand Down
10 changes: 2 additions & 8 deletions src/tokens/v1/data.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
use chrono::{DateTime, TimeZone, Utc};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

fn epoch() -> DateTime<Utc> {
Utc.timestamp_millis(0)
}

#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct AccessToken {
/// The creation date of the access token
#[serde(default = "epoch")]
pub created: DateTime<Utc>,
/// The access token prefix
#[serde(default)]
pub prefix: String,
/// The access token description
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}

Expand Down
32 changes: 19 additions & 13 deletions src/util/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,30 +53,35 @@ where

/// Execute a PUT request to update an existing resource.
///
/// A payload with the updated resource must be passed.
/// A payload with the updated resource can be passed.
/// The resource must exist, otherwise `false` is returned.
///
/// The correct authentication and tracing headers will be added to the request.
async fn update<A>(&self, url: Url, payload: A) -> Result<bool, ClientError<reqwest::Error>>
async fn update<A>(
&self,
url: Url,
payload: Option<A>,
) -> Result<bool, ClientError<reqwest::Error>>
where
Self: std::marker::Send,
A: Serialize + Send + Sync,
{
let req = self
.client()
.put(url)
.json(&payload)
.propagate_current_context()
.inject_token(self.token_provider())
.await?;
let req = if let Some(p) = payload {
self.client().post(url).json(&p)
} else {
self.client().post(url)
}
.propagate_current_context()
.inject_token(self.token_provider())
.await?;

Self::update_response(req.send().await?).await
}

async fn update_response(response: Response) -> Result<bool, ClientError<reqwest::Error>> {
log::debug!("Eval update response: {:#?}", response);
match response.status() {
StatusCode::OK | StatusCode::NO_CONTENT => Ok(true),
StatusCode::OK | StatusCode::NO_CONTENT | StatusCode::ACCEPTED => Ok(true),
StatusCode::NOT_FOUND => Ok(false),
_ => Self::default_response(response).await,
}
Expand Down Expand Up @@ -112,21 +117,22 @@ where
/// Execute a POST request to create a resource.
///
/// The correct authentication and tracing headers will be added to the request.
async fn create<A, T>(
async fn create<P, T>(
&self,
url: Url,
payload: Option<A>,
payload: Option<P>,
) -> Result<Option<T>, ClientError<reqwest::Error>>
where
Self: std::marker::Send,
A: Serialize + Send + Sync,
P: Serialize + Send + Sync,
T: DeserializeOwned,
{
let req = if let Some(p) = payload {
self.client().post(url).json(&p)
} else {
self.client().post(url)
}
.propagate_current_context()
.inject_token(self.token_provider())
.await?;

Expand Down

0 comments on commit 1f6565b

Please sign in to comment.