feat: initial commit

This commit is contained in:
transcaffeine 2023-06-28 17:40:58 +02:00
commit 4bb3305e84
Signed by: transcaffeine
GPG Key ID: 03624C433676E465
6 changed files with 2047 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1739
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View 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
View 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
View 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
View 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()
}
}