CB-931 ldap identity provider (#2609)

* CB-931 ldap identity provider

* CB-931 add bruteforce protection to ldap

---------

Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
Alexander Skoblikov
2024-05-20 15:05:09 +00:00
committed by GitHub
co-authored by Evgenia Bezborodova
parent 7cd22a84c2
commit 72ce16b2e8
12 changed files with 440 additions and 0 deletions
@@ -22,6 +22,7 @@ Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport,
Export-Package: io.cloudbeaver,
io.cloudbeaver.auth,
io.cloudbeaver.auth.provider,
io.cloudbeaver.auth.provider.fa,
io.cloudbeaver.auth.provider.local,
io.cloudbeaver.auth.provisioning,
io.cloudbeaver.websocket,
@@ -0,0 +1,79 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.auth.provider.fa;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.auth.*;
import java.time.LocalDateTime;
import java.util.Map;
public abstract class AbstractSessionExternal implements SMSessionExternal {
@NotNull
protected final Map<String, Object> authParameters;
@NotNull
protected final SMSession parentSession;
@NotNull
protected final SMAuthSpace space;
protected AbstractSessionExternal(
@NotNull SMSession parentSession,
@NotNull SMAuthSpace space,
@NotNull Map<String, Object> authParameters
) {
this.parentSession = parentSession;
this.space = space;
this.authParameters = authParameters;
}
@NotNull
@Override
public SMAuthSpace getSessionSpace() {
return space;
}
@NotNull
@Override
public SMSessionContext getSessionContext() {
return this.parentSession.getSessionContext();
}
@Nullable
@Override
public SMSessionPrincipal getSessionPrincipal() {
return parentSession.getSessionPrincipal();
}
@NotNull
@Override
public LocalDateTime getSessionStart() {
return parentSession.getSessionStart();
}
@Override
public void close() {
// do nothing
}
@Override
public Map<String, Object> getAuthParameters() {
return authParameters;
}
}
@@ -0,0 +1,14 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Vendor: Cloudbeaver LDAP
Bundle-Vendor: DBeaver Corp
Bundle-SymbolicName: io.cloudbeaver.service.ldap.auth;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Release-Date: 20240506
Bundle-RequiredExecutionEnvironment: JavaSE-17
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
Require-Bundle: org.jkiss.dbeaver.model;visibility:=reexport,
org.jkiss.dbeaver.registry;visibility:=reexport,
io.cloudbeaver.model
Automatic-Module-Name: io.cloudbeaver.service.ldap.auth
@@ -0,0 +1,5 @@
source.. = src/
output.. = target/classes/
bin.includes = .,\
META-INF/,\
plugin.xml
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.2"?>
<plugin>
<extension point="org.jkiss.dbeaver.auth.provider">
<authProvider id="ldap" label="LDAP" description="LDAP authentication provider"
configurable="true"
class="io.cloudbeaver.service.ldap.auth.LdapAuthProvider"
icon="platform:/plugin/org.jkiss.dbeaver.model/icons/idp/ldap.png"
>
<configuration>
<propertyGroup label="LDAP" description="LDAP authentication">
<property id="ldap-host" label="Host" type="string" description="LDAP server host" required="true"/>
<property id="ldap-port" label="Port" type="integer" defaultValue="389" required="true" description="LDAP server port, default is 389"/>
<property id="ldap-dn" label="Base Distinguished Name" type="string"
description="Base Distinguished Name applicable for all users, example: dc=myOrg,dc=com"
required="true"
/>
</propertyGroup>
</configuration>
<credentials>
<propertyGroup label="Auth credentials">
<property id="units" label="Unit paths" type="string" description="LDAP unit paths separated by commas. Example ou=unit1,ou=unit2" user="true"/>
<property id="user" label="User name" type="string" description="LDAP user name" user="true"/>
<property id="password" label="User password" type="string" description="LDAP user password"
user="true" encryption="plain"/>
</propertyGroup>
</credentials>
</authProvider>
</extension>
</plugin>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.cloudbeaver</groupId>
<artifactId>bundles</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.ldap.auth</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -0,0 +1,166 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.ldap.auth;
import io.cloudbeaver.DBWUserIdentity;
import io.cloudbeaver.auth.SMAuthProviderExternal;
import io.cloudbeaver.auth.SMBruteForceProtected;
import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.model.user.WebUser;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.DBPObject;
import org.jkiss.dbeaver.model.auth.SMSession;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration;
import org.jkiss.dbeaver.model.security.SMController;
import org.jkiss.utils.CommonUtils;
import javax.naming.Context;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class LdapAuthProvider implements SMAuthProviderExternal<SMSession>, SMBruteForceProtected {
public LdapAuthProvider() {
}
@Override
public Map<String, Object> authExternalUser(
@NotNull DBRProgressMonitor monitor,
@Nullable SMAuthProviderCustomConfiguration providerConfig,
@NotNull Map<String, Object> authParameters
) throws DBException {
if (providerConfig == null) {
throw new DBException("LDAP provider config is null");
}
String userName = JSONUtils.getString(authParameters, LdapConstants.CRED_USERNAME);
if (CommonUtils.isEmpty(userName)) {
throw new DBException("LDAP user name is empty");
}
String password = JSONUtils.getString(authParameters, LdapConstants.CRED_PASSWORD);
if (CommonUtils.isEmpty(password)) {
throw new DBException("LDAP password is empty");
}
String unit = CommonUtils.nullIfEmpty(JSONUtils.getString(authParameters, LdapConstants.CRED_UNITS));
LdapSettings ldapSettings = new LdapSettings(providerConfig);
Hashtable<String, String> environment = new Hashtable<>();
environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
var ldapProviderUrl = "ldap://" + ldapSettings.getHost() + ":" + ldapSettings.getPort();
environment.put(Context.PROVIDER_URL, ldapProviderUrl);
environment.put(Context.SECURITY_AUTHENTICATION, "simple");
String cn = "cn=" + userName;
var principal = Stream.of(cn, unit, ldapSettings.getBaseDN())
.filter(CommonUtils::isNotEmpty)
.collect(Collectors.joining(","));
environment.put(Context.SECURITY_PRINCIPAL, principal);
environment.put(Context.SECURITY_CREDENTIALS, password);
try {
DirContext context = new InitialDirContext(environment);
context.close();
Map<String, Object> userData = new HashMap<>();
userData.put(LdapConstants.CRED_USERNAME, userName);
userData.put(LdapConstants.CRED_SESSION_ID, UUID.randomUUID());
return userData;
} catch (Exception e) {
throw new DBException("LDAP authentication failed: " + e.getMessage(), e);
}
}
@NotNull
@Override
public DBWUserIdentity getUserIdentity(
@NotNull DBRProgressMonitor monitor,
@Nullable SMAuthProviderCustomConfiguration customConfiguration,
@NotNull Map<String, Object> authParameters
) throws DBException {
String userName = JSONUtils.getString(authParameters, LocalAuthProviderConstants.CRED_USER);
if (CommonUtils.isEmpty(userName)) {
throw new DBException("LDAP user name is empty");
}
return new DBWUserIdentity(userName, userName);
}
@Nullable
@Override
public DBPObject getUserDetails(
@NotNull DBRProgressMonitor monitor,
@NotNull WebSession webSession,
@NotNull SMSession session,
@NotNull WebUser user,
boolean selfIdentity
) throws DBException {
return null;
}
@NotNull
@Override
public String validateLocalAuth(
@NotNull DBRProgressMonitor monitor,
@NotNull SMController securityController,
@NotNull SMAuthProviderCustomConfiguration providerConfig,
@NotNull Map<String, Object> userCredentials,
@Nullable String activeUserId
) throws DBException {
String userId = JSONUtils.getString(userCredentials, LdapConstants.CRED_USERNAME);
if (CommonUtils.isEmpty(userId)) {
throw new DBException("LDAP user id not found");
}
return activeUserId == null ? userId : activeUserId;
}
@Override
public SMSession openSession(
@NotNull DBRProgressMonitor monitor,
@NotNull SMSession mainSession,
@Nullable SMAuthProviderCustomConfiguration customConfiguration,
@NotNull Map<String, Object> userCredentials
) throws DBException {
return new LdapSession(mainSession, mainSession.getSessionSpace(), userCredentials);
}
@Override
public void closeSession(@NotNull SMSession mainSession, SMSession session) throws DBException {
}
@Override
public void refreshSession(
@NotNull DBRProgressMonitor monitor,
@NotNull SMSession mainSession,
SMSession session
) throws DBException {
}
@Override
public Object getInputUsername(@NotNull Map<String, Object> cred) {
return cred.get(LdapConstants.CRED_USERNAME);
}
}
@@ -0,0 +1,29 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.ldap.auth;
public interface LdapConstants {
String PARAM_HOST = "ldap-host";
String PARAM_PORT = "ldap-port";
String PARAM_DN = "ldap-dn";
String CRED_USERNAME = "user";
String CRED_PASSWORD = "password";
String CRED_UNITS = "units";
String CRED_SESSION_ID = "session-id";
}
@@ -0,0 +1,41 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.ldap.auth;
import io.cloudbeaver.auth.provider.fa.AbstractSessionExternal;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.auth.SMAuthSpace;
import org.jkiss.dbeaver.model.auth.SMSession;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import java.util.Map;
public class LdapSession extends AbstractSessionExternal {
protected LdapSession(
@NotNull SMSession parentSession,
@NotNull SMAuthSpace space,
@NotNull Map<String, Object> authParameters
) {
super(parentSession, space, authParameters);
}
@NotNull
@Override
public String getSessionId() {
return JSONUtils.getString(authParameters, LdapConstants.CRED_SESSION_ID, "sessionNotFound");
}
}
@@ -0,0 +1,54 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.ldap.auth;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration;
import org.jkiss.utils.CommonUtils;
public class LdapSettings {
@NotNull
private final SMAuthProviderCustomConfiguration providerConfiguration;
@NotNull
private final String host;
@NotNull
private final String baseDN;
private final int port;
protected LdapSettings(SMAuthProviderCustomConfiguration providerConfiguration) {
this.providerConfiguration = providerConfiguration;
this.host = providerConfiguration.getParameter(LdapConstants.PARAM_HOST);
this.port = CommonUtils.isNotEmpty(providerConfiguration.getParameter(LdapConstants.PARAM_PORT)) ? Integer.parseInt(
providerConfiguration.getParameter(LdapConstants.PARAM_PORT)) : 389;
this.baseDN = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_DN, "");
}
@NotNull
public String getBaseDN() {
return baseDN;
}
@NotNull
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}
+2
View File
@@ -24,7 +24,9 @@
<module>io.cloudbeaver.service.rm</module>
<module>io.cloudbeaver.service.rm.nio</module>
<module>io.cloudbeaver.service.data.transfer</module>
<module>io.cloudbeaver.service.security</module>
<module>io.cloudbeaver.service.ldap.auth</module>
<module>io.cloudbeaver.resources.drivers.base</module>
@@ -39,7 +39,9 @@
<plugin id="io.cloudbeaver.service.fs" download-size="0" install-size="0" version="0.0.0" unpack="false"/>
<plugin id="io.cloudbeaver.service.rm" download-size="0" install-size="0" version="0.0.0" unpack="false"/>
<plugin id="io.cloudbeaver.service.rm.nio" download-size="0" install-size="0" version="0.0.0" unpack="false"/>
<plugin id="io.cloudbeaver.service.security" download-size="0" install-size="0" version="0.0.0" unpack="false"/>
<plugin id="io.cloudbeaver.service.ldap.auth" download-size="0" install-size="0" version="0.0.0" unpack="false"/>
<!-- Extra plugins -->
<plugin id="org.jkiss.dbeaver.data.office" download-size="0" install-size="0" version="0.0.0" unpack="false"/>