Verified Commit 62f9e811 authored by Levente Polyak's avatar Levente Polyak 🚀
Browse files

wip

parent d8bebffb
This diff is collapsed.
......@@ -8,12 +8,16 @@ repository = "https://gitlab.archlinux.org/archlinux/gluebuddy"
categories = ["command-line-utilities"]
[dependencies]
keycloak = "10"
gitlab = "0.1210"
reqwest = "0.10"
tokio = { version = "0.2", features = ["full"] }
keycloak = "12.0"
gitlab = { git = "file:///home/anthraxx/Projects/anthraxx/rust/rust-gitlab", branch = "master" }
#gitlab = "0.1308"
reqwest = "0.11"
tokio = { version = "1.1", features = ["full"] }
futures = "0.3"
anyhow = "1.0"
log = "0.4"
env_logger = "0.7"
env_logger = "0.8"
structopt = "0.3"
serde = {version = "1.0", features = ["derive"]}
strum = "0.20"
strum_macros = "0.20"
//! This module defines gitlab related actions and enforcements.
//!
//! ## Features
//!
//! - ensure the integrity of the Arch Linux root group
//! - add staff members as reporter
//! - ensure nobody except devops has higher privileges
use crate::args::*;
use crate::state::State;
mod types;
use types::*;
use anyhow::{Context, Result};
use log::{debug, error, info};
use std::env;
use gitlab::api::{ApiError, Query};
use gitlab::Gitlab;
use gitlab::api::common::VisibilityLevel;
use gitlab::api::projects::Projects;
use tokio::task;
pub struct GitLabGlue {
client: Gitlab,
}
impl GitLabGlue {
pub async fn new() -> Result<GitLabGlue> {
task::spawn_blocking(move || create_client()).await?
}
pub async fn gather<'a>(&'a self, state: &mut State<'a>) -> Result<()> {
Ok(())
}
pub async fn run<'a>(&self, state: &State<'a>, action: Action) -> Result<()> {
task::spawn_blocking(move || create_repository(action)).await??;
Ok(())
}
}
pub fn create_client() -> Result<GitLabGlue> {
let token =
&env::var("GLUEBUDDY_GITLAB_TOKEN").context("Missing env var GLUEBUDDY_GITLAB_TOKEN")?;
let client = Gitlab::new("gitlab.archlinux.org", token).unwrap();
Ok(GitLabGlue { client })
}
/*
fn protect_tags(client: &Gitlab, project_id: u64) -> std::result::Result<ProtectedTag, gitlab::api::ApiError<>> {
}
*/
fn create_repository(action: Action) -> Result<()> {
let token =
&env::var("GLUEBUDDY_GITLAB_TOKEN").context("Missing env var GLUEBUDDY_GITLAB_TOKEN")?;
let client = Gitlab::new("gitlab.archlinux.org", token).unwrap();
let group_endpoint = gitlab::api::groups::subgroups::GroupSubgroups::builder()
.group("bot-test")
.build()
.unwrap();
let groups: Vec<Group> = group_endpoint.query(&client).unwrap();
for group in groups {
println!("group: {}", group.name);
// TODO: remove this
if !group.name.eq("sandbox") {
continue;
}
let group_projects_endpoint = gitlab::api::groups::projects::GroupProjects::builder()
.group(group.id)
.archived(false)
.order_by(gitlab::api::groups::projects::GroupProjectsOrderBy::Id)
.build()
.unwrap();
let projects: Vec<GroupProjects> =
gitlab::api::paged(group_projects_endpoint, gitlab::api::Pagination::All)
.query(&client)?;
for project in projects {
println!(" project: {}", project.name);
let endpoint = gitlab::api::projects::protected_tags::ProtectedTag::builder()
.project(project.id)
.name("*")
.build()
.unwrap();
let protected_tag: Result<ProtectedTag, gitlab::api::ApiError<_>> =
endpoint.query(&client);
match protected_tag {
Ok(protected_tag) => {
debug!("protected tag {} exists", protected_tag.name);
let developer_has_create_access = protected_tag
.create_access_levels
.into_iter()
.any(|access| access.access_level == 30);
debug!("has create access: {}", developer_has_create_access);
if !developer_has_create_access {
debug!(">>> improper access level, re-protecting...");
if unprotect_tags(&client, &project).is_err() {
eprintln!("Failed to unprotect tags for project {}", project.name);
}
if protect_tags(&client, &project).is_err() {
eprintln!("Failed to protect tags for project {}", project.name);
}
}
}
Err(_) => {
if protect_tags(&client, &project).is_err() {
eprintln!("Failed to protect tags for project {}", project.name);
}
}
}
/* This is just debug printing
let protected_tag: ProtectedTag =
gitlab::api::projects::protected_tags::ProtectedTag::builder()
.project(project.id)
.name("*")
.build()
.unwrap()
.query(&client)
.unwrap();
println!(" protected-tag: {}", protected_tag.name);
for access_level in protected_tag.create_access_levels {
println!(
" create access -> {} {}",
access_level.access_level_description, access_level.access_level
)
}
*/
let protected_branch: Result<ProtectedBranch, _> =
gitlab::api::projects::protected_branches::ProtectedBranch::builder()
.project(project.id)
.name("main")
.build()
.unwrap()
.query(&client);
match protected_branch {
Ok(protected_branch) => {
debug!("protection for branch {} exists", protected_branch.name);
println!(" protected-branch: {}", protected_branch.name);
for access_level in &protected_branch.push_access_levels {
println!(
" push access -> {}",
access_level.access_level_description
)
}
for access_level in &protected_branch.merge_access_levels {
println!(
" merge access -> {}",
access_level.access_level_description
)
}
let developer_has_push_access = protected_branch
.push_access_levels
.into_iter()
.any(|access| access.access_level == 30);
let developer_has_merge_access = protected_branch
.merge_access_levels
.into_iter()
.any(|access| access.access_level == 30);
debug!(
"has push access: {} has merge access: {}",
developer_has_push_access, developer_has_merge_access
);
if !developer_has_merge_access || !developer_has_push_access {
println!(">>> improper access level, re-protecting...");
if unprotect_branch(&client, &project).is_err() {
eprintln!("Failed to unprotect branch main for project {}", project.name);
}
if protect_branch(&client, &project).is_err() {
eprintln!("Failed to protect branch main for project {}", project.name);
}
}
}
Err(_) => {
if protect_branch(&client, &project).is_err() {
eprintln!("Failed to protect branch main for project {}", project.name);
}
}
}
/* just debug output printing
let protected_branch: ProtectedBranch =
gitlab::api::projects::protected_branches::ProtectedBranch::builder()
.project(project.id)
.name("main")
.build()
.unwrap()
.query(&client)
.unwrap();
println!(" protected-branch: {}", protected_branch.name);
for access_level in protected_branch.push_access_levels {
println!(
" push access -> {}",
access_level.access_level_description
)
}
for access_level in protected_branch.merge_access_levels {
println!(
" merge access -> {}",
access_level.access_level_description
)
}
*/
if project.visibility != ProjectVisibilityLevel::Public
|| project.request_access_enabled != false
{
println!(" edit project settings");
let endpoint = gitlab::api::projects::EditProject::builder()
.project(project.id)
.visibility(VisibilityLevel::Public)
.request_access_enabled(false)
.build()
.unwrap();
gitlab::api::ignore(endpoint).query(&client).unwrap();
}
}
}
Ok(())
}
fn unprotect_tags(client: &Gitlab, project: &GroupProjects) -> Result<()> {
let endpoint =
gitlab::api::projects::protected_tags::UnprotectTag::builder()
.project(project.id)
.name("*")
.build()
.unwrap();
let _: () = gitlab::api::ignore(endpoint).query(client)?;
Ok(())
}
fn protect_tags(client: &Gitlab, project: &GroupProjects) -> Result<ProtectedTag> {
debug!("protecting tag *");
let endpoint = gitlab::api::projects::protected_tags::ProtectTag::builder()
.project(project.id)
.name("*")
.create_access_level(gitlab::api::common::ProtectedAccessLevel::Developer)
.build()
.unwrap();
let result: ProtectedTag = endpoint.query(client)?;
Ok(result)
}
fn protect_branch(client: &Gitlab, project: &GroupProjects) -> Result<ProtectedBranch> {
// protect main branch
let endpoint = gitlab::api::projects::protected_branches::ProtectBranch::builder().project(project.id).name("main")
.push_access_level(gitlab::api::projects::protected_branches::ProtectedAccessLevel::Developer)
.merge_access_level(gitlab::api::projects::protected_branches::ProtectedAccessLevel::Developer)
.build().unwrap();
let result: ProtectedBranch = endpoint.query(client)?;
Ok(result)
}
fn unprotect_branch(client: &Gitlab, project: &GroupProjects) -> Result<()> {
let endpoint =
gitlab::api::projects::protected_branches::UnprotectBranch::builder()
.project(project.id)
.name("main")
.build()
.unwrap();
let _: () = gitlab::api::ignore(endpoint).query(client)?;
Ok(())
}
use structopt::StructOpt;
use structopt::clap::{AppSettings, Shell};
use structopt::StructOpt;
use std::io::stdout;
......@@ -23,8 +23,10 @@ pub enum Command {
Apply,
/// Keycloak module commands
Keycloak(Action),
/// Gitlab module commands
Gitlab(Action),
/// Generate shell completions
#[structopt(name="completions")]
#[structopt(name = "completions")]
Completions(Completions),
}
......@@ -39,7 +41,7 @@ pub enum Action {
/// Generate and show an execution plan
Plan,
/// Builds or changes infrastructure
Apply
Apply,
}
pub fn gen_completions(args: &Completions) -> Result<()> {
......
//! This module defines keycloak related actions and enforcements.
//!
//! ## Features
//!
//! - enforce multi-factor authentication for all staff members
use crate::args::*;
use reqwest::Client;
use keycloak::{KeycloakAdmin, KeycloakAdminToken};
use keycloak::types::{UserRepresentation, CredentialRepresentation};
use futures::future::try_join_all;
use anyhow::{Context, Result};
use log::{debug, info};
use std::env;
pub async fn run(action: Action) -> Result<()> {
enforce_multi_factor_authentication(action).await?;
Ok(())
}
// TODO: error handling for all unwrap shizzle
async fn enforce_multi_factor_authentication(action: Action) -> Result<()> {
let username = &env::var("GLUEBUDDY_KEYCLOAK_USERNAME").context("Missing env var GLUEBUDDY_KEYCLOAK_USERNAME")?;
let password = &env::var("GLUEBUDDY_KEYCLOAK_PASSWORD").context("Missing env var GLUEBUDDY_KEYCLOAK_PASSWORD")?;
let realm = &env::var("GLUEBUDDY_KEYCLOAK_REALM").context("Missing GLUEBUDDY_KEYCLOAK_REALM env var")?;
let url = &env::var("GLUEBUDDY_KEYCLOAK_URL").context("Missing GLUEBUDDY_KEYCLOAK_URL env var")?;
let client = Client::new();
info!("acquire API token for keycloak {} using realm {}", url, realm);
let admin_token = KeycloakAdminToken::acquire(url, username, password, &client).await?;
let admin = KeycloakAdmin::new(url, admin_token, client);
let groups_for_2fa = vec!["Arch Linux Staff"];
let groups = admin.groups_get(realm, None, None, None, None).await?;
let groups = groups.iter().filter(|group| {
groups_for_2fa.contains(&group.name.as_ref().unwrap().as_ref())
}).collect::<Vec<_>>();
let groups_members = groups.into_iter().flat_map(|group| {
let group_name = group.name.as_ref().unwrap().as_ref();
debug!("collect members of group {}", group_name);
vec![Box::pin(admin.groups_members_get(realm, group.id.as_ref().unwrap(), None, None, None))].into_iter().chain(
group.sub_groups.as_ref().unwrap().iter().map(|sub_group| {
debug!("collect members of sub group {}", sub_group.name.as_ref().unwrap());
Box::pin(admin.groups_members_get(realm, sub_group.id.as_ref().unwrap(), None, None, None))
}))
});
// TODO: remove duplicates who are in multiple groups
let f = try_join_all(groups_members).await?;
let members = f.into_iter().flatten().filter(|member| {
let username = member.username.as_ref().unwrap();
// Skip all users that already have a require action to configure TOTP
if let Some(required_actions) = &member.required_actions {
if required_actions.contains(&"CONFIGURE_TOTP".into()) {
debug!("CONFIGURE_TOTP present in required actions, skipping user {}", username);
return false;
}
}
debug!("CONFIGURE_TOTP not present in required actions, proceeding with user {}", username);
true
}).collect::<Vec<_>>();
info!("collected {} users whose credentials need to be checked", members.len());
let users_credentials = try_join_all(members.into_iter().map(|member| users_credentials_get(&admin, realm, member))).await?;
for (member, credentials) in users_credentials {
let username = member.username.as_ref().unwrap();
let credential_types = credentials.iter().map(|credential| credential.type_.as_ref().unwrap().as_ref()).collect::<Vec<_>>();
let required_actions = member.required_actions.as_ref().map(|actions| actions.into_iter().map(|s| s.as_ref()).collect::<Vec<_>>()).unwrap_or(vec![].into());
debug!("user {} configured credentials: {:?}, required_actions: {:?}", username, credential_types, required_actions);
let has_otp = credentials.into_iter().any(|credential| credential.type_.as_ref().map(|type_| type_.eq("otp")).unwrap_or(false));
if has_otp {
debug!("otp present in credentials, skipping user {}", username);
continue;
}
info!("enforce required action CONFIGURE_TOTP for user {}", username);
match action {
Action::Plan => {},
Action::Apply => {
users_required_actions_add(&admin, realm, member.into()).await?;
//admin.users_put(realm, member.id.as_ref().unwrap(), member.into());
},
}
}
Ok(())
}
async fn users_credentials_get<'a>(admin: &'a KeycloakAdmin<'a>, realm: &str, member: UserRepresentation<'a>) -> Result<(UserRepresentation<'a>, Vec<CredentialRepresentation<'a>>)> {
let credentials = admin.users_credentials_get(realm, member.id.as_ref().unwrap().as_ref()).await?;
Ok((member, credentials))
}
// add docs -> make a second loop and remove require user action for TOTP in case the credentials already have totp, this is required as get->check->put is not race condition free and a user can setup totp in between get->put
// to reduce window of opportunity, we do an additional get->set->put per user inside a lopp
async fn users_required_actions_add<'a>(admin: &'a KeycloakAdmin<'a>, realm: &str, member: UserRepresentation<'a>, ) -> Result<()> {
let mut member = admin.user_get(realm, &member.id.as_ref().unwrap()).await?;
member.required_actions = match member.required_actions {
None => Some(vec!["CONFIGURE_TOTP".into()]),
Some(mut required_actions) => {
let totp = "CONFIGURE_TOTP".into();
if !required_actions.contains(&totp) {
required_actions.push(totp);
}
Some(required_actions)
},
};
Ok(())
}
extern crate anyhow;
#[macro_use]
extern crate serde;
#[macro_use]
extern crate strum;
#[macro_use]
extern crate strum_macros;
use args::*;
mod args;
mod keycloak;
use state::State;
mod state;
mod mods;
use mods::gitlab::GitLabGlue;
use mods::keycloak::Keycloak;
use structopt::StructOpt;
use anyhow::Result;
use log::error;
use env_logger::Env;
use log::error;
async fn run(args: Args) -> Result<()> {
/* Early exit for completions */
match args.command {
Command::Completions(completions) => args::gen_completions(&completions)?,
Command::Keycloak(action) => keycloak::run(action).await?,
Command::Plan => keycloak::run(Action::Plan).await?,
Command::Apply => keycloak::run(Action::Apply).await?,
Command::Completions(completions) => {
args::gen_completions(&completions)?;
return Ok(());
}
_ => {}
}
let mut state = State::new();
//let keycloak_glue = Keycloak::new().await?;
let gitlab_glue = GitLabGlue::new().await?;
//keycloak_glue.gather(&mut state).await?;
gitlab_glue.gather(&mut state).await?;
match args.command {
Command::Completions(_) => {}
Command::Keycloak(action) => {
//keycloak_glue.run(&state, action).await?;
}
Command::Gitlab(action) => gitlab_glue.run(&state, action).await?,
Command::Plan => {
//keycloak_glue.run(&state, Action::Plan).await?;
gitlab_glue.run(&state, Action::Plan).await?;
}
Command::Apply => {
//keycloak_glue.run(&state, Action::Apply).await?;
gitlab_glue.run(&state, Action::Apply).await?;
}
}
Ok(())
}
......@@ -32,8 +67,7 @@ async fn main() {
_ => "debug",
};
env_logger::init_from_env(Env::default()
.default_filter_or(logging));
env_logger::init_from_env(Env::default().default_filter_or(logging));
if let Err(err) = run(args).await {
error!("Error: {:?}", err);
......
pub mod gitlab;
pub mod keycloak;
pub mod gitlab;
pub mod types;
pub use crate::mods::gitlab::gitlab::GitLabGlue;
//! This module defines gitlab related actions and enforcements.
//!
//! ## Features
//!
//! - ensure the integrity of the Arch Linux root group
//! - add staff members as reporter
//! - ensure nobody except devops has higher privileges
use crate::args::Action;
use crate::state::State;
use crate::mods::gitlab::types::*;
use anyhow::{Context, Result};
use log::{debug, error, info};
use std::env;
use gitlab::api::{ApiError, Query};
use gitlab::Gitlab;
use gitlab::api::common::{AccessLevel, VisibilityLevel};
use gitlab::api::projects::{Projects, FeatureAccessLevel};
use gitlab::api::groups::members::{GroupMembers, AddGroupMember, RemoveGroupMember};
use tokio::task;
const MAIN_BRANCH: &str = "main";
const ALL_TAGS: &str = "*";
pub struct GitLabGlue {
client: Gitlab,
}
impl GitLabGlue {
pub async fn new() -> Result<GitLabGlue> {
task::spawn_blocking(move || create_client()).await?
}
pub async fn gather<'a>(&'a self, state: &mut State<'a>) -> Result<()> {
Ok(())
}
pub async fn run<'a>(&self, state: &State<'a>, action: Action) -> Result<()> {
task::spawn_blocking(move || update_gitlab_group_members(&action)).await??;
//task::spawn_blocking(move || update_package_repositories(&action)).await??;
Ok(())
}
}
pub fn create_client() -> Result<GitLabGlue> {
let token =
&env::var("GLUEBUDDY_GITLAB_TOKEN").context("Missing env var GLUEBUDDY_GITLAB_TOKEN")?;
let client = Gitlab::new("gitlab.archlinux.org", token).unwrap();
Ok(GitLabGlue { client })
}
/*
fn protect_tags(client: &Gitlab, project_id: u64) -> std::result::Result<ProtectedTag, gitlab::api::ApiError<>> {
}
*/
fn update_gitlab_group_members(action: &Action) -> Result<()> {
let token =
&env::var("GLUEBUDDY_GITLAB_TOKEN").context("Missing env var GLUEBUDDY_GITLAB_TOKEN")?;
let client = Gitlab::new("gitlab.archlinux.org", token).unwrap();
println!("members");
let members_endpoint = gitlab::api::groups::members::GroupMembers::builder()
.group("bot-test")
.build