Skip to content

Commit

Permalink
feat: add patching of oauth provider
Browse files Browse the repository at this point in the history
  • Loading branch information
dsluijk committed Dec 11, 2022
1 parent 5ca383c commit 326b4ed
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 69 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,11 @@ Open an issue!
This operator changes some behavior compared to a "vanilla" installation of Authentik.
The most notable of these are, in no particulair order:

- The initial setup flow is deleted, along with the associated stage. Normally this is used in the `/if/flow/initial-setup/` page.
- The `akadmin` user is deleted, along with the `authentik Admins` group.
- A group `akOperator authentik service group` is created. **Do not delete this group**.
- A service account `ak-operator` is created. **Also do not delete this**.
- An `ak-operator-authentik__operatortoken` api token is created. You get it now, **don't delete this**.
- An hardcoded bootstrap token is added, but removed soon after.
- An hardcoded bootstrap token is added, but removed soon after initial start.

## Development

Expand Down
25 changes: 2 additions & 23 deletions src/akapi/provider/create_oauth.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
use async_trait::async_trait;
use reqwest::StatusCode;
use serde::Serialize;
use thiserror::Error;

use crate::{
akapi::{types::OAuthProvider, AkApiRoute, AkClient},
resources::authentik_provider_oauth::crd,
};
use crate::akapi::{types::OAuthProvider, AkApiRoute, AkClient};

pub struct CreateOAuthProvider;

#[async_trait]
impl AkApiRoute for CreateOAuthProvider {
type Body = CreateOAuthProviderBody;
type Body = OAuthProvider;
type Response = OAuthProvider;
type Error = CreateOAuthProviderError;

Expand All @@ -38,23 +34,6 @@ impl AkApiRoute for CreateOAuthProvider {
}
}

#[derive(Debug, Serialize)]
pub struct CreateOAuthProviderBody {
pub name: String,
pub authorization_flow: String,
pub property_mappings: Vec<String>,
pub client_type: crd::ClientType,
pub client_id: String,
pub client_secret: String,
pub access_code_validity: String,
pub token_validity: String,
pub include_claims_in_id_token: bool,
pub signing_key: Option<String>,
pub redirect_uris: String,
pub sub_mode: crd::SubjectMode,
pub issuer_mode: crd::IssuerMode,
}

#[derive(Error, Debug)]
pub enum CreateOAuthProviderError {
#[error("An unknown error occured ({0}).")]
Expand Down
2 changes: 2 additions & 0 deletions src/akapi/provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ mod create_oauth;
mod delete_oauth;
mod find;
mod find_oauth;
mod patch_oauth;

pub use create_oauth::*;
pub use delete_oauth::*;
pub use find::*;
pub use find_oauth::*;
pub use patch_oauth::*;
43 changes: 43 additions & 0 deletions src/akapi/provider/patch_oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use async_trait::async_trait;
use reqwest::StatusCode;
use thiserror::Error;

use crate::akapi::{types::OAuthProvider, AkApiRoute, AkClient};

pub struct PatchOAuthProvider;

#[async_trait]
impl AkApiRoute for PatchOAuthProvider {
type Body = OAuthProvider;
type Response = OAuthProvider;
type Error = PatchOAuthProviderError;

#[instrument]
async fn send(ak: &AkClient, body: Self::Body) -> Result<Self::Response, Self::Error> {
let res = ak
.patch(&format!("/api/v3/providers/oauth2/{}/", body.pk))
.json(&body)
.send()
.await?;

match res.status() {
StatusCode::OK => {
let body: OAuthProvider = res.json().await?;

Ok(body)
}
code => Err(Self::Error::Unknown(format!(
"Invalid status code {}",
code
))),
}
}
}

#[derive(Error, Debug)]
pub enum PatchOAuthProviderError {
#[error("An unknown error occured ({0}).")]
Unknown(String),
#[error("Failed to send HTTP request: {0}")]
ConnectionError(#[from] reqwest::Error),
}
13 changes: 7 additions & 6 deletions src/akapi/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};

use crate::resources::{
authentik_application::crd::PolicyMode,
authentik_provider_oauth::crd::{IssuerMode, SubjectMode},
authentik_provider_oauth::crd::{ClientType, IssuerMode, SubjectMode},
};

#[derive(Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -54,18 +54,19 @@ pub struct Provider {
pub component: String,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
pub struct OAuthProvider {
#[serde(skip_serializing)]
pub pk: usize,
pub name: String,
pub authorization_flow: String,
pub property_mappings: Option<Vec<String>>,
pub component: String,
pub assigned_application_name: Option<String>,
pub assigned_application_slug: Option<String>,
pub client_type: Option<String>,
pub client_type: Option<ClientType>,
pub client_id: Option<String>,
pub client_secret: Option<String>,
pub access_code_validity: Option<String>,
pub token_validity: Option<String>,
pub include_claims_in_id_token: bool,
pub signing_key: Option<String>,
pub redirect_uris: Option<String>,
pub sub_mode: Option<SubjectMode>,
Expand Down
5 changes: 2 additions & 3 deletions src/resources/authentik_application/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ pub async fn reconcile(obj: &crd::AuthentikApplication, client: Client) -> Resul
obj.name_any()
))?;

let new_app = build_application(obj.spec.clone(), &provider);
// Get the application, create or patch depending on if it exists.
match GetApplication::send(&ak, obj.spec.slug.clone()).await? {
Some(app) => {
let new_app = build_application(obj.spec.clone(), &provider);
// Compare the serialized versions of the applications.
// The non-serialized object contains values we don't care about, and can conflict.
if serde_json::to_string(&app)? != serde_json::to_string(&new_app)? {
Expand All @@ -54,8 +54,7 @@ pub async fn reconcile(obj: &crd::AuthentikApplication, client: Client) -> Resul
}
}
None => {
let body = build_application(obj.spec.clone(), &provider);
CreateApplication::send(&ak, body).await?;
CreateApplication::send(&ak, new_app).await?;
}
};

Expand Down
98 changes: 63 additions & 35 deletions src/resources/authentik_provider_oauth/provider.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::{hash_map::RandomState, HashSet};

use anyhow::{anyhow, Result};
use kube::{Client, ResourceExt};

Expand All @@ -7,9 +9,10 @@ use crate::akapi::{
flow::GetFlow,
propertymappings::{FindScopeMapping, FindScopeMappingBody},
provider::{
CreateOAuthProvider, CreateOAuthProviderBody, DeleteOAuthProvider,
DeleteOAuthProviderError, FindOAuthProvider, FindOAuthProviderBody,
CreateOAuthProvider, DeleteOAuthProvider, DeleteOAuthProviderError, FindOAuthProvider,
FindOAuthProviderBody, PatchOAuthProvider,
},
types::{Flow, OAuthProvider},
AkApiRoute, AkClient,
};

Expand All @@ -25,23 +28,6 @@ pub async fn reconcile(obj: &crd::AuthentikOAuthProvider, client: Client) -> Res
let api_key = get_valid_token(client.clone(), &ns, &instance).await?;
let ak = AkClient::new(&api_key, &instance, &ns)?;

// Check if the provider already exists.
let providers = FindOAuthProvider::send(
&ak,
FindOAuthProviderBody {
name: Some(obj.spec.name.clone()),
},
)
.await?;

match providers
.iter()
.find(|&provider| provider.name == obj.spec.name)
{
Some(_) => return Ok(()),
None => (),
}

// Get the flow.
let flow = GetFlow::send(&ak, obj.spec.flow.clone()).await?;

Expand Down Expand Up @@ -88,26 +74,33 @@ pub async fn reconcile(obj: &crd::AuthentikOAuthProvider, client: Client) -> Res
None
};

// Create the provider.
CreateOAuthProvider::send(
// Check if the provider already exists.
let providers = FindOAuthProvider::send(
&ak,
CreateOAuthProviderBody {
signing_key,
name: obj.spec.name.clone(),
authorization_flow: flow.pk,
property_mappings: scopes,
client_type: obj.spec.client_type.clone(),
client_id: obj.spec.client_id.clone(),
client_secret: obj.spec.client_secret.clone(),
include_claims_in_id_token: obj.spec.claims_in_token,
redirect_uris: obj.spec.redirect_uris.join("\n"),
access_code_validity: obj.spec.access_code_validity.clone(),
token_validity: obj.spec.token_validity.clone(),
sub_mode: obj.spec.subject_mode.clone(),
issuer_mode: obj.spec.issuer_mode.clone(),
FindOAuthProviderBody {
name: Some(obj.spec.name.clone()),
},
)
.await?;
let provider = providers
.iter()
.find(|&provider| provider.name == obj.spec.name);

let new_provider = build_provider(&obj.spec, provider, &flow, signing_key, scopes);
match provider {
Some(provider) => {
// Compare the serialized versions of the provider.
// The non-serialized object contains values we don't care about, and can conflict.
if serde_json::to_string(&provider)? != serde_json::to_string(&new_provider)? {
// There is a difference in the objects, patching it.
PatchOAuthProvider::send(&ak, new_provider).await?;
}
}
None => {
// Create the provider.
CreateOAuthProvider::send(&ak, new_provider).await?;
}
}

Ok(())
}
Expand Down Expand Up @@ -149,3 +142,38 @@ pub async fn cleanup(obj: &crd::AuthentikOAuthProvider, client: Client) -> Resul
Err(e) => Err(e.into()),
}
}

fn build_provider(
spec: &crd::AuthentikOAuthProviderSpec,
old_provider: Option<&OAuthProvider>,
flow: &Flow,
signing_key: Option<String>,
scopes: Vec<String>,
) -> OAuthProvider {
let mappings = old_provider
.and_then(|p| p.property_mappings.clone())
.filter(|mappings| {
let old_set: HashSet<String, RandomState> = HashSet::from_iter(mappings.to_owned());
let new_set: HashSet<String, RandomState> = HashSet::from_iter(scopes.clone());

old_set == new_set
})
.unwrap_or(scopes);

OAuthProvider {
pk: old_provider.map(|p| p.pk).unwrap_or(0),
signing_key,
name: spec.name.clone(),
authorization_flow: flow.pk.clone(),
property_mappings: Some(mappings),
client_type: Some(spec.client_type.clone()),
client_id: Some(spec.client_id.clone()),
client_secret: Some(spec.client_secret.clone()),
include_claims_in_id_token: spec.claims_in_token,
redirect_uris: Some(spec.redirect_uris.join("\n")),
access_code_validity: Some(spec.access_code_validity.clone()),
token_validity: Some(spec.token_validity.clone()),
sub_mode: Some(spec.subject_mode.clone()),
issuer_mode: Some(spec.issuer_mode.clone()),
}
}

0 comments on commit 326b4ed

Please sign in to comment.