Skip to content
This repository has been archived by the owner on Mar 2, 2020. It is now read-only.

Commit

Permalink
ldap authentication
Browse files Browse the repository at this point in the history
Signed-off-by: Boris Rybalkin <[email protected]>
  • Loading branch information
cyberb committed Oct 11, 2019
1 parent c91ceb6 commit 81219b1
Show file tree
Hide file tree
Showing 8 changed files with 959 additions and 21 deletions.
786 changes: 786 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ nix = "0.14"
base64 = "0.10"
task_scheduler = "0.2.0"
structopt = "0.2.18"

# Statically link SQLite (use the crate version provided by Diesel)
# The highest version which Diesel currently allows is 0.12.0
libsqlite3-sys = { version = "0.12.0", features = ["bundled"] }
ldap3 = "0.6.1"

[dev-dependencies]
serde_json = "1.0"
Expand Down
67 changes: 67 additions & 0 deletions src/env/config/ldap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Pi-hole: A black hole for Internet advertisements
// (c) 2019 Pi-hole, LLC (https://pi-hole.net)
// Network-wide ad blocking via your own hardware.
//
// API
// LDAP Config
//
// This file is copyright under the latest version of the EUPL.
// Please see LICENSE file for your rights under this license.

/// Configuration settings for LDAP authentication
#[derive(Deserialize, Clone)]
pub struct LdapConfig {
/// If LDAP should be enabled
#[serde(default = "default_enabled")]
pub enabled: bool,

/// LDAP server address
#[serde(default = "default_address")]
pub address: String,

/// Bind Dn
#[serde(default = "default_bind_dn")]
pub bind_dn: String
}

impl Default for LdapConfig {
fn default() -> Self {
LdapConfig {
enabled: default_enabled(),
address: default_address(),
bind_dn: default_bind_dn()
}
}
}

impl LdapConfig {
pub fn is_valid(&self) -> bool {
true
}
}

fn default_enabled() -> bool {
false
}

fn default_address() -> String {
"ldap://localhost:389".to_owned()
}

fn default_bind_dn() -> String {
"".to_owned()
}

#[cfg(test)]
mod test {
use super::LdapConfig;

/// The default config is valid
#[test]
fn valid_ldap() {
let ldap_config = LdapConfig::default();

assert!(ldap_config.is_valid())
}

}
1 change: 1 addition & 0 deletions src/env/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ mod file_locations;
mod general;
mod root_config;
mod web;
mod ldap;

pub use self::root_config::Config;
6 changes: 4 additions & 2 deletions src/env/config/root_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// Please see LICENSE file for your rights under this license.

use crate::{
env::config::{file_locations::Files, general::General, web::WebConfig},
env::config::{file_locations::Files, general::General, web::WebConfig, ldap::LdapConfig},
util::{Error, ErrorKind}
};
use failure::{Fail, ResultExt};
Expand All @@ -29,7 +29,9 @@ pub struct Config {
#[serde(default)]
pub file_locations: Files,
#[serde(default)]
pub web: WebConfig
pub web: WebConfig,
#[serde(default)]
pub ldap: LdapConfig
}

impl Config {
Expand Down
89 changes: 75 additions & 14 deletions src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,22 @@
// This file is copyright under the latest version of the EUPL.
// Please see LICENSE file for your rights under this license.

use crate::util::{reply_success, Error, ErrorKind, Reply};
use ldap3::LdapConn;
use crate::{
env::Env
};
use crate::util::{reply_success, reply_data, Error, ErrorKind, Reply};
use rocket::{
http::{Cookie, Cookies},
request::{self, FromRequest, Request, State},
Outcome
};
use std::sync::atomic::{AtomicUsize, Ordering};
use failure::ResultExt;

const USER_ATTR: &str = "user_id";
const AUTH_HEADER: &str = "X-Pi-hole-Authenticate";
const AUTH_HEADER_USERNAME: &str = "X-Pi-hole-Authenticate-username";

/// When used as a request guard, requests must be authenticated
pub struct User {
Expand All @@ -30,6 +36,11 @@ pub struct AuthData {
next_id: AtomicUsize
}

#[derive(Serialize)]
pub struct AuthMode {
pub mode: String
}

impl User {
/// Try to get the user ID from cookies
fn get_from_cookie(cookies: &mut Cookies) -> Option<Self> {
Expand Down Expand Up @@ -75,27 +86,66 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
None => return Error::from(ErrorKind::Unknown).into_outcome()
};

// Check if a key is required for authentication
if !auth_data.key_required() {
return Outcome::Success(User::create_and_store_user(request, &auth_data));
}
let key_opt = request.headers().get_one(AUTH_HEADER);
let env: State<Env> = match request.guard().succeeded() {
Some(env) => env,
None => return Error::from(ErrorKind::Unknown).into_outcome()
};
let ldap_config = &env.config().ldap;

// Check the user's key, if provided
if let Some(key) = request.headers().get_one(AUTH_HEADER) {
if auth_data.key_matches(key) {
// The key matches, so create and store a new user and cookie
Outcome::Success(Self::create_and_store_user(request, &auth_data))
} else {
// The key does not match
Error::from(ErrorKind::Unauthorized).into_outcome()
if ldap_config.enabled {
println!("LDAP is enabled");
let key = match key_opt {
Some(key) => key,
None => return Error::from(ErrorKind::LdapMissingKey).into_outcome()
};
let username = match request.headers().get_one(AUTH_HEADER_USERNAME) {
Some(username) => username,
None => return Error::from(ErrorKind::LdapMissingUsername).into_outcome()
};

match ldap_login(&ldap_config.address, &ldap_config.bind_dn, username, key) {
Ok(_) => Outcome::Success(User::create_and_store_user(request, &auth_data)),
Err(e) => e.into_outcome()
}

} else {

// Check if a key is required for authentication
if !auth_data.key_required() {
return Outcome::Success(User::create_and_store_user(request, &auth_data));
}

// Check the user's key, if provided
if let Some(key) = key_opt {
if auth_data.key_matches(key) {
// The key matches, so create and store a new user and cookie
Outcome::Success(Self::create_and_store_user(request, &auth_data))
} else {
// The key does not match
Error::from(ErrorKind::Unauthorized).into_outcome()
}
} else {
// A key is required but not provided
Error::from(ErrorKind::Unauthorized).into_outcome()
Error::from(ErrorKind::Unauthorized).into_outcome()
}

}
}
}

fn ldap_login(ldap_address: &str, bind_dn: &str, username: &str, key: &str) -> Result<(), Error> {
println!("LDAP address: {}, user: {}", ldap_address, username);
let bind_dn = bind_dn.replace("{}", username);
LdapConn::new(&ldap_address)
.map_err(|e| ErrorKind::LdapConnectError(format!("{:?}", e)))?
.simple_bind(&bind_dn, key)
.map_err(|e| ErrorKind::LdapBindError(format!("{:?}", e)))?
.success()
.context(ErrorKind::LdapUnauthorized)?;
Ok(())
}

impl AuthData {
/// Create a new API key
pub fn new(key: Option<String>) -> AuthData {
Expand Down Expand Up @@ -127,6 +177,17 @@ impl AuthData {
}
}
}
/// Get current auth mode (key/ldap) for UI to show corresponding controls
#[get("/auth/mode")]
pub fn get_auth_mode(env: State<Env>) -> Reply {
let ldap_config = &env.config().ldap;
let mode = if ldap_config.enabled {
"ldap"
} else {
"key"
};
reply_data(AuthMode { mode: mode.to_owned() })
}

/// Provides an endpoint to authenticate or check if already authenticated
#[get("/auth")]
Expand Down
1 change: 1 addition & 0 deletions src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ fn setup(
// Mount the API
.mount(&api_mount_path_str, routes![
version::version,
auth::get_auth_mode,
auth::check,
auth::logout,
stats::get_summary,
Expand Down
28 changes: 24 additions & 4 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,17 @@ pub enum ErrorKind {
#[fail(display = "Error while interacting with the FTL database")]
FtlDatabase,
#[fail(display = "Error while interacting with the Gravity database")]
GravityDatabase
GravityDatabase,
#[fail(display = "Missing key")]
LdapMissingKey,
#[fail(display = "Missing username")]
LdapMissingUsername,
#[fail(display = "Bind error: {}", _0)]
LdapBindError(String),
#[fail(display = "Unauthorized")]
LdapUnauthorized,
#[fail(display = "Connection error: {}", _0)]
LdapConnectError(String)
}

impl Error {
Expand Down Expand Up @@ -239,7 +249,12 @@ impl ErrorKind {
ErrorKind::SharedMemoryLock => "shared_memory_lock",
ErrorKind::SharedMemoryVersion(_, _) => "shared_memory_version",
ErrorKind::FtlDatabase => "ftl_database",
ErrorKind::GravityDatabase => "gravity_database"
ErrorKind::GravityDatabase => "gravity_database",
ErrorKind::LdapMissingKey => "ldap_missing_key",
ErrorKind::LdapMissingUsername => "ldap_missing_username",
ErrorKind::LdapConnectError(_) => "ldap_connection_error",
ErrorKind::LdapBindError(_) => "ldap_bind_error",
ErrorKind::LdapUnauthorized => "ldap_unauthorized"
}
}

Expand All @@ -251,7 +266,7 @@ impl ErrorKind {
ErrorKind::InvalidDomain | ErrorKind::BadRequest | ErrorKind::InvalidSettingValue => {
Status::BadRequest
}
ErrorKind::Unauthorized => Status::Unauthorized,
ErrorKind::Unauthorized | ErrorKind::LdapUnauthorized => Status::Unauthorized,
ErrorKind::Unknown
| ErrorKind::GravityError
| ErrorKind::FtlConnectionFail
Expand All @@ -268,7 +283,12 @@ impl ErrorKind {
| ErrorKind::SharedMemoryLock
| ErrorKind::SharedMemoryVersion(_, _)
| ErrorKind::FtlDatabase
| ErrorKind::GravityDatabase => Status::InternalServerError
| ErrorKind::GravityDatabase
| ErrorKind::LdapBindError(_)
| ErrorKind::LdapConnectError(_)
| ErrorKind::LdapMissingUsername
| ErrorKind::LdapMissingKey
=> Status::InternalServerError
}
}

Expand Down

0 comments on commit 81219b1

Please sign in to comment.