Initial commit
This commit is contained in:
commit
85f35c7daa
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
.env
|
77
.pre-commit-config.yaml
Normal file
77
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
fail_fast: true
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
args:
|
||||||
|
- --allow-multiple-documents
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-added-large-files
|
||||||
|
- id: check-merge-conflict
|
||||||
|
|
||||||
|
- repo: https://github.com/crate-ci/typos
|
||||||
|
rev: v1.31.1
|
||||||
|
hooks:
|
||||||
|
- id: typos
|
||||||
|
args: ["--force-exclude"]
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: fmt
|
||||||
|
name: fmt
|
||||||
|
description: Format files with cargo fmt.
|
||||||
|
entry: cargo +nightly fmt
|
||||||
|
language: system
|
||||||
|
types: [rust]
|
||||||
|
args: ["--", "--check"]
|
||||||
|
# For some reason some formatting is different depending on how you invoke?
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
|
- id: clippy
|
||||||
|
name: clippy
|
||||||
|
description: Lint rust sources
|
||||||
|
entry: cargo clippy
|
||||||
|
language: system
|
||||||
|
args: ["--", "-D", "warnings"]
|
||||||
|
types: [file]
|
||||||
|
files: (\.rs|Cargo.lock)$
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
|
- id: audit
|
||||||
|
name: audit
|
||||||
|
description: Audit packages
|
||||||
|
entry: cargo audit
|
||||||
|
args: ["--deny", "warnings"]
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
verbose: true
|
||||||
|
always_run: true
|
||||||
|
|
||||||
|
- id: udeps
|
||||||
|
name: unused
|
||||||
|
description: Check for unused crates
|
||||||
|
entry: cargo udeps
|
||||||
|
args: ["--workspace"]
|
||||||
|
language: system
|
||||||
|
types: [file]
|
||||||
|
files: (\.rs|Cargo.lock)$
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
|
- id: test
|
||||||
|
name: test
|
||||||
|
description: Rust test
|
||||||
|
entry: cargo test
|
||||||
|
language: system
|
||||||
|
args: ["--workspace"]
|
||||||
|
types: [file]
|
||||||
|
files: (\.rs|Cargo.lock)$
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
|
- repo: https://github.com/hadolint/hadolint
|
||||||
|
rev: v2.12.0
|
||||||
|
hooks:
|
||||||
|
- id: hadolint
|
2
.rustfmt.toml
Normal file
2
.rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
imports_granularity = "Module"
|
||||||
|
group_imports = "StdExternalCrate"
|
2119
Cargo.lock
generated
Normal file
2119
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "authelia-controller"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
default-run = "authelia-controller"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre = "0.6.3"
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
futures-util = "0.3.31"
|
||||||
|
k8s-openapi = { version = "0.24.0", features = ["v1_31"] }
|
||||||
|
kube = { version = "0.99.0", features = ["derive", "runtime"] }
|
||||||
|
schemars = "0.8.22"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
tokio = { version = "1.44.2", features = ["full"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
|
11
src/bin/crdgen.rs
Normal file
11
src/bin/crdgen.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use kube::CustomResourceExt;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let resources =
|
||||||
|
[
|
||||||
|
serde_yaml::to_string(&authelia_controller::resources::AccessControlRule::crd())
|
||||||
|
.unwrap(),
|
||||||
|
]
|
||||||
|
.join("---\n");
|
||||||
|
print!("{resources}")
|
||||||
|
}
|
33
src/context.rs
Normal file
33
src/context.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
use kube::runtime::events::{Recorder, Reporter};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Context {
|
||||||
|
pub client: kube::Client,
|
||||||
|
pub controller_name: String,
|
||||||
|
pub namespace: String,
|
||||||
|
pub deployment_name: String,
|
||||||
|
pub secret_name: String,
|
||||||
|
pub recorder: Recorder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn new(
|
||||||
|
client: kube::Client,
|
||||||
|
controller_name: &str,
|
||||||
|
namespace: impl Into<String>,
|
||||||
|
deployment_name: impl Into<String>,
|
||||||
|
secret_name: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
let reporter: Reporter = controller_name.into();
|
||||||
|
let recorder = Recorder::new(client.clone(), reporter);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
controller_name: controller_name.into(),
|
||||||
|
namespace: namespace.into(),
|
||||||
|
deployment_name: deployment_name.into(),
|
||||||
|
secret_name: secret_name.into(),
|
||||||
|
recorder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod context;
|
||||||
|
pub mod resources;
|
70
src/main.rs
Normal file
70
src/main.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use authelia_controller::context::Context;
|
||||||
|
use authelia_controller::resources::AccessControlRule;
|
||||||
|
use dotenvy::dotenv;
|
||||||
|
use futures_util::{StreamExt as _, TryStreamExt as _};
|
||||||
|
use kube::runtime::reflector::{self};
|
||||||
|
use kube::runtime::{WatchStreamExt, watcher};
|
||||||
|
use kube::{Api, Client};
|
||||||
|
use tracing::{error, info};
|
||||||
|
use tracing_subscriber::layer::SubscriberExt as _;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt as _;
|
||||||
|
use tracing_subscriber::{EnvFilter, Registry};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> color_eyre::Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
dotenv().ok();
|
||||||
|
|
||||||
|
let env_filter = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;
|
||||||
|
if std::env::var("CARGO").is_ok() {
|
||||||
|
let logger = tracing_subscriber::fmt::layer().compact();
|
||||||
|
Registry::default().with(logger).with(env_filter).init();
|
||||||
|
} else {
|
||||||
|
let logger = tracing_subscriber::fmt::layer().json();
|
||||||
|
Registry::default().with(logger).with(env_filter).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
let namespace = std::env::var("AUTHELIA_NAMESPACE").unwrap_or("authelia".into());
|
||||||
|
let deployment = std::env::var("AUTHELIA_DEPLOYMENT").unwrap_or("authelia".into());
|
||||||
|
let secret = std::env::var("AUTHELIA_SECRET").unwrap_or("authelia-acl".into());
|
||||||
|
|
||||||
|
info!("Starting");
|
||||||
|
|
||||||
|
let client = Client::try_default().await?;
|
||||||
|
let access_control_rules = Api::<AccessControlRule>::all(client.clone());
|
||||||
|
|
||||||
|
let (reader, writer) = reflector::store();
|
||||||
|
|
||||||
|
let wc = watcher::Config::default().any_semantic();
|
||||||
|
let mut stream = watcher(access_control_rules, wc)
|
||||||
|
.default_backoff()
|
||||||
|
.reflect(writer)
|
||||||
|
.applied_objects()
|
||||||
|
.boxed();
|
||||||
|
|
||||||
|
let context = Arc::new(Context::new(
|
||||||
|
client,
|
||||||
|
"authelia.huizinga.dev",
|
||||||
|
namespace,
|
||||||
|
deployment,
|
||||||
|
secret,
|
||||||
|
));
|
||||||
|
|
||||||
|
let interval = 15;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
reader.wait_until_ready().await.unwrap();
|
||||||
|
loop {
|
||||||
|
if let Err(err) = AccessControlRule::update_acl(reader.state(), context.clone()).await {
|
||||||
|
error!("Failed to update: {err}");
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(interval)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while stream.try_next().await?.is_some() {}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
128
src/resources/access_control_rule.rs
Normal file
128
src/resources/access_control_rule.rs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use k8s_openapi::api::apps::v1::Deployment;
|
||||||
|
use k8s_openapi::api::core::v1::Secret;
|
||||||
|
use kube::api::{ObjectMeta, Patch, PatchParams};
|
||||||
|
use kube::{Api, CustomResource, ResourceExt};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
|
use crate::context::Context;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, Hash)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum AccessPolicy {
|
||||||
|
Deny,
|
||||||
|
Bypass,
|
||||||
|
OneFactor,
|
||||||
|
TwoFactor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, Hash)]
|
||||||
|
#[kube(
|
||||||
|
kind = "AccessControlRule",
|
||||||
|
group = "authelia.huizinga.dev",
|
||||||
|
version = "v1"
|
||||||
|
)]
|
||||||
|
#[kube(
|
||||||
|
shortname = "acl",
|
||||||
|
doc = "Custom resource for managing authelia access rules"
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AccessControlRuleSpec {
|
||||||
|
domain: String,
|
||||||
|
policy: AccessPolicy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Hash)]
|
||||||
|
struct AccessControl {
|
||||||
|
rules: Vec<AccessControlRuleSpec>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Hash)]
|
||||||
|
struct TopLevel {
|
||||||
|
access_control: AccessControl,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccessControlRule {
|
||||||
|
pub async fn update_acl(
|
||||||
|
mut rules: Vec<Arc<AccessControlRule>>,
|
||||||
|
ctx: Arc<Context>,
|
||||||
|
) -> Result<(), kube::Error> {
|
||||||
|
debug!("Updating acl");
|
||||||
|
rules.sort_by_cached_key(|rule| rule.name_any());
|
||||||
|
|
||||||
|
let rules = rules
|
||||||
|
.iter()
|
||||||
|
.inspect(|rule| trace!(name = rule.name_any(), "Rule found"))
|
||||||
|
.map(|rule| rule.spec.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let top = TopLevel {
|
||||||
|
access_control: AccessControl { rules },
|
||||||
|
};
|
||||||
|
|
||||||
|
let contents = BTreeMap::from([(
|
||||||
|
"configuration.acl.yaml".into(),
|
||||||
|
serde_yaml::to_string(&top).expect("serializer should not fail"),
|
||||||
|
)]);
|
||||||
|
|
||||||
|
let secret = Secret {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
string_data: Some(contents),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
name = ctx.secret_name,
|
||||||
|
namespace = ctx.namespace,
|
||||||
|
"Applying secret"
|
||||||
|
);
|
||||||
|
let secrets = Api::<Secret>::namespaced(ctx.client.clone(), &ctx.namespace);
|
||||||
|
secrets
|
||||||
|
.patch(
|
||||||
|
&ctx.secret_name,
|
||||||
|
&PatchParams::apply(&ctx.controller_name),
|
||||||
|
&Patch::Apply(&secret),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
top.hash(&mut hasher);
|
||||||
|
let hash = hasher.finish();
|
||||||
|
|
||||||
|
let patch = serde_json::json!({
|
||||||
|
"spec": {
|
||||||
|
"template": {
|
||||||
|
"metadata": {
|
||||||
|
"annotations": {
|
||||||
|
"authelia.huizinga.dev/aclHash": hash.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
name = ctx.deployment_name,
|
||||||
|
namespace = ctx.namespace,
|
||||||
|
hash,
|
||||||
|
"Updating deployment hash"
|
||||||
|
);
|
||||||
|
let deployments = Api::<Deployment>::namespaced(ctx.client.clone(), &ctx.namespace);
|
||||||
|
deployments
|
||||||
|
.patch(
|
||||||
|
&ctx.deployment_name,
|
||||||
|
&PatchParams::default(),
|
||||||
|
&Patch::Strategic(&patch),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
3
src/resources/mod.rs
Normal file
3
src/resources/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod access_control_rule;
|
||||||
|
|
||||||
|
pub use self::access_control_rule::AccessControlRule;
|
7
yaml/rule-test-1.yaml
Normal file
7
yaml/rule-test-1.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
apiVersion: authelia.huizinga.dev/v1
|
||||||
|
kind: AccessControlRule
|
||||||
|
metadata:
|
||||||
|
name: test-1
|
||||||
|
spec:
|
||||||
|
domain: "test-1.domain"
|
||||||
|
policy: one_factor
|
7
yaml/rule-test-2.yaml
Normal file
7
yaml/rule-test-2.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
apiVersion: authelia.huizinga.dev/v1
|
||||||
|
kind: AccessControlRule
|
||||||
|
metadata:
|
||||||
|
name: test-2
|
||||||
|
spec:
|
||||||
|
domain: "test-2.domain"
|
||||||
|
policy: one_factor
|
7
yaml/rule-test-3.yaml
Normal file
7
yaml/rule-test-3.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
apiVersion: authelia.huizinga.dev/v1
|
||||||
|
kind: AccessControlRule
|
||||||
|
metadata:
|
||||||
|
name: test-3
|
||||||
|
spec:
|
||||||
|
domain: "test-3.domain"
|
||||||
|
policy: one_factor
|
Loading…
Reference in New Issue
Block a user