Skip to content

Commit

Permalink
feat(graphQL): add ldap crendential related apis
Browse files Browse the repository at this point in the history
Signed-off-by: Wei Zhang <[email protected]>
  • Loading branch information
zwpaper committed Dec 27, 2024
1 parent 5e685cf commit 206df93
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 50 deletions.
1 change: 1 addition & 0 deletions ee/tabby-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion ee/tabby-schema/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ type Mutation {
deleteInvitation(id: ID!): ID!
updateOauthCredential(input: UpdateOAuthCredentialInput!): Boolean!
deleteOauthCredential(provider: OAuthProvider!): Boolean!
testLdapCredential(input: UpdateLdapCredentialInput!): Boolean!
updateLdapCredential(input: UpdateLdapCredentialInput!): Boolean!
deleteLdapCredential: Boolean!
updateEmailSetting(input: EmailSettingInput!): Boolean!
Expand Down Expand Up @@ -768,7 +769,6 @@ type Query {
oauthCredential(provider: OAuthProvider!): OAuthCredential
oauthCallbackUrl(provider: OAuthProvider!): String!
ldapCredential: LdapCredential
testLdapCredential: Boolean!
serverInfo: ServerInfo!
license: LicenseInfo!
jobs: [String!]!
Expand Down
51 changes: 46 additions & 5 deletions ee/tabby-schema/src/dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -67,6 +68,27 @@ impl TryFrom<OAuthCredentialDAO> for OAuthCredential {
}
}

impl TryFrom<LdapCredentialDAO> for LdapCredential {
type Error = anyhow::Error;

fn try_from(val: LdapCredentialDAO) -> Result<Self, Self::Error> {
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<EmailSettingDAO> for EmailSetting {
type Error = anyhow::Error;

Expand Down Expand Up @@ -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<Self> {
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 {
Expand Down
78 changes: 57 additions & 21 deletions ee/tabby-schema/src/schema/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,30 +379,61 @@ 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(length(
min = 1,
code = "host",
message = "host should not be empty and should be a valid hostname or IP address"
))]
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<Utc>,
pub updated_at: DateTime<Utc>,
Expand Down Expand Up @@ -476,8 +507,13 @@ pub trait AuthenticationService: Send + Sync {
) -> Result<Option<OAuthCredential>>;

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<Option<LdapCredential>>;
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<Box<[u8]>>) -> Result<()>;
Expand Down
16 changes: 10 additions & 6 deletions ee/tabby-schema/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,12 +455,7 @@ impl Query {

async fn ldap_credential(ctx: &Context) -> Result<Option<LdapCredential>> {
check_admin(ctx).await?;
Ok(None)
}

async fn test_ldap_credential(ctx: &Context) -> Result<bool> {
check_admin(ctx).await?;
Ok(false)
ctx.locator.auth().read_ldap_credential().await
}

async fn server_info(ctx: &Context) -> Result<ServerInfo> {
Expand Down Expand Up @@ -1095,18 +1090,27 @@ impl Mutation {
Ok(true)
}

async fn test_ldap_credential(ctx: &Context, input: UpdateLdapCredentialInput) -> Result<bool> {
check_admin(ctx).await?;
ctx.locator.auth().test_ldap_credential(input).await?;
Ok(true)
}

async fn update_ldap_credential(
ctx: &Context,
input: UpdateLdapCredentialInput,
) -> Result<bool> {
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<bool> {
check_admin(ctx).await?;
ctx.locator.auth().delete_ldap_credential().await?;
Ok(true)
}

Expand Down
5 changes: 3 additions & 2 deletions ee/tabby-webserver/src/ldap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
91 changes: 76 additions & 15 deletions ee/tabby-webserver/src/service/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -323,23 +324,13 @@ impl AuthenticationService for AuthenticationServiceImpl {
}

async fn token_auth_ldap(&self, user_id: &str, password: &str) -> Result<TokenAuthResponse> {
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()
.await
.context("Failed to read license info")?;

ldap_login(
Arc::new(Mutex::new(client)),
&self.db,
&*self.setting,
&license,
Expand Down Expand Up @@ -581,6 +572,60 @@ impl AuthenticationService for AuthenticationServiceImpl {
Ok(())
}

async fn read_ldap_credential(&self) -> Result<Option<LdapCredential>> {
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,
);

if let Err(e) = client.validate("", "").await {
if e.to_string().contains("User not found") {
return Ok(());
} else {
bail!("Failed to connect to LDAP server: {e}");
}
}

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")?;
Expand Down Expand Up @@ -611,15 +656,31 @@ impl AuthenticationService for AuthenticationServiceImpl {
}

async fn ldap_login(
client: Arc<Mutex<dyn LdapClient>>,
db: &DbConn,
setting: &dyn SettingService,
license: &LicenseInfo,
mail: &dyn EmailService,
user_id: &str,
password: &str,
) -> Result<TokenAuthResponse> {
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)))?;
Expand Down

0 comments on commit 206df93

Please sign in to comment.