From 4d0a065381c1c77e3850f47b29111dbbaad6b8f7 Mon Sep 17 00:00:00 2001 From: Wei Zhang Date: Fri, 27 Dec 2024 15:38:29 +0800 Subject: [PATCH] feat(graphQL): add ldap crendential related apis Signed-off-by: Wei Zhang --- ee/tabby-db/src/lib.rs | 1 + ee/tabby-schema/graphql/schema.graphql | 2 +- ee/tabby-schema/src/dao.rs | 51 ++++++++++++++-- ee/tabby-schema/src/schema/auth.rs | 74 +++++++++++++++------- ee/tabby-schema/src/schema/mod.rs | 19 ++++-- ee/tabby-webserver/src/ldap.rs | 5 +- ee/tabby-webserver/src/service/auth.rs | 85 +++++++++++++++++++++----- 7 files changed, 187 insertions(+), 50 deletions(-) diff --git a/ee/tabby-db/src/lib.rs b/ee/tabby-db/src/lib.rs index f40196ddf903..a73465aff58a 100644 --- a/ee/tabby-db/src/lib.rs +++ b/ee/tabby-db/src/lib.rs @@ -8,6 +8,7 @@ pub use email_setting::EmailSettingDAO; pub use integrations::IntegrationDAO; pub use invitations::InvitationDAO; pub use job_runs::JobRunDAO; +pub use ldap_credential::LdapCredentialDAO; pub use notifications::NotificationDAO; pub use oauth_credential::OAuthCredentialDAO; pub use provided_repositories::ProvidedRepositoryDAO; diff --git a/ee/tabby-schema/graphql/schema.graphql b/ee/tabby-schema/graphql/schema.graphql index 650e6cb67997..8c09fd6ba37d 100644 --- a/ee/tabby-schema/graphql/schema.graphql +++ b/ee/tabby-schema/graphql/schema.graphql @@ -631,6 +631,7 @@ type Mutation { deleteInvitation(id: ID!): ID! updateOauthCredential(input: UpdateOAuthCredentialInput!): Boolean! deleteOauthCredential(provider: OAuthProvider!): Boolean! + testLdapCredential(credential: UpdateLdapCredentialInput!): Boolean! updateLdapCredential(input: UpdateLdapCredentialInput!): Boolean! deleteLdapCredential: Boolean! updateEmailSetting(input: EmailSettingInput!): Boolean! @@ -768,7 +769,6 @@ type Query { oauthCredential(provider: OAuthProvider!): OAuthCredential oauthCallbackUrl(provider: OAuthProvider!): String! ldapCredential: LdapCredential - testLdapCredential: Boolean! serverInfo: ServerInfo! license: LicenseInfo! jobs: [String!]! diff --git a/ee/tabby-schema/src/dao.rs b/ee/tabby-schema/src/dao.rs index 60b7c64c8073..ebc6d5748fe0 100644 --- a/ee/tabby-schema/src/dao.rs +++ b/ee/tabby-schema/src/dao.rs @@ -2,19 +2,20 @@ use anyhow::bail; use hash_ids::HashIds; use lazy_static::lazy_static; use tabby_db::{ - EmailSettingDAO, IntegrationDAO, InvitationDAO, JobRunDAO, NotificationDAO, OAuthCredentialDAO, - ServerSettingDAO, ThreadDAO, ThreadMessageAttachmentClientCode, ThreadMessageAttachmentCode, - ThreadMessageAttachmentDoc, ThreadMessageAttachmentIssueDoc, ThreadMessageAttachmentPullDoc, - ThreadMessageAttachmentWebDoc, UserEventDAO, + EmailSettingDAO, IntegrationDAO, InvitationDAO, JobRunDAO, LdapCredentialDAO, NotificationDAO, + OAuthCredentialDAO, ServerSettingDAO, ThreadDAO, ThreadMessageAttachmentClientCode, + ThreadMessageAttachmentCode, ThreadMessageAttachmentDoc, ThreadMessageAttachmentIssueDoc, + ThreadMessageAttachmentPullDoc, ThreadMessageAttachmentWebDoc, UserEventDAO, }; use crate::{ + auth::LdapEncryptionKind, integration::{Integration, IntegrationKind, IntegrationStatus}, interface::UserValue, notification::{Notification, NotificationRecipient}, repository::RepositoryKind, schema::{ - auth::{self, OAuthCredential, OAuthProvider}, + auth::{self, LdapCredential, OAuthCredential, OAuthProvider}, email::{AuthMethod, EmailSetting, Encryption}, job, repository::{ @@ -67,6 +68,27 @@ impl TryFrom for OAuthCredential { } } +impl TryFrom for LdapCredential { + type Error = anyhow::Error; + + fn try_from(val: LdapCredentialDAO) -> Result { + Ok(LdapCredential { + host: val.host, + port: val.port as i32, + bind_dn: val.bind_dn, + bind_password: val.bind_password, + base_dn: val.base_dn, + user_filter: val.user_filter, + encryption: LdapEncryptionKind::from_enum_str(&val.encryption)?, + skip_tls_verify: val.skip_tls_verify, + email_attribute: val.email_attribute, + name_attribute: val.name_attribute, + created_at: val.created_at, + updated_at: val.updated_at, + }) + } +} + impl TryFrom for EmailSetting { type Error = anyhow::Error; @@ -447,6 +469,25 @@ impl DbEnum for OAuthProvider { } } +impl DbEnum for LdapEncryptionKind { + fn as_enum_str(&self) -> &'static str { + match self { + LdapEncryptionKind::None => "none", + LdapEncryptionKind::StartTLS => "starttls", + LdapEncryptionKind::LDAPS => "ldaps", + } + } + + fn from_enum_str(s: &str) -> anyhow::Result { + match s { + "none" => Ok(LdapEncryptionKind::None), + "starttls" => Ok(LdapEncryptionKind::StartTLS), + "ldaps" => Ok(LdapEncryptionKind::LDAPS), + _ => bail!("Invalid Ldap encryption kind"), + } + } +} + impl DbEnum for AuthMethod { fn as_enum_str(&self) -> &'static str { match self { diff --git a/ee/tabby-schema/src/schema/auth.rs b/ee/tabby-schema/src/schema/auth.rs index 0e60f6e6e3de..45a65cdf2616 100644 --- a/ee/tabby-schema/src/schema/auth.rs +++ b/ee/tabby-schema/src/schema/auth.rs @@ -379,30 +379,57 @@ pub enum LdapEncryptionKind { #[derive(GraphQLInputObject, Validate)] pub struct UpdateLdapCredentialInput { - host: String, - port: i32, - bind_dn: String, - bind_password: String, - base_dn: String, - user_filter: String, - encryption: LdapEncryptionKind, - skip_tls_verify: bool, - email_attribute: String, - name_attribute: String, + #[validate(url)] + pub host: String, + pub port: i32, + + #[validate(length(min = 1, code = "bind_dn", message = "bind_dn cannot be empty"))] + pub bind_dn: String, + #[validate(length( + min = 1, + code = "bind_password", + message = "bind_password cannot be empty" + ))] + pub bind_password: String, + + #[validate(length(min = 1, code = "base_dn", message = "base_dn cannot be empty"))] + pub base_dn: String, + #[validate(length( + min = 1, + code = "user_filter", + message = "user_filter cannot be empty, and should be in the format of `(uid=%s)`" + ))] + pub user_filter: String, + + pub encryption: LdapEncryptionKind, + pub skip_tls_verify: bool, + + #[validate(length( + min = 1, + code = "email_attribute", + message = "email_attribute cannot be empty" + ))] + pub email_attribute: String, + #[validate(length( + min = 1, + code = "name_attribute", + message = "name_attribute cannot be empty" + ))] + pub name_attribute: String, } #[derive(GraphQLObject)] pub struct LdapCredential { - host: String, - port: i32, - bind_dn: String, - bind_password: String, - base_dn: String, - user_filter: String, - encryption: LdapEncryptionKind, - skip_tls_verify: bool, - email_attribute: String, - name_attribute: String, + pub host: String, + pub port: i32, + pub bind_dn: String, + pub bind_password: String, + pub base_dn: String, + pub user_filter: String, + pub encryption: LdapEncryptionKind, + pub skip_tls_verify: bool, + pub email_attribute: String, + pub name_attribute: String, pub created_at: DateTime, pub updated_at: DateTime, @@ -476,8 +503,13 @@ pub trait AuthenticationService: Send + Sync { ) -> Result>; async fn update_oauth_credential(&self, input: UpdateOAuthCredentialInput) -> Result<()>; - async fn delete_oauth_credential(&self, provider: OAuthProvider) -> Result<()>; + + async fn read_ldap_credential(&self) -> Result>; + async fn test_ldap_credential(&self, input: UpdateLdapCredentialInput) -> Result<()>; + async fn update_ldap_credential(&self, input: UpdateLdapCredentialInput) -> Result<()>; + async fn delete_ldap_credential(&self) -> Result<()>; + async fn update_user_active(&self, id: &ID, active: bool) -> Result<()>; async fn update_user_role(&self, id: &ID, is_admin: bool) -> Result<()>; async fn update_user_avatar(&self, id: &ID, avatar: Option>) -> Result<()>; diff --git a/ee/tabby-schema/src/schema/mod.rs b/ee/tabby-schema/src/schema/mod.rs index 4e31ea67f024..99aca039833c 100644 --- a/ee/tabby-schema/src/schema/mod.rs +++ b/ee/tabby-schema/src/schema/mod.rs @@ -455,12 +455,7 @@ impl Query { async fn ldap_credential(ctx: &Context) -> Result> { check_admin(ctx).await?; - Ok(None) - } - - async fn test_ldap_credential(ctx: &Context) -> Result { - check_admin(ctx).await?; - Ok(false) + ctx.locator.auth().read_ldap_credential().await } async fn server_info(ctx: &Context) -> Result { @@ -1095,6 +1090,15 @@ impl Mutation { Ok(true) } + async fn test_ldap_credential( + ctx: &Context, + credential: UpdateLdapCredentialInput, + ) -> Result { + check_admin(ctx).await?; + ctx.locator.auth().test_ldap_credential(credential).await?; + Ok(true) + } + async fn update_ldap_credential( ctx: &Context, input: UpdateLdapCredentialInput, @@ -1102,11 +1106,14 @@ impl Mutation { check_admin(ctx).await?; check_license(ctx, &[LicenseType::Enterprise]).await?; input.validate()?; + + ctx.locator.auth().update_ldap_credential(input).await?; Ok(true) } async fn delete_ldap_credential(ctx: &Context) -> Result { check_admin(ctx).await?; + ctx.locator.auth().delete_ldap_credential().await?; Ok(true) } diff --git a/ee/tabby-webserver/src/ldap.rs b/ee/tabby-webserver/src/ldap.rs index 4ee147355be0..6bccde03f3a3 100644 --- a/ee/tabby-webserver/src/ldap.rs +++ b/ee/tabby-webserver/src/ldap.rs @@ -9,7 +9,8 @@ pub trait LdapClient: Send + Sync { } pub fn new_ldap_client( - address: String, + host: String, + port: i64, bind_dn: String, bind_password: String, base_dn: String, @@ -18,7 +19,7 @@ pub fn new_ldap_client( name_attr: String, ) -> impl LdapClient { LdapClientImpl { - address, + address: format!("ldap://{}:{}", host, port), bind_dn, bind_password, base_dn, diff --git a/ee/tabby-webserver/src/service/auth.rs b/ee/tabby-webserver/src/service/auth.rs index 30b8c0d25ea3..93ea7bc38166 100644 --- a/ee/tabby-webserver/src/service/auth.rs +++ b/ee/tabby-webserver/src/service/auth.rs @@ -13,9 +13,10 @@ use juniper::ID; use tabby_db::{DbConn, InvitationDAO}; use tabby_schema::{ auth::{ - AuthenticationService, Invitation, JWTPayload, OAuthCredential, OAuthError, OAuthProvider, - OAuthResponse, RefreshTokenResponse, RegisterResponse, RequestInvitationInput, - TokenAuthResponse, UpdateOAuthCredentialInput, UserSecured, + AuthenticationService, Invitation, JWTPayload, LdapCredential, OAuthCredential, OAuthError, + OAuthProvider, OAuthResponse, RefreshTokenResponse, RegisterResponse, + RequestInvitationInput, TokenAuthResponse, UpdateLdapCredentialInput, + UpdateOAuthCredentialInput, UserSecured, }, email::EmailService, is_demo_mode, @@ -323,15 +324,6 @@ impl AuthenticationService for AuthenticationServiceImpl { } async fn token_auth_ldap(&self, user_id: &str, password: &str) -> Result { - let client = ldap::new_ldap_client( - "ldap://localhost:3890".to_string(), - "cn=admin,ou=people,dc=ikw,dc=app".to_string(), - "password".to_string(), - "ou=people,dc=ikw,dc=app".to_string(), - "(&(objectClass=inetOrgPerson)(uid=%s))".to_string(), - "mail".to_string(), - "cn".to_string(), - ); let license = self .license .read() @@ -339,7 +331,6 @@ impl AuthenticationService for AuthenticationServiceImpl { .context("Failed to read license info")?; ldap_login( - Arc::new(Mutex::new(client)), &self.db, &*self.setting, &license, @@ -581,6 +572,54 @@ impl AuthenticationService for AuthenticationServiceImpl { Ok(()) } + async fn read_ldap_credential(&self) -> Result> { + let credential = self.db.read_ldap_credential().await?; + match credential { + Some(c) => Ok(Some(c.try_into()?)), + None => Ok(None), + } + } + + async fn test_ldap_credential(&self, input: UpdateLdapCredentialInput) -> Result<()> { + let mut client = ldap::new_ldap_client( + input.host, + input.port as i64, + input.bind_dn, + input.bind_password, + input.base_dn, + input.user_filter, + input.email_attribute, + input.name_attribute, + ); + + let _ = client.validate("", "").await?; + + Ok(()) + } + + async fn update_ldap_credential(&self, input: UpdateLdapCredentialInput) -> Result<()> { + self.db + .update_ldap_credential( + &input.host, + input.port, + &input.bind_dn, + &input.bind_password, + &input.base_dn, + &input.user_filter, + &input.encryption.as_enum_str(), + input.skip_tls_verify, + &input.email_attribute, + &input.name_attribute, + ) + .await?; + Ok(()) + } + + async fn delete_ldap_credential(&self) -> Result<()> { + self.db.delete_ldap_credential().await?; + Ok(()) + } + async fn update_user_active(&self, id: &ID, active: bool) -> Result<()> { let id = id.as_rowid()?; let user = self.db.get_user(id).await?.context("User doesn't exits")?; @@ -611,7 +650,6 @@ impl AuthenticationService for AuthenticationServiceImpl { } async fn ldap_login( - client: Arc>, db: &DbConn, setting: &dyn SettingService, license: &LicenseInfo, @@ -619,7 +657,24 @@ async fn ldap_login( user_id: &str, password: &str, ) -> Result { - let user = client.lock().await.validate(user_id, password).await?; + let credential = db.read_ldap_credential().await?; + if credential.is_none() { + bail!("LDAP is not configured"); + } + + let credential = credential.unwrap(); + let mut client = ldap::new_ldap_client( + credential.host, + credential.port, + credential.bind_dn, + credential.bind_password, + credential.base_dn, + credential.user_filter, + credential.email_attribute, + credential.name_attribute, + ); + + let user = client.validate(user_id, password).await?; let user_id = get_or_create_sso_user(license, db, setting, mail, &user.email, &user.name) .await .map_err(|e| CoreError::Other(anyhow!("fail to get or create ldap user: {}", e)))?;