Ldap support #2
@@ -100,4 +100,5 @@ AUTHORIZATION_TYPE=local
|
|||||||
# LDAP_USERNAME_ATTRIBUTE=sAMAccountName
|
# LDAP_USERNAME_ATTRIBUTE=sAMAccountName
|
||||||
# LDAP_USER_FILTER=(uid={username})
|
# LDAP_USER_FILTER=(uid={username})
|
||||||
# LDAP_USERNAME_ATTRIBUTE=uid
|
# LDAP_USERNAME_ATTRIBUTE=uid
|
||||||
|
# email auth: LDAP_EMAIL_ATTRIBUTE=mail
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,15 @@
|
|||||||
dn: ou=people,dc=example,dc=org
|
dn: ou=people,dc=organization,dc=local
|
||||||
objectClass: organizationalUnit
|
objectClass: organizationalUnit
|
||||||
ou: people
|
ou: people
|
||||||
|
|
||||||
dn: uid=mateusz,ou=people,dc=example,dc=org
|
dn: uid=admin,ou=people,dc=organization,dc=local
|
||||||
objectClass: inetOrgPerson
|
objectClass: inetOrgPerson
|
||||||
objectClass: organizationalPerson
|
objectClass: organizationalPerson
|
||||||
objectClass: person
|
objectClass: person
|
||||||
objectClass: top
|
objectClass: top
|
||||||
uid: mateusz
|
uid: admin
|
||||||
cn: Mateusz Testowy
|
cn: Organization Administrator
|
||||||
sn: Testowy
|
sn: Administrator
|
||||||
displayName: Mateusz Testowy
|
displayName: Organization Administrator
|
||||||
mail: mateusz@example.org
|
mail: admin@organization.local
|
||||||
userPassword: test1234
|
userPassword: test1234!
|
||||||
|
|
||||||
dn: uid=anna,ou=people,dc=example,dc=org
|
|
||||||
objectClass: inetOrgPerson
|
|
||||||
objectClass: organizationalPerson
|
|
||||||
objectClass: person
|
|
||||||
objectClass: top
|
|
||||||
uid: anna
|
|
||||||
cn: Anna Testowa
|
|
||||||
sn: Testowa
|
|
||||||
displayName: Anna Testowa
|
|
||||||
mail: anna@example.org
|
|
||||||
userPassword: test1234
|
|
||||||
@@ -10,7 +10,7 @@ services:
|
|||||||
LDAP_CONFIG_PASSWORD: config
|
LDAP_CONFIG_PASSWORD: config
|
||||||
LDAP_TLS: "false"
|
LDAP_TLS: "false"
|
||||||
ports:
|
ports:
|
||||||
- "12389:389"
|
- "10.87.2.6:389:389"
|
||||||
volumes:
|
volumes:
|
||||||
- ./bootstrap:/container/service/slapd/assets/config/bootstrap/ldif/custom:ro
|
- ./bootstrap:/container/service/slapd/assets/config/bootstrap/ldif/custom:ro
|
||||||
command: --copy-service
|
command: --copy-service
|
||||||
@@ -23,6 +23,6 @@ services:
|
|||||||
PHPLDAPADMIN_LDAP_HOSTS: ldap
|
PHPLDAPADMIN_LDAP_HOSTS: ldap
|
||||||
PHPLDAPADMIN_HTTPS: "false"
|
PHPLDAPADMIN_HTTPS: "false"
|
||||||
ports:
|
ports:
|
||||||
- "12390:80"
|
- "10.87.2.6:8088:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- ldap
|
- ldap
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ async fn home(State(state): State<SharedState>) -> Response {
|
|||||||
include_str!("../static/home.html"),
|
include_str!("../static/home.html"),
|
||||||
&state.asset_version,
|
&state.asset_version,
|
||||||
state.registration_enabled,
|
state.registration_enabled,
|
||||||
|
state.ldap.is_some(),
|
||||||
&state.frontend_log_level,
|
&state.frontend_log_level,
|
||||||
state.upload_max_size_bytes,
|
state.upload_max_size_bytes,
|
||||||
"home",
|
"home",
|
||||||
@@ -195,6 +196,7 @@ async fn pad(State(state): State<SharedState>, Path(slug): Path<String>) -> Resp
|
|||||||
&html,
|
&html,
|
||||||
&state.asset_version,
|
&state.asset_version,
|
||||||
state.registration_enabled,
|
state.registration_enabled,
|
||||||
|
state.ldap.is_some(),
|
||||||
&state.frontend_log_level,
|
&state.frontend_log_level,
|
||||||
state.upload_max_size_bytes,
|
state.upload_max_size_bytes,
|
||||||
"pad",
|
"pad",
|
||||||
@@ -222,6 +224,7 @@ async fn public_page(State(state): State<SharedState>, Path(token): Path<String>
|
|||||||
include_str!("../static/public.html"),
|
include_str!("../static/public.html"),
|
||||||
&state.asset_version,
|
&state.asset_version,
|
||||||
state.registration_enabled,
|
state.registration_enabled,
|
||||||
|
state.ldap.is_some(),
|
||||||
&state.frontend_log_level,
|
&state.frontend_log_level,
|
||||||
state.upload_max_size_bytes,
|
state.upload_max_size_bytes,
|
||||||
"public",
|
"public",
|
||||||
@@ -254,6 +257,7 @@ async fn workspace(
|
|||||||
&html,
|
&html,
|
||||||
&state.asset_version,
|
&state.asset_version,
|
||||||
state.registration_enabled,
|
state.registration_enabled,
|
||||||
|
state.ldap.is_some(),
|
||||||
&state.frontend_log_level,
|
&state.frontend_log_level,
|
||||||
state.upload_max_size_bytes,
|
state.upload_max_size_bytes,
|
||||||
"workspace",
|
"workspace",
|
||||||
@@ -308,6 +312,7 @@ async fn note(
|
|||||||
&html,
|
&html,
|
||||||
&state.asset_version,
|
&state.asset_version,
|
||||||
state.registration_enabled,
|
state.registration_enabled,
|
||||||
|
state.ldap.is_some(),
|
||||||
&state.frontend_log_level,
|
&state.frontend_log_level,
|
||||||
state.upload_max_size_bytes,
|
state.upload_max_size_bytes,
|
||||||
"note",
|
"note",
|
||||||
|
|||||||
+5
-3
@@ -27,12 +27,13 @@ pub fn render_html(
|
|||||||
template: &str,
|
template: &str,
|
||||||
asset_version: &str,
|
asset_version: &str,
|
||||||
registration_enabled: bool,
|
registration_enabled: bool,
|
||||||
|
external_auth: bool,
|
||||||
frontend_log_level: &str,
|
frontend_log_level: &str,
|
||||||
upload_max_size_bytes: usize,
|
upload_max_size_bytes: usize,
|
||||||
entrypoint: &str,
|
entrypoint: &str,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let urls = AssetUrls::new(asset_version);
|
let urls = AssetUrls::new(asset_version);
|
||||||
let frontend_config = frontend_config(frontend_log_level, upload_max_size_bytes);
|
let frontend_config = frontend_config(frontend_log_level, upload_max_size_bytes, external_auth);
|
||||||
let html = template
|
let html = template
|
||||||
.replace("__APP_STYLESHEET__", &urls.stylesheet("styles"))
|
.replace("__APP_STYLESHEET__", &urls.stylesheet("styles"))
|
||||||
.replace("__APP_IMPORT_MAP__", &urls.import_map())
|
.replace("__APP_IMPORT_MAP__", &urls.import_map())
|
||||||
@@ -59,11 +60,12 @@ pub fn stylesheet_tag(asset_version: &str, name: &str) -> String {
|
|||||||
AssetUrls::new(asset_version).stylesheet(name)
|
AssetUrls::new(asset_version).stylesheet(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn frontend_config(frontend_log_level: &str, upload_max_size_bytes: usize) -> String {
|
fn frontend_config(frontend_log_level: &str, upload_max_size_bytes: usize, external_auth: bool) -> String {
|
||||||
format!(
|
format!(
|
||||||
r#"<script>window.__RUSTPAD_CONFIG__=Object.freeze({{frontendLogLevel:"{}",uploadMaxSizeBytes:{}}});</script>"#,
|
r#"<script>window.__RUSTPAD_CONFIG__=Object.freeze({{frontendLogLevel:"{}",uploadMaxSizeBytes:{},externalAuth:{}}});</script>"#,
|
||||||
escape_js_string(frontend_log_level),
|
escape_js_string(frontend_log_level),
|
||||||
upload_max_size_bytes,
|
upload_max_size_bytes,
|
||||||
|
external_auth,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -53,13 +53,26 @@ pub async fn authenticate(
|
|||||||
config.email_attribute.as_str(),
|
config.email_attribute.as_str(),
|
||||||
config.display_name_attribute.as_str(),
|
config.display_name_attribute.as_str(),
|
||||||
];
|
];
|
||||||
let (entries, _) = ldap
|
let (mut entries, _) = ldap
|
||||||
.search(&config.base_dn, Scope::Subtree, &filter, attributes)
|
.search(&config.base_dn, Scope::Subtree, &filter, attributes.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(|error| format!("LDAP search failed: {error}"))?
|
.map_err(|error| format!("LDAP search failed: {error}"))?
|
||||||
.success()
|
.success()
|
||||||
.map_err(|error| format!("LDAP search rejected: {error}"))?;
|
.map_err(|error| format!("LDAP search rejected: {error}"))?;
|
||||||
|
|
||||||
|
// Allow users to sign in with their directory e-mail even when the configured
|
||||||
|
// primary filter searches by uid/sAMAccountName only.
|
||||||
|
if entries.is_empty() && login.trim().contains('@') {
|
||||||
|
let email_filter = format!("({}={})", config.email_attribute, escaped);
|
||||||
|
let (email_entries, _) = ldap
|
||||||
|
.search(&config.base_dn, Scope::Subtree, &email_filter, attributes)
|
||||||
|
.await
|
||||||
|
.map_err(|error| format!("LDAP e-mail search failed: {error}"))?
|
||||||
|
.success()
|
||||||
|
.map_err(|error| format!("LDAP e-mail search rejected: {error}"))?;
|
||||||
|
entries = email_entries;
|
||||||
|
}
|
||||||
|
|
||||||
if entries.len() != 1 {
|
if entries.len() != 1 {
|
||||||
let _ = ldap.unbind().await;
|
let _ = ldap.unbind().await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
+12
-2
@@ -24,6 +24,7 @@ export function bindIdentityDialog({ dialog, onIdentity, initialMode = "login" }
|
|||||||
const resetButton = dialog.querySelector("#show-reset");
|
const resetButton = dialog.querySelector("#show-reset");
|
||||||
const backButton = dialog.querySelector("#reset-back");
|
const backButton = dialog.querySelector("#reset-back");
|
||||||
const registrationEnabled = document.body.dataset.registrationEnabled === "true";
|
const registrationEnabled = document.body.dataset.registrationEnabled === "true";
|
||||||
|
const externalAuth = window.__RUSTPAD_CONFIG__?.externalAuth === true;
|
||||||
let mode = initialMode;
|
let mode = initialMode;
|
||||||
|
|
||||||
const setMode = (nextMode) => {
|
const setMode = (nextMode) => {
|
||||||
@@ -47,9 +48,12 @@ export function bindIdentityDialog({ dialog, onIdentity, initialMode = "login" }
|
|||||||
submit.textContent = resetting ? "Send reset link" : registering ? "Create account" : "Log in";
|
submit.textContent = resetting ? "Send reset link" : registering ? "Create account" : "Log in";
|
||||||
switchMode.hidden = resetting || !registrationEnabled;
|
switchMode.hidden = resetting || !registrationEnabled;
|
||||||
switchMode.textContent = registering ? "Already registered? Log in" : "Create an account";
|
switchMode.textContent = registering ? "Already registered? Log in" : "Create an account";
|
||||||
resetButton.hidden = resetting || registering;
|
resetButton.hidden = resetting || registering || externalAuth;
|
||||||
backButton.hidden = !resetting;
|
backButton.hidden = !resetting;
|
||||||
const loginMode = mode === "login";
|
const loginMode = mode === "login";
|
||||||
|
email.type = externalAuth && loginMode ? "text" : "email";
|
||||||
|
if (externalAuth && loginMode) password.removeAttribute("minlength");
|
||||||
|
else password.minLength = 8;
|
||||||
form.autocomplete = loginMode ? "on" : "off";
|
form.autocomplete = loginMode ? "on" : "off";
|
||||||
nickname.autocomplete = "off";
|
nickname.autocomplete = "off";
|
||||||
nickname.dataset.bwignore = "true";
|
nickname.dataset.bwignore = "true";
|
||||||
@@ -143,6 +147,7 @@ function bindLegacyIdentityDialog({ dialog, onIdentity }) {
|
|||||||
const backButton = dialog.querySelector("#auth-back");
|
const backButton = dialog.querySelector("#auth-back");
|
||||||
const logoutButton = dialog.querySelector("#logout-account");
|
const logoutButton = dialog.querySelector("#logout-account");
|
||||||
const registrationEnabled = document.body.dataset.registrationEnabled === "true";
|
const registrationEnabled = document.body.dataset.registrationEnabled === "true";
|
||||||
|
const externalAuth = window.__RUSTPAD_CONFIG__?.externalAuth === true;
|
||||||
let mode = "login";
|
let mode = "login";
|
||||||
|
|
||||||
const updateActions = () => {
|
const updateActions = () => {
|
||||||
@@ -170,6 +175,9 @@ function bindLegacyIdentityDialog({ dialog, onIdentity }) {
|
|||||||
modeTitle.textContent = mode === "register" ? "Register nickname" : "Log in";
|
modeTitle.textContent = mode === "register" ? "Register nickname" : "Log in";
|
||||||
authSubmit.textContent = mode === "register" ? "Register and continue" : "Log in and continue";
|
authSubmit.textContent = mode === "register" ? "Register and continue" : "Log in and continue";
|
||||||
const loginMode = mode === "login";
|
const loginMode = mode === "login";
|
||||||
|
email.type = externalAuth && loginMode ? "text" : "email";
|
||||||
|
if (externalAuth && loginMode) password.removeAttribute("minlength");
|
||||||
|
else password.minLength = 8;
|
||||||
form.autocomplete = loginMode ? "on" : "off";
|
form.autocomplete = loginMode ? "on" : "off";
|
||||||
nickname.autocomplete = "off";
|
nickname.autocomplete = "off";
|
||||||
nickname.dataset.bwignore = "true";
|
nickname.dataset.bwignore = "true";
|
||||||
@@ -194,7 +202,9 @@ function bindLegacyIdentityDialog({ dialog, onIdentity }) {
|
|||||||
dialog.addEventListener("close", collapse);
|
dialog.addEventListener("close", collapse);
|
||||||
dialog.addEventListener("cancel", collapse);
|
dialog.addEventListener("cancel", collapse);
|
||||||
|
|
||||||
dialog.querySelector("#show-reset")?.addEventListener("click", async () => {
|
const legacyResetButton = dialog.querySelector("#show-reset");
|
||||||
|
if (legacyResetButton) legacyResetButton.hidden = externalAuth;
|
||||||
|
legacyResetButton?.addEventListener("click", async () => {
|
||||||
const value = email.value.trim() || await askInput({
|
const value = email.value.trim() || await askInput({
|
||||||
title: "Reset password",
|
title: "Reset password",
|
||||||
message: "Enter the e-mail address assigned to your local account.",
|
message: "Enter the e-mail address assigned to your local account.",
|
||||||
|
|||||||
Reference in New Issue
Block a user