use crate::Client;
use crate::error::Result;
use crate::resource::Resource;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateGroupRequest {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<Vec<i32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateGroupRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<Vec<i32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateTokenRequest {
pub user: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_used: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub write_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateTokenRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_used: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub write_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserRequest {
pub username: String,
pub password: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub first_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_staff: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_joined: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_login: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub groups: Option<Vec<i32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<Vec<i32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateUserRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub first_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_staff: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_joined: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_login: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub groups: Option<Vec<i32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<Vec<i32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateObjectPermissionRequest {
pub name: String,
pub object_types: Vec<String>,
pub actions: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constraints: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub groups: Option<Vec<i32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub users: Option<Vec<i32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateObjectPermissionRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object_types: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constraints: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub groups: Option<Vec<i32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub users: Option<Vec<i32>>,
}
pub type UserConfig = HashMap<String, Value>;
pub type Group = crate::models::Group;
pub type ObjectPermission = crate::models::ObjectPermission;
pub type Token = crate::models::Token;
pub type TokenProvision = crate::models::TokenProvision;
pub type TokenProvisionRequest = crate::models::TokenProvisionRequest;
pub type User = crate::models::User;
pub type GroupsApi = Resource<crate::models::Group>;
pub type PermissionsApi = Resource<crate::models::ObjectPermission>;
pub type TokensApi = Resource<crate::models::Token>;
pub type UsersResource = Resource<crate::models::User>;
#[derive(Clone)]
pub struct UsersApi {
client: Client,
}
impl UsersApi {
pub(crate) fn new(client: Client) -> Self {
Self { client }
}
pub async fn config(&self) -> Result<UserConfig> {
self.client.get("users/config/").await
}
pub fn groups(&self) -> GroupsApi {
Resource::new(self.client.clone(), "users/groups/")
}
pub fn permissions(&self) -> PermissionsApi {
Resource::new(self.client.clone(), "users/permissions/")
}
pub fn tokens(&self) -> TokensApi {
Resource::new(self.client.clone(), "users/tokens/")
}
pub async fn provision_token(&self, body: &TokenProvisionRequest) -> Result<TokenProvision> {
self.client.post("users/tokens/provision/", body).await
}
pub fn users(&self) -> UsersResource {
Resource::new(self.client.clone(), "users/users/")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ClientConfig;
use httpmock::{Method::POST, MockServer};
use serde_json::json;
fn test_client() -> Client {
let config = ClientConfig::new("https://netbox.example.com", "token");
Client::new(config).unwrap()
}
fn assert_path<T>(resource: Resource<T>, expected: &str)
where
T: serde::de::DeserializeOwned,
{
let paginator = resource.paginate(None).unwrap();
assert_eq!(paginator.next_url(), Some(expected));
}
fn assert_missing(value: &serde_json::Value, key: &str) {
assert!(value.get(key).is_none(), "expected {} to be omitted", key);
}
#[test]
fn users_accessors_return_expected_paths() {
let api = UsersApi::new(test_client());
assert_path(api.groups(), "users/groups/");
assert_path(api.permissions(), "users/permissions/");
assert_path(api.tokens(), "users/tokens/");
assert_path(api.users(), "users/users/");
}
#[test]
fn serialize_group_requests() {
let create = CreateGroupRequest {
name: "operators".to_string(),
description: None,
permissions: Some(vec![1, 2]),
};
let value = serde_json::to_value(&create).unwrap();
assert_eq!(value["name"], "operators");
assert_eq!(value["permissions"], json!([1, 2]));
assert_missing(&value, "description");
let update = UpdateGroupRequest {
name: None,
description: Some("Updated".to_string()),
permissions: None,
};
let value = serde_json::to_value(&update).unwrap();
assert_eq!(value["description"], "Updated");
assert_missing(&value, "name");
}
#[test]
fn serialize_token_requests() {
let create = CreateTokenRequest {
user: 5,
expires: Some("2030-01-01T00:00:00Z".to_string()),
last_used: None,
key: None,
write_enabled: Some(true),
description: None,
};
let value = serde_json::to_value(&create).unwrap();
assert_eq!(value["user"], 5);
assert_eq!(value["expires"], "2030-01-01T00:00:00Z");
assert_eq!(value["write_enabled"], true);
assert_missing(&value, "description");
let update = UpdateTokenRequest {
user: None,
expires: None,
last_used: Some("2024-01-01T00:00:00Z".to_string()),
key: Some("abcd".to_string()),
write_enabled: None,
description: Some("Updated".to_string()),
};
let value = serde_json::to_value(&update).unwrap();
assert_eq!(value["last_used"], "2024-01-01T00:00:00Z");
assert_eq!(value["key"], "abcd");
assert_eq!(value["description"], "Updated");
assert_missing(&value, "user");
}
#[test]
fn serialize_user_requests() {
let create = CreateUserRequest {
username: "alice".to_string(),
password: "secret".to_string(),
first_name: Some("Alice".to_string()),
last_name: None,
email: Some("alice@example.com".to_string()),
is_staff: Some(true),
is_active: Some(true),
date_joined: None,
last_login: None,
groups: Some(vec![1]),
permissions: None,
};
let value = serde_json::to_value(&create).unwrap();
assert_eq!(value["username"], "alice");
assert_eq!(value["password"], "secret");
assert_eq!(value["first_name"], "Alice");
assert_eq!(value["email"], "alice@example.com");
assert_eq!(value["is_staff"], true);
assert_eq!(value["groups"], json!([1]));
assert_missing(&value, "last_name");
let update = UpdateUserRequest {
username: None,
password: None,
first_name: None,
last_name: Some("Smith".to_string()),
email: None,
is_staff: None,
is_active: Some(false),
date_joined: None,
last_login: None,
groups: None,
permissions: Some(vec![9]),
};
let value = serde_json::to_value(&update).unwrap();
assert_eq!(value["last_name"], "Smith");
assert_eq!(value["is_active"], false);
assert_eq!(value["permissions"], json!([9]));
assert_missing(&value, "username");
}
#[test]
fn serialize_object_permission_requests() {
let create = CreateObjectPermissionRequest {
name: "dcim_view".to_string(),
object_types: vec!["dcim.device".to_string()],
actions: vec!["view".to_string()],
description: None,
enabled: Some(true),
constraints: Some(json!({"status": "active"})),
groups: None,
users: Some(vec![2]),
};
let value = serde_json::to_value(&create).unwrap();
assert_eq!(value["name"], "dcim_view");
assert_eq!(value["object_types"], json!(["dcim.device"]));
assert_eq!(value["actions"], json!(["view"]));
assert_eq!(value["enabled"], true);
assert_eq!(value["constraints"]["status"], "active");
assert_eq!(value["users"], json!([2]));
let update = UpdateObjectPermissionRequest {
name: None,
object_types: None,
actions: None,
description: Some("Updated".to_string()),
enabled: None,
constraints: None,
groups: Some(vec![1]),
users: None,
};
let value = serde_json::to_value(&update).unwrap();
assert_eq!(value["description"], "Updated");
assert_eq!(value["groups"], json!([1]));
assert_missing(&value, "name");
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn provision_token_uses_expected_path() {
let server = MockServer::start();
let base_url = server.base_url();
let config = ClientConfig::new(&base_url, "token").with_max_retries(0);
let client = Client::new(config).unwrap();
let api = UsersApi::new(client);
let response = json!({
"id": 1,
"url": "http://example.com/api/users/tokens/1/",
"display_url": "http://example.com/users/tokens/1/",
"display": "token",
"user": {
"id": 1,
"url": "http://example.com/api/users/users/1/",
"display": "admin",
"username": "admin"
},
"created": "2024-01-01T00:00:00Z",
"expires": null,
"last_used": "2024-01-01T00:00:00Z",
"key": "token",
"write_enabled": true,
"description": "provisioned"
});
server.mock(|when, then| {
when.method(POST).path("/api/users/tokens/provision/");
then.status(201).json_body(response);
});
let request = TokenProvisionRequest {
expires: None,
write_enabled: Some(true),
description: Some("provisioned".to_string()),
username: "admin".to_string(),
password: "secret".to_string(),
};
let token = api.provision_token(&request).await.unwrap();
assert_eq!(token.key.as_deref(), Some("token"));
}
}