Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Linux Intune policy application #365

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"src/common",
"src/pam",
"src/nss",
"src/policies",
"src/sketching",
"src/proto",
"src/crypto",
Expand Down Expand Up @@ -47,6 +48,7 @@ anyhow = "^1.0.96"
tokio = { version = "^1.28.1", features = ["rt", "macros", "sync", "time", "net", "io-util", "signal", "rt-multi-thread"] }
tokio-util = { version = "^0.7.8", features = ["codec"] }
async-trait = "^0.1.87"
himmelblau_policies = { path = "src/policies" }
pem = "^3.0.5"
chrono = "^0.4.40"
os-release = "^0.1.0"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Himmelblau is an interoperability suite for Microsoft Azure Entra ID and Intune.
The name of the project comes from a German word for Azure (sky blue).

Himmelblau supports Linux authentication to Microsoft Azure Entra ID via PAM and NSS modules.
The PAM and NSS modules communicate with Entra ID via the himmelblaud daemon. Himmelblau plans to
enforce Intune MDM policies, but this work isn't completed yet.
The PAM and NSS modules communicate with Entra ID via the himmelblaud daemon. Himmelblau is capable
of enforcing Intune MDM polices, but is not yet able to mark the device compliant in Entra Id.

[![sambaXP 2024: Bridging Worlds – Linux and Azure AD](img/sambaxp.png)](https://www.youtube.com/watch?v=G07FTKoNTRA "sambaXP 2024: Bridging Worlds – Linux and Azure AD")

Expand Down
12 changes: 12 additions & 0 deletions man/man5/himmelblau.conf.5
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,18 @@ fi
.fi
.RE

.TP
.B apply_policy
.RE
A boolean option that enables the experimental application of Intune policies to the authenticated user. This setting does not mark the device as compliant in Entra ID, nor does it register the device as managed in Intune. Instead, it attempts to apply any assigned Intune policies to the user session.

For this feature to function correctly, an `app_id` must be defined for the corresponding domain, and the "DeviceManagementConfiguration.Read.All" API permission must be granted to it.

By default, this option is disabled.

.EXAMPLES
apply_policy = false

.TP
.B authority_host
.RE
Expand Down
4 changes: 4 additions & 0 deletions src/common/src/unix_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ pub enum TaskRequest {
LogonScript(String, String),
KerberosCCache(uid_t, uid_t, Vec<u8>, Vec<u8>),
LoadProfilePhoto(String, String),
ApplyPolicy(String, String, String),
}

impl TaskRequest {
Expand All @@ -168,6 +169,9 @@ impl TaskRequest {
TaskRequest::LoadProfilePhoto(account_id, _) => {
format!("LoadProfilePhoto({}, ...)", account_id)
}
TaskRequest::ApplyPolicy(account_id, object_id, _) => {
format!("ApplyPolicy({}, {}, ...)", account_id, object_id)
}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/config/himmelblau.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@
# provides flexibility for environments requiring custom name transformations.
# name_mapping_script =
#
# Whether to apply Intune policies (experimental). This will not mark the device
# compliant in Entra Id, nor create an Intune managed device in the portal. It
# will only attempt to apply the policies assigned in Intune to the user. An
# app_id MUST be defined for this domain for policy application to succeed, and
# the "DeviceManagementConfiguration.Read.All" API permission MUST be granted.
# apply_policy = false ; {true|false}
#
# authority_host = login.microsoftonline.com
#
# The location of the cache database
Expand Down
1 change: 1 addition & 0 deletions src/daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ serde = { workspace = true }
serde_json.workspace = true
futures = "^0.3.28"
systemd-journal-logger = "^2.1.1"
himmelblau_policies = { workspace = true }
users = "^0.11.0"
sketching = { workspace = true }
walkdir = { workspace = true }
Expand Down
74 changes: 74 additions & 0 deletions src/daemon/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,80 @@ async fn handle_client(
}
}

// Apply Intune policies
let domain = split_username(account_id)
.map(|(_, domain)| domain)
.unwrap_or("");
if cfg.get_apply_policy()
&& cfg.get_app_id(domain).is_some()
{
if let Some(token) = cachelayer
.get_user_accesstoken(
Id::Name(account_id.to_string()),
vec!["DeviceManagementConfiguration.Read.All".to_string()],
None,
)
.await {
if let Ok(uuid) = token.uuid() {
if let Some(access_token) =
token.access_token.clone()
{
let (tx, rx) = oneshot::channel();

match task_channel_tx
.send_timeout(
(
TaskRequest::ApplyPolicy(
account_id.to_string(),
uuid.to_string(),
access_token
.to_string(),
),
tx,
),
Duration::from_millis(100),
)
.await
{
Ok(()) => {
// Now wait for the other end OR timeout.
match time::timeout_at(
time::Instant::now()
+ Duration::from_millis(
1000,
),
rx,
)
.await
{
Ok(Ok(status)) => {
if status == 0 {
debug!("Successfully applied Intune policies");
} else {
debug!("Authentication was explicitly denied due to Intune failure");
resp = PamAuthResponse::Denied;
}
}
Ok(Err(e)) => {
debug!("Authentication was explicitly denied due to Intune failure: {}", e);
resp = PamAuthResponse::Denied;
}
Err(e) => {
debug!("Authentication was explicitly denied due to Intune failure: {}", e);
resp = PamAuthResponse::Denied;
}
}
}
Err(e) => {
debug!("Authentication was explicitly denied due to Intune failure: {}", e);
resp = PamAuthResponse::Denied;
}
}
}
}
}
}

ClientResponse::PamAuthenticateStepResponse(resp)
}
_ => ClientResponse::PamAuthenticateStepResponse(resp),
Expand Down
18 changes: 18 additions & 0 deletions src/daemon/src/tasks_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use std::{fs, io};
use bytes::{BufMut, BytesMut};
use futures::{SinkExt, StreamExt};
use himmelblau::graph::Graph;
use himmelblau_policies::policies::apply_group_policy;
use himmelblau_unix_common::config::{split_username, HimmelblauConfig};
use himmelblau_unix_common::constants::{DEFAULT_CCACHE_DIR, DEFAULT_CONFIG_PATH};
use himmelblau_unix_common::unix_proto::{HomeDirectoryInfo, TaskRequest, TaskResponse};
Expand Down Expand Up @@ -491,6 +492,23 @@ async fn handle_tasks(stream: UnixStream, cfg: &HimmelblauConfig) {
return;
}
}
Some(Ok(TaskRequest::ApplyPolicy(account_id, object_id, access_token))) => {
let res = apply_group_policy(cfg, &access_token, &account_id, &object_id)
.await
.unwrap_or_else(|e| {
error!("Failed to apply Intune policies: {:?}", e);
false
});

// Indicate the status response
if let Err(e) = reqs
.send(TaskResponse::Success(if res { 0 } else { 1 }))
.await
{
error!("Error -> {:?}", e);
return;
}
}
Some(Err(e)) => {
error!("Error -> {:?}", e);
return;
Expand Down
5 changes: 5 additions & 0 deletions src/policies/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ serde_json = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
regex = "^1.9.1"
base64.workspace = true
tokio.workspace = true
himmelblau_unix_common = { workspace = true }
os-release = "0.1.0"
semver = "1.0.25"
47 changes: 5 additions & 42 deletions src/policies/src/chromium_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::cse::CSE;
use crate::policies::{Policy, PolicyType, ValueType};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use himmelblau_unix_common::config::HimmelblauConfig;
use regex::Regex;
use serde_json;
use std::collections::HashMap;
Expand All @@ -32,11 +33,7 @@ static CHROME_MANAGED_POLICIES_PATH: &str = "/etc/opt/chrome/policies/managed";
static RECOMMENDED_POLICIES_PATH: &str = "/etc/chromium/policies/recommended";
static CHROME_RECOMMENDED_POLICIES_PATH: &str = "/etc/opt/chrome/policies/recommended";

pub struct ChromiumUserCSE {
pub graph_url: String,
pub access_token: String,
pub id: String,
}
pub struct ChromiumUserCSE {}

fn write_policy_to_file(dirname: &str, key: &str, val: &ValueType) -> Result<()> {
let mut file = File::create(format!("{}/{}.json", dirname, key))?;
Expand All @@ -48,23 +45,11 @@ fn write_policy_to_file(dirname: &str, key: &str, val: &ValueType) -> Result<()>

#[async_trait]
impl CSE for ChromiumUserCSE {
fn new(graph_url: &str, access_token: &str, id: &str) -> Self {
ChromiumUserCSE {
graph_url: graph_url.to_string(),
access_token: access_token.to_string(),
id: id.to_string(),
}
fn new(_config: &HimmelblauConfig, _username: &str) -> Self {
ChromiumUserCSE {}
}

async fn process_group_policy(
&self,
deleted_gpo_list: Vec<Arc<dyn Policy>>,
changed_gpo_list: Vec<Arc<dyn Policy>>,
) -> Result<bool> {
debug!("Applying Chromium policy to user with id {}", self.id);

for _gpo in deleted_gpo_list { /* TODO: Unapply policies that have been removed */ }

async fn process_group_policy(&self, changed_gpo_list: Vec<Arc<dyn Policy>>) -> Result<bool> {
for gpo in changed_gpo_list {
let pattern = Regex::new(r"^\\Google\\Google Chrome")?;
let defs = gpo.list_policy_settings(pattern)?;
Expand Down Expand Up @@ -97,28 +82,6 @@ impl CSE for ChromiumUserCSE {
}
Ok(true)
}

async fn rsop(&self, gpo: Arc<dyn Policy>) -> Result<HashMap<String, String>> {
let pattern = Regex::new(r"^\\Google\\Google Chrome")?;
let defs = gpo.list_policy_settings(pattern)?;
let mut res: HashMap<String, String> = HashMap::new();
for def in defs {
if def.enabled() && def.class_type() == PolicyType::User {
let key = match convert_display_name_to_name(
&def.key(),
key_is_recommended(&def.get_compare_pattern()),
) {
Ok(key) => key,
Err(e) => {
error!("{}", e);
continue;
}
};
res.insert(key, serde_json::to_string(&def.value())?);
}
}
Ok(res)
}
}

fn key_is_recommended(key: &str) -> bool {
Expand Down
Loading
Loading