feat: initial commit
This commit is contained in:
commit
4bb3305e84
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1739
Cargo.lock
generated
Normal file
1739
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "pdns-oidc-tsigkey-manager"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
config = "0.13.1"
|
||||
axum = "0.6.18"
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
serde_with = "3.0.0"
|
||||
serde_json = "1.0.99"
|
||||
reqwest = { version = "0.11.18", features = ["json"] }
|
||||
url = { version = "2.4.0", features = ["serde"] }
|
||||
tokio = { version = "1.28.2", features = ["full"] }
|
63
src/api.rs
Normal file
63
src/api.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::{State},
|
||||
};
|
||||
use axum::Json;
|
||||
use serde::{Deserialize,Serialize};
|
||||
use crate::*;
|
||||
use crate::PowerDnsOidcTsigkeyError;
|
||||
|
||||
pub async fn list_keys(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<TsigKey>>, PowerDnsOidcTsigkeyError> {
|
||||
let req = state.http_client.get::<String>((config_cell.get().unwrap().powerdns.url.to_string() + "/servers/localhost/tsigkeys").into())
|
||||
.header("X-API-Key", config_cell.get().unwrap().powerdns.api_token.clone());
|
||||
let response = req
|
||||
.send()
|
||||
.await?;
|
||||
match response.status().is_success() {
|
||||
true => {
|
||||
let powerdns_tsig_keys: Vec<PowerDnsTsigKey> = response.json().await?;
|
||||
Ok(Json(powerdns_tsig_keys.into_iter().map(|key| key.into()).collect()))
|
||||
},
|
||||
false => Err(PowerDnsOidcTsigkeyError::from_response(response).await)
|
||||
}
|
||||
}
|
||||
//pub async fn list_key(Path(key_id): Path<String>) -> Json<TsigKey> {}
|
||||
//pub async fn create_key() -> Json<TsigKey> {}
|
||||
//pub async fn delete_key(Path(key_id): Path<String>) {}
|
||||
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct TsigKeyList {
|
||||
pub keys: Vec<TsigKey>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct TsigKey {
|
||||
pub name: String,
|
||||
pub algorithm: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct PowerDnsTsigKey {
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
pub algorithm: String,
|
||||
pub key: String,
|
||||
#[serde(rename = "type")]
|
||||
pub object_type: String,
|
||||
}
|
||||
|
||||
impl From<PowerDnsTsigKey> for TsigKey {
|
||||
fn from(tsigkey: PowerDnsTsigKey) -> Self {
|
||||
Self {
|
||||
name: tsigkey.name,
|
||||
algorithm: tsigkey.algorithm,
|
||||
key: Some(tsigkey.key).filter(|s| !s.is_empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
159
src/main.rs
Normal file
159
src/main.rs
Normal file
@ -0,0 +1,159 @@
|
||||
mod api;
|
||||
pub(crate) mod settings;
|
||||
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::Arc,
|
||||
fmt::Display,
|
||||
};
|
||||
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use axum::{
|
||||
routing::{get},
|
||||
Router,
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
|
||||
use crate::settings::PowerDnsOidcTsigkeyConfig;
|
||||
use reqwest::Response as ReqwestResponse;
|
||||
use reqwest::Client;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
//use serde_with::{serde_as, DisplayFromStr};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PowerDnsOidcTsigkeyError {
|
||||
ApiResponse(String),
|
||||
PowerDnsApi(StatusCode, PowerDnsApiError),
|
||||
Reqwest(reqwest::Error),
|
||||
Config(config::ConfigError),
|
||||
Serde(serde_json::Error),
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl PowerDnsOidcTsigkeyError {
|
||||
pub async fn from_response(res: ReqwestResponse) -> Self {
|
||||
match res
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.map(reqwest::header::HeaderValue::to_str)
|
||||
{
|
||||
Some(Ok(content_type)) if content_type.starts_with("application/json") => {
|
||||
let status_code = res.status();
|
||||
match res.json().await.map_err(reqwest::Error::from) {
|
||||
Ok(json_result) => Self::PowerDnsApi(status_code, json_result),
|
||||
Err(err) => Self::Reqwest(err),
|
||||
}
|
||||
}
|
||||
_ => match res.text().await {
|
||||
Ok(text) => Self::ApiResponse(text),
|
||||
Err(err) => Self::Reqwest(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for PowerDnsOidcTsigkeyError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let sc = match self {
|
||||
Self::PowerDnsApi(sc, _) => sc,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(sc, self.to_string()).into_response()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PowerDnsOidcTsigkeyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PowerDnsOidcTsigkeyError::ApiResponse(err) => f.write_fmt(format_args!("API Error: {:#?}", err)),
|
||||
PowerDnsOidcTsigkeyError::PowerDnsApi(sc, err) => f.write_fmt(format_args!("API Error {}: {:#?}", sc, err)),
|
||||
PowerDnsOidcTsigkeyError::Reqwest(err) => f.write_fmt(format_args!("Reqwest Error: {:#?}", err)),
|
||||
PowerDnsOidcTsigkeyError::Config(err) => f.write_fmt(format_args!("Config Error: {:#?}", err)),
|
||||
PowerDnsOidcTsigkeyError::Serde(err) => f.write_fmt(format_args!("JSON Error: {:#?}", err)),
|
||||
PowerDnsOidcTsigkeyError::Io(err) => f.write_fmt(format_args!("IO Error: {:#?}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PowerDnsOidcTsigkeyError {}
|
||||
|
||||
impl From<reqwest::Error> for PowerDnsOidcTsigkeyError {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Self::Reqwest(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<config::ConfigError> for PowerDnsOidcTsigkeyError {
|
||||
fn from(e: config::ConfigError) -> Self {
|
||||
Self::Config(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for PowerDnsOidcTsigkeyError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::Serde(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for PowerDnsOidcTsigkeyError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Self::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PowerDnsApiError {
|
||||
pub error: String,
|
||||
pub errors: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub(crate) type PowerDnsOidcTsigkeyResult<T> = Result<T, PowerDnsOidcTsigkeyError>;
|
||||
|
||||
async fn parse_json<Out: DeserializeOwned>(res: ReqwestResponse) -> PowerDnsOidcTsigkeyResult<Out> {
|
||||
match res.status().is_success() {
|
||||
true => Ok(res.json().await?),
|
||||
false => Err(PowerDnsOidcTsigkeyError::from_response(res).await),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppState {
|
||||
http_client: Client,
|
||||
}
|
||||
|
||||
static config_cell: OnceCell<PowerDnsOidcTsigkeyConfig> = OnceCell::const_new();
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
match settings::PowerDnsOidcTsigkeyConfig::load("config.yaml") {
|
||||
Ok(config) => {
|
||||
config_cell.set(config).unwrap();
|
||||
run().await;
|
||||
},
|
||||
Err(e) => println!("Failed to load config.yaml: {:?}", e),
|
||||
};
|
||||
}
|
||||
|
||||
async fn run() {
|
||||
let addr: SocketAddr = (config_cell.get().unwrap().server.bind_address, config_cell.get().unwrap().server.port).into();
|
||||
let state = AppState { http_client: reqwest::Client::new() };
|
||||
// let router = create_router(state);
|
||||
let router = Router::new()
|
||||
.route("/api/v1/tsigkeys", get(api::list_keys))
|
||||
// .route("/api/v1/tsigkeys/create", post(api::create_key))
|
||||
// .route("/api/v1/tsigkeys/:keyid",
|
||||
// put(api::create_key).delete(api::delete_key).get(api::list_key))
|
||||
.with_state(Arc::new(state));
|
||||
axum::Server::bind(&addr)
|
||||
.serve(router.into_make_service())
|
||||
.await.unwrap();
|
||||
}
|
||||
|
||||
//fn create_router(state: AppState) -> Result<Router, Box<Error>> {
|
||||
// Ok(router)
|
||||
//}
|
69
src/settings.rs
Normal file
69
src/settings.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use url::Url;
|
||||
use serde::{Deserialize};
|
||||
use config::{Config, ConfigError, Environment, File};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PowerDnsOidcTsigkeyConfig {
|
||||
/// OIDC Provider
|
||||
pub oidc: OidcConfig,
|
||||
/// PowerDNS config
|
||||
pub powerdns: PowerDnsConfig,
|
||||
/// Logging config
|
||||
pub log: LogConfig,
|
||||
/// HTTP(s) config
|
||||
pub server: ServerConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PowerDnsConfig {
|
||||
/// URL where PowerDNS API can be reached
|
||||
pub url: Url,
|
||||
/// Api Token to use for authentication
|
||||
pub api_token: String,
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LogConfig {
|
||||
/// The log level
|
||||
pub level: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
/// IpAddress to listen on
|
||||
pub bind_address: IpAddr,
|
||||
/// Port to listen on
|
||||
pub port: u16,
|
||||
/// TLS config
|
||||
pub tls: ServerTlsConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ServerTlsConfig {
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct OidcConfig {
|
||||
pub issuer: Url,
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub scopes: Vec<String>,
|
||||
pub username_claim: String
|
||||
}
|
||||
|
||||
impl PowerDnsOidcTsigkeyConfig {
|
||||
pub fn load(filename: &str) -> Result<Self, ConfigError> {
|
||||
Config::builder()
|
||||
.add_source(File::with_name(filename))
|
||||
.add_source(Environment::with_prefix("POTK").separator("_"))
|
||||
.set_default("log.level", "INFO")?
|
||||
.set_default("server.bind_address", "::")?
|
||||
.set_default("server.port", 8080)?
|
||||
.build()?
|
||||
.try_deserialize()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user