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

ldap authentication #233

Open
wants to merge 1 commit into
base: development
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
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This transitively adds a requirement on OpenSSL, which needs some native bindings. The docker build containers will need to be updated to support this requirement.


[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, Debug)]
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 @@ -10,6 +10,7 @@

mod file_locations;
mod general;
mod ldap;
mod root_config;
mod web;

Expand Down
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, ldap::LdapConfig, web::WebConfig},
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
82 changes: 68 additions & 14 deletions src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
// 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 crate::{
env::Env,
util::{reply_data, reply_success, Error, ErrorKind, Reply}
};
use failure::ResultExt;
use ldap3::LdapConn;
use rocket::{
http::{Cookie, Cookies},
request::{self, FromRequest, Request, State},
Expand All @@ -18,6 +23,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};

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,61 @@ 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;

if ldap_config.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()
};
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved

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) = 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))
// 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 {
// The key does not match
// A key is required but not provided
Error::from(ErrorKind::Unauthorized).into_outcome()
}
} else {
// A key is required but not provided
Error::from(ErrorKind::Unauthorized).into_outcome()
}
}
}

fn ldap_login(ldap_address: &str, bind_dn: &str, username: &str, key: &str) -> Result<(), Error> {
let bind_dn = bind_dn.replace("{}", username);
LdapConn::new(&ldap_address)
.context(ErrorKind::LdapConnectError)?
.simple_bind(&bind_dn, key)
.context(ErrorKind::LdapBindError)?
.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 +172,15 @@ 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 @@ -167,6 +167,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
33 changes: 26 additions & 7 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")]
LdapBindError,
#[fail(display = "Unauthorized")]
LdapUnauthorized,
#[fail(display = "Connection error")]
LdapConnectError
}

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 @@ -248,10 +263,12 @@ impl ErrorKind {
match self {
ErrorKind::NotFound => Status::NotFound,
ErrorKind::AlreadyExists => Status::Conflict,
ErrorKind::InvalidDomain | ErrorKind::BadRequest | ErrorKind::InvalidSettingValue => {
Status::BadRequest
}
ErrorKind::Unauthorized => Status::Unauthorized,
ErrorKind::InvalidDomain
| ErrorKind::BadRequest
| ErrorKind::InvalidSettingValue
| ErrorKind::LdapMissingUsername
| ErrorKind::LdapMissingKey => Status::BadRequest,
ErrorKind::Unauthorized | ErrorKind::LdapUnauthorized => Status::Unauthorized,
ErrorKind::Unknown
| ErrorKind::GravityError
| ErrorKind::FtlConnectionFail
Expand All @@ -268,7 +285,9 @@ impl ErrorKind {
| ErrorKind::SharedMemoryLock
| ErrorKind::SharedMemoryVersion(_, _)
| ErrorKind::FtlDatabase
| ErrorKind::GravityDatabase => Status::InternalServerError
| ErrorKind::GravityDatabase
| ErrorKind::LdapBindError
| ErrorKind::LdapConnectError => Status::InternalServerError
}
}

Expand Down