Compare commits

..

6 commits
main ... docker

Author SHA1 Message Date
Frank Villaro-Dixon 55004fbd61 add tag 0.0.2
All checks were successful
ci / docker-build (push) Successful in 4m1s
Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
2024-05-24 22:57:18 +02:00
Frank Villaro-Dixon 589fe948cf test2
Some checks failed
ci / docker-build (push) Has been cancelled
Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
2024-05-24 22:54:50 +02:00
Frank Villaro-Dixon ef0b2e34c1 fix canonical name
Some checks failed
ci / docker-build (push) Failing after 19s
Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
2024-05-24 22:52:16 +02:00
Frank Villaro-Dixon 41db3fc3e4 hello
Some checks failed
ci / docker-build (push) Failing after 20s
Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
2024-05-24 22:50:20 +02:00
Frank Villaro-Dixon 188ec25586 push tags
Some checks failed
ci / docker-build (push) Failing after 19s
Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
2024-05-24 22:34:06 +02:00
Frank Villaro-Dixon 8138911e25 build dockerfile
All checks were successful
ci / docker-build (push) Successful in 4m26s
Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
2024-05-24 22:19:35 +02:00
7 changed files with 35 additions and 309 deletions

View file

@ -3,7 +3,7 @@ name: ci
on: on:
push: push:
branches: branches:
- main - docker
tags: tags:
- '*' - '*'
@ -41,7 +41,7 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Extract metadata (tags, labels) for Dockerhub - name: Extract metadata (tags, labels) for Forge hub
id: metadockerhub id: metadockerhub
uses: https://github.com/docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 uses: https://github.com/docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with: with:

98
Cargo.lock generated
View file

@ -236,55 +236,6 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "anstream"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "anstyle-parse"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@ -398,12 +349,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "colorchoice"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -506,29 +451,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "env_filter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -764,12 +686,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.3.1" version = "1.3.1"
@ -852,12 +768,6 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.11"
@ -1060,9 +970,7 @@ name = "opentsdb-auth-proxy"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"env_logger",
"glob-match", "glob-match",
"log",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
@ -1740,12 +1648,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View file

@ -5,9 +5,7 @@ edition = "2021"
[dependencies] [dependencies]
actix-web = "4.6.0" actix-web = "4.6.0"
env_logger = "0.11.3"
glob-match = "0.2.1" glob-match = "0.2.1"
log = "0.4.21"
reqwest = "0.12.4" reqwest = "0.12.4"
serde = { version = "1.0.202", features = ["serde_derive"] } serde = { version = "1.0.202", features = ["serde_derive"] }
serde_json = "1.0.117" serde_json = "1.0.117"

View file

@ -1,11 +1,8 @@
# OpenTSDB Auth Proxy # OpenTSDB Auth Proxy
This is a simple read/write proxy for the [OpenTSDB](https://github.com/OpenTSDB/opentsdb) This is a simple proxy for the [OpenTSDB](https://github.com/OpenTSDB/opentsdb)
time series database. It handles authentication and authorization. time series database. It handles authentication and authorization.
**Warning**: This proxy is currently half baked. It works for my needs though.
if you need more features, don't hesitate to make a PR ;-)
This proxy can be publicly exposed. When sending data to opentsdb, set the endpoint This proxy can be publicly exposed. When sending data to opentsdb, set the endpoint
to this proxy instead. Each client will send the data alongside an authentication to this proxy instead. Each client will send the data alongside an authentication
token. token.
@ -13,40 +10,16 @@ token.
If the token matches the host and the metric matches the list of allowed metrics, If the token matches the host and the metric matches the list of allowed metrics,
then the request is forwarded to the opentsdb server. then the request is forwarded to the opentsdb server.
Supported routes:
- POST `/put`
- GET `/query`
Supported authentications:
- sha256
Supported authorizations:
- `metrics`: read & write
- `read_metrics`
- `write_metrics`
## Container images
You can find the images on:
- the dockerhub as [`frankkkkk/opentsdb-auth-proxy`](https://hub.docker.com/r/frankkkkk/opentsdb-auth-proxy)
- my hub: [`forge.k3s.fr/frank/opentsdb-auth-proxy`](https://forge.k3s.fr/frank/-/packages/container/opentsdb-auth-proxy/main)
## Configuration ## Configuration
Take a look at the provided [sample configuration](./example-cfg.yml) Take a look at the provided [sample configuration](./example-cfg.yml)
### Authentication tokens ### Authentication tokens
Right now, one authentication token is supported: Right now, two authentication tokens are supported:
- sha256 - sha256
- plain (not recommended)
#### Sha256 #### Sha256

View file

@ -1,16 +1,11 @@
clients: clients:
# Exclusive writers
- name: pyranometer - name: pyranometer
write_metrics: metrics:
- irradiance - irradiance
- temperature - temperature
read_metrics:
- weather.*
auth: auth:
type: sha256 type: sha256
hash: ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb # a hash: ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb # a
# Reader and writer on the same metric
- name: tgbt - name: tgbt
metrics: metrics:
- testproxy.* - testproxy.*
@ -18,25 +13,9 @@ clients:
type: sha256 type: sha256
hash: ac790471b321143716e7773d589af923236ebdd435ba17c671df3558becc5154 # 7a5becc5b5bb581522fd0bb8891bb99a70275620 hash: ac790471b321143716e7773d589af923236ebdd435ba17c671df3558becc5154 # 7a5becc5b5bb581522fd0bb8891bb99a70275620
# Reader and writer on different metrics
- name: tgbt
write_metrics:
- barfoo
read_metrics:
- foobar.*
auth:
# ...
# Reader only
- name: consumer
read_metrics:
- irradiance
auth:
# ...
config: config:
opentsdb: opentsdb:
url: http://192.168.30.2/api/ url: http://opentsdb/api
server: server:
port: 8080 port: 8080

View file

@ -15,13 +15,8 @@ pub struct Config {
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct Client { pub struct Client {
pub name: String, pub name: String,
#[serde(default)]
pub metrics: Vec<String>, pub metrics: Vec<String>,
#[serde(default)] pub auth: Auth,
pub read_metrics: Vec<String>,
#[serde(default)]
pub write_metrics: Vec<String>,
pub auth: Option<Auth>,
} }
impl Client { impl Client {
@ -31,24 +26,6 @@ impl Client {
return true; return true;
} }
} }
for m in &self.write_metrics {
if glob_match(m, metric) {
return true;
}
}
false
}
pub fn can_read(&self, metric: &str) -> bool {
for m in &self.metrics {
if glob_match(m, metric) {
return true;
}
}
for m in &self.read_metrics {
if glob_match(m, metric) {
return true;
}
}
false false
} }
} }
@ -67,7 +44,7 @@ impl Auth {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(token); hasher.update(token);
let result = hasher.finalize(); let result = hasher.finalize();
format!("{:x}", result) == self.hash return format!("{:x}", result) == self.hash;
} }
_ => false, _ => false,
} }
@ -104,7 +81,7 @@ fn default_opentsdb_url() -> String {
pub fn load_config_file(filename: &str) -> Config { pub fn load_config_file(filename: &str) -> Config {
let yaml_content = fs::read_to_string(filename) let yaml_content = fs::read_to_string(filename)
.unwrap_or_else(|_| panic!("Unable to read config file `{}`", filename)); .expect(format!("Unable to read config file {}", filename).as_str());
let config: Config = serde_yaml::from_str(&yaml_content).expect("Unable to parse YAML"); let config: Config = serde_yaml::from_str(&yaml_content).expect("Unable to parse YAML");
config config
} }
@ -112,5 +89,5 @@ pub fn load_config_file(filename: &str) -> Config {
pub fn try_authenticate_client<'a>(clients: &'a [Client], token: &str) -> Option<&'a Client> { pub fn try_authenticate_client<'a>(clients: &'a [Client], token: &str) -> Option<&'a Client> {
clients clients
.iter() .iter()
.find(|client| client.auth.is_some() && client.auth.as_ref().unwrap().is_valid_token(token)) .find(|client| client.auth.is_valid_token(token))
} }

View file

@ -1,7 +1,6 @@
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use actix_web::{web, App, HttpResponse, HttpServer, Responder}; use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use log::{debug, error, info};
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@ -16,95 +15,48 @@ struct ClientData {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct QPutParams { struct QSParams {
token: String, token: String,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
struct OtsdbPutData { struct OtsdbData {
metric: String, metric: String,
value: StringIntFloat, value: String,
timestamp: i64, timestamp: f64,
tags: HashMap<String, StringIntFloat>, tags: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct QQueryParams {
#[serde(default)]
token: String,
#[serde(flatten)]
q: OpentsdbQuery,
}
#[derive(Debug, Deserialize, Serialize)]
struct OpentsdbQuery {
start: StringInt,
end: Option<StringInt>,
m: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
enum StringIntFloat {
String(String),
Integer(i64),
Float(f64),
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
enum StringInt {
String(String),
Integer(i64),
} }
const CONFIG_FILE: &str = "config.yaml"; const CONFIG_FILE: &str = "config.yaml";
fn get_metric(m: &String) -> String {
let mut metric = m.clone();
let pts: Vec<&str> = metric.split(":").collect();
pts[1].to_string()
}
#[actix_web::post("/put")] #[actix_web::post("/put")]
async fn put_post( async fn put_post(
shared: web::Data<ClientData>, shared: web::Data<ClientData>,
qs: web::Query<QPutParams>, qs: web::Query<QSParams>,
body: web::Json<OtsdbPutData>, body: web::Json<OtsdbData>,
) -> impl Responder { ) -> impl Responder {
println!("Body: {:?}", body);
let authenticated_client = config::try_authenticate_client(&shared.cfg.clients, &qs.token); let authenticated_client = config::try_authenticate_client(&shared.cfg.clients, &qs.token);
if authenticated_client.is_none() { if authenticated_client.is_none() {
let emsg = format!( return HttpResponse::Unauthorized().body("Unauthorized. Please specify a valid token.");
"Unauthorized. Unknown token: {}. Please specify a valid tokne.",
qs.token
);
error!("{}", emsg);
return HttpResponse::Unauthorized().body(emsg);
} }
let client = authenticated_client.unwrap(); let client = authenticated_client.unwrap();
if !client.can_write(&body.metric) { if !client.can_write(&body.metric) {
let emsg = format!( return HttpResponse::Forbidden().body(format!(
"Not allowed to write metric `{}`. Allowed metrics: {} and {}", "Not allowed to write metric `{}`. Allowed metrics: {}",
body.metric, body.metric,
client.metrics.join(", "), client.metrics.join(", ")
client.write_metrics.join(", ") // XXX make it nicer ));
);
error!("{}", emsg);
return HttpResponse::Forbidden().body(emsg);
} }
println!("Client: {:?}", client);
let post_url = format!("{}put", shared.cfg.config.opentsdb.url); let post_url = format!("{}put", shared.cfg.config.opentsdb.url);
let otsdb_body = serde_json::to_string(&body).unwrap(); let otsdb_body = serde_json::to_string(&body).unwrap();
info!( println!("POST URL: {}", post_url);
"{} sent metric {}={:?}",
client.name, body.metric, body.value
);
debug!("POST {} with body: {}", post_url, otsdb_body);
let response = shared let response = shared
.web_client .web_client
@ -118,67 +70,11 @@ async fn put_post(
let status = resp.status(); let status = resp.status();
let body = resp.text().await.unwrap_or_else(|_| "".to_string()); let body = resp.text().await.unwrap_or_else(|_| "".to_string());
debug!("OpenTSDB response {}: {}", status, body);
let sstatus = let sstatus =
StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
HttpResponse::Ok().status(sstatus).body(body) HttpResponse::Ok().status(sstatus).body(body)
} }
Err(err) => { Err(err) => HttpResponse::InternalServerError().body(format!("Error: {}", err)),
error!("OpenTSDB error: {}", err);
HttpResponse::InternalServerError().body(format!("Proxy error: {}", err))
}
}
}
#[actix_web::get("/query")]
async fn query_get(shared: web::Data<ClientData>, qs: web::Query<QQueryParams>) -> impl Responder {
let authenticated_client = config::try_authenticate_client(&shared.cfg.clients, &qs.token);
if authenticated_client.is_none() {
let emsg = format!(
"Unauthorized. Unknown token: {}. Please specify a valid tokne.",
qs.token
);
error!("{}", emsg);
return HttpResponse::Unauthorized().body(emsg);
}
let client = authenticated_client.unwrap();
println!("Query get: {:?}", qs);
let metric = get_metric(&qs.q.m);
if !client.can_read(&metric) {
let emsg = format!("Not allowed to read metric `{}`", metric);
error!("{}", emsg);
return HttpResponse::Forbidden().body(emsg);
}
let get_url = format!("{}query", shared.cfg.config.opentsdb.url);
//let otsdb_body = serde_json::to_string(&body).unwrap();
//let query_string
info!("{} get metric {}", client.name, metric);
debug!("GET {} with qs: {:?}", get_url, qs.q);
let response = shared.web_client.get(get_url).query(&qs.q).send().await;
match response {
Ok(resp) => {
let status = resp.status();
let body = resp.text().await.unwrap_or_else(|_| "".to_string());
debug!("OpenTSDB response {}: {}", status, body);
let sstatus =
StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
HttpResponse::Ok().status(sstatus).body(body)
}
Err(err) => {
error!("OpenTSDB error: {}", err);
HttpResponse::InternalServerError().body(format!("Proxy error: {}", err))
}
} }
} }
@ -187,23 +83,24 @@ async fn main() -> std::io::Result<()> {
let cfg_file = env::var("CONFIG_FILE").unwrap_or(CONFIG_FILE.to_string()); let cfg_file = env::var("CONFIG_FILE").unwrap_or(CONFIG_FILE.to_string());
let cfg = config::load_config_file(&cfg_file); let cfg = config::load_config_file(&cfg_file);
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); println!("Config: {:?}", cfg);
println!("Loaded config: {:#?}", cfg);
let server_port = cfg.config.server.port.clone(); let server_port = cfg.config.server.port.clone();
let web_client = Client::new(); let web_client = Client::new();
let shared = ClientData { web_client, cfg }; let shared = ClientData {
web_client: web_client,
cfg: cfg,
};
let client_data = web::Data::new(shared); let client_data = web::Data::new(shared);
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.app_data(client_data.clone()) .app_data(client_data.clone()) //.client_data.clone())
.app_data(web::JsonConfig::default().content_type_required(false)) .app_data(web::JsonConfig::default().content_type_required(false))
.wrap(Logger::new("%r %s")) // k8s already logs timestamp .wrap(Logger::default())
.service(put_post) .service(put_post)
.service(query_get) //.route("/put", web::post().to(put_post))
}) })
.bind(format!("[::]:{}", server_port))? .bind(format!("[::]:{}", server_port))?
.run() .run()