mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Added LDAP authentication support. Fixes #2186
This commit is contained in:
parent
8ceeb39268
commit
f77aa3284f
@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
|
||||
New features
|
||||
************
|
||||
|
||||
| `Issue #2186 <https://redmine.postgresql.org/issues/2186>`_ - Added LDAP authentication support.
|
||||
| `Issue #5184 <https://redmine.postgresql.org/issues/5184>`_ - Added support for parameter toast_tuple_target and parallel_workers of the table.
|
||||
| `Issue #5264 <https://redmine.postgresql.org/issues/5264>`_ - Added support of Packages, Sequences and Synonyms to the Schema Diff.
|
||||
| `Issue #5353 <https://redmine.postgresql.org/issues/5353>`_ - Added an option to prevent a browser tab being opened at startup.
|
||||
|
@ -39,3 +39,4 @@ python-dateutil>=2.8.0
|
||||
SQLAlchemy>=1.3.13
|
||||
Flask-Security-Too>=3.0.0
|
||||
sshtunnel>=0.1.4
|
||||
ldap3>=2.5.1
|
||||
|
@ -488,6 +488,65 @@ MASTER_PASSWORD_REQUIRED = True
|
||||
##########################################################################
|
||||
ENHANCED_COOKIE_PROTECTION = True
|
||||
|
||||
##########################################################################
|
||||
# External Authentication Sources
|
||||
##########################################################################
|
||||
|
||||
# Default setting is internal
|
||||
# External Supported Sources: ldap
|
||||
# Multiple authentication can be achieved by setting this parameter to
|
||||
# ['ldap', 'internal']. pgAdmin will authenticate the user with ldap first,
|
||||
# in case of failure internal authentication will be done.
|
||||
|
||||
AUTHENTICATION_SOURCES = ['internal']
|
||||
|
||||
##########################################################################
|
||||
# LDAP Configuration
|
||||
##########################################################################
|
||||
|
||||
# After ldap authentication, user will be added into the SQLite database
|
||||
# automatically, if set to True.
|
||||
# Set it to False, if user should not be added automatically,
|
||||
# in this case Admin has to add the user manually in the SQLite database.
|
||||
|
||||
LDAP_AUTO_CREATE_USER = True
|
||||
|
||||
# Connection timeout
|
||||
LDAP_CONNECTION_TIMEOUT = 10
|
||||
|
||||
# Server connection details (REQUIRED)
|
||||
# example: ldap://<ip-address>:<port> or ldap://<hostname>:<port>
|
||||
LDAP_SERVER_URI = 'ldap://<ip-address>:<port>'
|
||||
|
||||
# BaseDN (REQUIRED)
|
||||
# AD example:
|
||||
# (&(objectClass=user)(memberof=CN=MYGROUP,CN=Users,dc=example,dc=com))
|
||||
# OpenLDAP example: CN=Users,dc=example,dc=com
|
||||
LDAP_BASE_DN = '<Base-DN>'
|
||||
|
||||
# The LDAP attribute containing user names. In OpenLDAP, this may be 'uid'
|
||||
# whilst in AD, 'sAMAccountName' might be appropriate. (REQUIRED)
|
||||
LDAP_USERNAME_ATTRIBUTE = '<User-id>'
|
||||
|
||||
# Search ldap for further authentication
|
||||
LDAP_SEARCH_BASE_DN = '<Search-Base-DN>'
|
||||
|
||||
# Filter string for the user search.
|
||||
# For OpenLDAP, '(cn=*)' may well be enough.
|
||||
# For AD, you might use '(objectClass=user)' (REQUIRED)
|
||||
LDAP_SEARCH_FILTER = '(objectclass=*)'
|
||||
|
||||
# Search scope for users (one of BASE, LEVEL or SUBTREE)
|
||||
LDAP_SEARCH_SCOPE = 'SUBTREE'
|
||||
|
||||
# Use TLS? If the URI scheme is ldaps://, this is ignored.
|
||||
LDAP_USE_STARTTLS = False
|
||||
|
||||
# TLS/SSL certificates. Specify if required, otherwise leave empty
|
||||
LDAP_CA_CERT_FILE = ''
|
||||
LDAP_CERT_FILE = ''
|
||||
LDAP_KEY_FILE = ''
|
||||
|
||||
##########################################################################
|
||||
# Local config settings
|
||||
##########################################################################
|
||||
|
51
web/migrations/versions/7fedf8531802_.py
Normal file
51
web/migrations/versions/7fedf8531802_.py
Normal file
@ -0,0 +1,51 @@
|
||||
|
||||
"""empty message
|
||||
|
||||
Revision ID: 7fedf8531802
|
||||
Revises: aff1436e3c8c
|
||||
Create Date: 2020-02-26 11:24:54.353288
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from pgadmin.model import db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7fedf8531802'
|
||||
down_revision = 'aff1436e3c8c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
db.engine.execute("ALTER TABLE user RENAME TO user_old")
|
||||
|
||||
db.engine.execute("""
|
||||
CREATE TABLE user (
|
||||
id INTEGER NOT NULL,
|
||||
username VARCHAR(256) NOT NULL,
|
||||
email VARCHAR(256),
|
||||
password VARCHAR(256),
|
||||
active BOOLEAN NOT NULL,
|
||||
confirmed_at DATETIME,
|
||||
masterpass_check VARCHAR(256),
|
||||
auth_source VARCHAR(256) NOT NULL DEFAULT 'internal',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (username, auth_source),
|
||||
CHECK (active IN (0, 1))
|
||||
);
|
||||
""")
|
||||
|
||||
db.engine.execute("""
|
||||
INSERT INTO user (
|
||||
id, username, email, password, active, confirmed_at, masterpass_check
|
||||
) SELECT
|
||||
id, email, email, password, active, confirmed_at, masterpass_check
|
||||
FROM user_old""")
|
||||
|
||||
db.engine.execute("DROP TABLE user_old")
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -160,6 +160,18 @@ if 'PGADMIN_INT_KEY' in globals():
|
||||
else:
|
||||
app.PGADMIN_INT_KEY = ''
|
||||
|
||||
# Authentication sources
|
||||
app.PGADMIN_DEFAULT_AUTH_SOURCE = 'internal'
|
||||
app.PGADMIN_SUPPORTED_AUTH_SOURCE = ['internal', 'ldap']
|
||||
|
||||
if len(config.AUTHENTICATION_SOURCES) > 0:
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE = config.AUTHENTICATION_SOURCES[0]
|
||||
else:
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE = app.PGADMIN_DEFAULT_AUTH_SOURCE
|
||||
|
||||
app.logger.debug(
|
||||
"Authentication Source: %s" % app.PGADMIN_DEFAULT_AUTH_SOURCE)
|
||||
|
||||
# Output a startup message if we're not under the runtime and startup.
|
||||
# If we're under WSGI, we don't need to worry about this
|
||||
if __name__ == '__main__':
|
||||
|
@ -38,7 +38,7 @@ from datetime import timedelta
|
||||
from pgadmin.setup import get_version, set_version
|
||||
from pgadmin.utils.ajax import internal_server_error
|
||||
from pgadmin.utils.csrf import pgCSRFProtect
|
||||
|
||||
from pgadmin import authenticate
|
||||
|
||||
# If script is running under python3, it will not have the xrange function
|
||||
# defined
|
||||
@ -398,6 +398,7 @@ def create_app(app_name=None):
|
||||
# Load all available server drivers
|
||||
##########################################################################
|
||||
driver.init_app(app)
|
||||
authenticate.init_app(app)
|
||||
|
||||
##########################################################################
|
||||
# Register language to the preferences after login
|
||||
|
156
web/pgadmin/authenticate/__init__.py
Normal file
156
web/pgadmin/authenticate/__init__.py
Normal file
@ -0,0 +1,156 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""A blueprint module implementing the Authentication."""
|
||||
|
||||
import flask
|
||||
import pickle
|
||||
from flask import current_app, flash
|
||||
from flask_babelex import gettext
|
||||
from flask_security import current_user
|
||||
from flask_security.views import _security, _ctx
|
||||
from flask_security.utils import config_value, get_post_logout_redirect
|
||||
from flask import session
|
||||
|
||||
import config
|
||||
from pgadmin.utils import PgAdminModule
|
||||
from .registry import AuthSourceRegistry
|
||||
|
||||
MODULE_NAME = 'authenticate'
|
||||
|
||||
|
||||
class AuthenticateModule(PgAdminModule):
|
||||
def get_exposed_url_endpoints(self):
|
||||
return ['authenticate.login']
|
||||
|
||||
|
||||
blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='')
|
||||
|
||||
|
||||
@blueprint.route('/login', endpoint='login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""
|
||||
Entry point for all the authentication sources.
|
||||
The user input will be validated and authenticated.
|
||||
"""
|
||||
form = _security.login_form()
|
||||
auth_obj = AuthSourceManager(form, config.AUTHENTICATION_SOURCES)
|
||||
session['_auth_source_manager_obj'] = None
|
||||
|
||||
# Validate the user
|
||||
if not auth_obj.validate():
|
||||
for field in form.errors:
|
||||
for error in form.errors[field]:
|
||||
flash(error, 'warning')
|
||||
return flask.redirect(get_post_logout_redirect())
|
||||
|
||||
# Authenticate the user
|
||||
status, msg = auth_obj.authenticate()
|
||||
if status:
|
||||
# Login the user
|
||||
status, msg = auth_obj.login()
|
||||
if not status:
|
||||
flash(gettext(msg), 'danger')
|
||||
return flask.redirect(get_post_logout_redirect())
|
||||
|
||||
session['_auth_source_manager_obj'] = auth_obj.as_dict()
|
||||
return flask.redirect('/')
|
||||
|
||||
flash(gettext(msg), 'danger')
|
||||
return flask.redirect(get_post_logout_redirect())
|
||||
|
||||
|
||||
class AuthSourceManager():
|
||||
"""This class will manage all the authentication sources.
|
||||
"""
|
||||
def __init__(self, form, sources):
|
||||
self.form = form
|
||||
self.auth_sources = sources
|
||||
self.source = None
|
||||
self.source_friendly_name = None
|
||||
|
||||
def as_dict(self):
|
||||
"""
|
||||
Returns the dictionary object representing this object.
|
||||
"""
|
||||
|
||||
res = dict()
|
||||
res['source_friendly_name'] = self.source_friendly_name
|
||||
res['auth_sources'] = self.auth_sources
|
||||
|
||||
return res
|
||||
|
||||
def set_source(self, source):
|
||||
self.source = source
|
||||
|
||||
@property
|
||||
def get_source(self):
|
||||
return self.source
|
||||
|
||||
def set_source_friendly_name(self, name):
|
||||
self.source_friendly_name = name
|
||||
|
||||
@property
|
||||
def get_source_friendly_name(self):
|
||||
return self.source_friendly_name
|
||||
|
||||
def validate(self):
|
||||
"""Validate through all the sources."""
|
||||
for src in self.auth_sources:
|
||||
source = get_auth_sources(src)
|
||||
if source.validate(self.form):
|
||||
return True
|
||||
return False
|
||||
|
||||
def authenticate(self):
|
||||
"""Authenticate through all the sources."""
|
||||
status = False
|
||||
msg = None
|
||||
for src in self.auth_sources:
|
||||
source = get_auth_sources(src)
|
||||
status, msg = source.authenticate(self.form)
|
||||
if status:
|
||||
self.set_source(source)
|
||||
return status, msg
|
||||
return status, msg
|
||||
|
||||
def login(self):
|
||||
status, msg = self.source.login(self.form)
|
||||
if status:
|
||||
self.set_source_friendly_name(self.source.get_friendly_name())
|
||||
return status, msg
|
||||
|
||||
|
||||
def get_auth_sources(type):
|
||||
"""Get the authenticated source object from the registry"""
|
||||
|
||||
auth_sources = getattr(current_app, '_pgadmin_auth_sources', None)
|
||||
|
||||
if auth_sources is None or not isinstance(auth_sources, dict):
|
||||
auth_sources = dict()
|
||||
|
||||
if type in auth_sources:
|
||||
return auth_sources[type]
|
||||
|
||||
auth_source = AuthSourceRegistry.create(type)
|
||||
|
||||
if auth_source is not None:
|
||||
auth_sources[type] = auth_source
|
||||
setattr(current_app, '_pgadmin_auth_sources', auth_sources)
|
||||
|
||||
return auth_source
|
||||
|
||||
|
||||
def init_app(app):
|
||||
auth_sources = dict()
|
||||
|
||||
setattr(app, '_pgadmin_auth_sources', auth_sources)
|
||||
AuthSourceRegistry.load_auth_sources()
|
||||
|
||||
return auth_sources
|
98
web/pgadmin/authenticate/internal.py
Normal file
98
web/pgadmin/authenticate/internal.py
Normal file
@ -0,0 +1,98 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""Implements Internal Authentication"""
|
||||
|
||||
import six
|
||||
from flask import current_app
|
||||
from flask_security import login_user
|
||||
from abc import abstractmethod, abstractproperty
|
||||
from flask_babelex import gettext
|
||||
|
||||
from .registry import AuthSourceRegistry
|
||||
from pgadmin.model import User
|
||||
|
||||
|
||||
@six.add_metaclass(AuthSourceRegistry)
|
||||
class BaseAuthentication(object):
|
||||
|
||||
DEFAULT_MSG = {
|
||||
'USER_DOES_NOT_EXIST': 'Specified user does not exist',
|
||||
'LOGIN_FAILED': 'Login failed',
|
||||
'EMAIL_NOT_PROVIDED': 'Email/Username not provided',
|
||||
'PASSWORD_NOT_PROVIDED': 'Password not provided'
|
||||
}
|
||||
|
||||
@abstractproperty
|
||||
def get_friendly_name(cls):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def authenticate(cls):
|
||||
pass
|
||||
|
||||
def validate(self, form):
|
||||
username = form.data['email']
|
||||
password = form.data['password']
|
||||
|
||||
if username is None or username == '':
|
||||
form.email.errors = list(form.email.errors)
|
||||
form.email.errors.append(gettext(
|
||||
self.messages('EMAIL_NOT_PROVIDED')))
|
||||
return False
|
||||
if password is None or password == '':
|
||||
form.password.errors = list(form.password.errors)
|
||||
form.password.errors.append(
|
||||
self.messages('PASSWORD_NOT_PROVIDED'))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def login(self, form):
|
||||
username = form.data['email']
|
||||
user = getattr(form, 'user', None)
|
||||
|
||||
if user is None:
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if user is None:
|
||||
current_app.logger.exception(
|
||||
self.messages('USER_DOES_NOT_EXIST'))
|
||||
return False, self.messages('USER_DOES_NOT_EXIST')
|
||||
|
||||
# Login user through flask_security
|
||||
status = login_user(user)
|
||||
if not status:
|
||||
current_app.logger.exception(self.messages('LOGIN_FAILED'))
|
||||
return False, self.messages('LOGIN_FAILED')
|
||||
return True, None
|
||||
|
||||
def messages(self, msg_key):
|
||||
return self.DEFAULT_MSG[msg_key] if msg_key in self.DEFAULT_MSG\
|
||||
else None
|
||||
|
||||
|
||||
class InternalAuthentication(BaseAuthentication):
|
||||
|
||||
def get_friendly_name(cls):
|
||||
return gettext("internal")
|
||||
|
||||
def validate(self, form):
|
||||
"""User validation"""
|
||||
|
||||
# Flask security validation
|
||||
return form.validate_on_submit()
|
||||
|
||||
def authenticate(self, form):
|
||||
username = form.data['email']
|
||||
user = getattr(form, 'user',
|
||||
User.query.filter_by(username=username).first())
|
||||
if user and user.is_authenticated and form.validate_on_submit():
|
||||
return True, None
|
||||
return False, self.messages('USER_DOES_NOT_EXIST')
|
186
web/pgadmin/authenticate/ldap.py
Normal file
186
web/pgadmin/authenticate/ldap.py
Normal file
@ -0,0 +1,186 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""A blueprint module implementing the ldap authentication."""
|
||||
|
||||
import ssl
|
||||
import config
|
||||
from ldap3 import Connection, Server, Tls, ALL, ALL_ATTRIBUTES
|
||||
from ldap3.core.exceptions import LDAPSocketOpenError, LDAPBindError,\
|
||||
LDAPInvalidScopeError, LDAPAttributeError, LDAPInvalidFilterError,\
|
||||
LDAPStartTLSError
|
||||
from flask_babelex import gettext
|
||||
|
||||
from .internal import BaseAuthentication
|
||||
from pgadmin.model import User, ServerGroup, db, Role
|
||||
from flask_security import login_user
|
||||
from flask import current_app
|
||||
from pgadmin.tools.user_management import create_user
|
||||
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
except ImportError:
|
||||
from urlparse import urlparse
|
||||
|
||||
|
||||
class LDAPAuthentication(BaseAuthentication):
|
||||
"""Ldap Authentication Class"""
|
||||
|
||||
def get_friendly_name(self):
|
||||
return gettext("ldap")
|
||||
|
||||
def authenticate(self, form):
|
||||
self.username = form.data['email']
|
||||
self.password = form.data['password']
|
||||
|
||||
status, msg = self.connect()
|
||||
|
||||
if not status:
|
||||
return status, msg
|
||||
|
||||
status, user_email = self.search_ldap_user()
|
||||
|
||||
if not status:
|
||||
return status, user_email
|
||||
|
||||
return self.__auto_create_user(user_email)
|
||||
|
||||
def connect(self):
|
||||
"""Setup the connection to the LDAP server and authenticate the user.
|
||||
"""
|
||||
|
||||
# Parse the server URI
|
||||
uri = getattr(config, 'LDAP_SERVER_URI', None)
|
||||
|
||||
if uri:
|
||||
uri = urlparse(uri)
|
||||
|
||||
# Create the TLS configuration object if required
|
||||
tls = None
|
||||
|
||||
if type(uri) == str:
|
||||
return False, "LDAP configuration error: Set the proper LDAP URI."
|
||||
|
||||
if uri.scheme == 'ldaps' or config.LDAP_USE_STARTTLS:
|
||||
|
||||
ca_cert_file = getattr(config, 'LDAP_CA_CERT_FILE', None)
|
||||
cert_file = getattr(config, 'LDAP_CERT_FILE', None)
|
||||
key_file = getattr(config, 'LDAP_KEY_FILE', None)
|
||||
cert_validate = ssl.CERT_NONE
|
||||
|
||||
if ca_cert_file and cert_file and key_file:
|
||||
cert_validate = ssl.CERT_REQUIRED
|
||||
|
||||
tls = Tls(
|
||||
local_private_key_file=key_file,
|
||||
local_certificate_file=cert_file,
|
||||
validate=cert_validate,
|
||||
version=ssl.PROTOCOL_TLSv1,
|
||||
ca_certs_file=ca_cert_file)
|
||||
|
||||
try:
|
||||
# Create the server object
|
||||
server = Server(uri.hostname,
|
||||
port=uri.port,
|
||||
use_ssl=(uri.scheme == 'ldaps'),
|
||||
get_info=ALL,
|
||||
tls=tls,
|
||||
connect_timeout=config.LDAP_CONNECTION_TIMEOUT)
|
||||
except ValueError as e:
|
||||
return False, "LDAP configuration error: %s." % e
|
||||
|
||||
# Create the connection
|
||||
try:
|
||||
user_dn = "{0}={1},{2}".format(config.LDAP_USERNAME_ATTRIBUTE,
|
||||
self.username,
|
||||
config.LDAP_BASE_DN
|
||||
)
|
||||
self.conn = Connection(server,
|
||||
user=user_dn,
|
||||
password=self.password,
|
||||
auto_bind=True
|
||||
)
|
||||
|
||||
except LDAPSocketOpenError as e:
|
||||
current_app.logger.exception(
|
||||
"Error connecting to the LDAP server: %s\n" % e)
|
||||
return False, "Error connecting to the LDAP server:" \
|
||||
" %s\n" % e.args[0]
|
||||
except LDAPBindError as e:
|
||||
current_app.logger.exception(
|
||||
"Error binding to the LDAP server.")
|
||||
return False, "Error binding to the LDAP server."
|
||||
except Exception as e:
|
||||
current_app.logger.exception(
|
||||
"Error connecting to the LDAP server: %s\n" % e)
|
||||
return False, "Error connecting to the LDAP server:" \
|
||||
" %s\n" % e.args[0]
|
||||
|
||||
# Enable TLS if STARTTLS is configured
|
||||
if not uri.scheme == 'ldaps' and config.LDAP_USE_STARTTLS:
|
||||
try:
|
||||
self.conn.start_tls()
|
||||
except LDAPStartTLSError as e:
|
||||
current_app.logger.exception(
|
||||
"Error starting TLS: %s\n" % e)
|
||||
return False, "Error starting TLS: %s\n" % e.args[0]
|
||||
|
||||
return True, None
|
||||
|
||||
def __auto_create_user(self, user_email):
|
||||
"""Add the ldap user to the internal SQLite database."""
|
||||
if config.LDAP_AUTO_CREATE_USER:
|
||||
user = User.query.filter_by(
|
||||
username=self.username).first()
|
||||
if user is None:
|
||||
return create_user({
|
||||
'username': self.username,
|
||||
'email': user_email,
|
||||
'role': 2,
|
||||
'active': True,
|
||||
'auth_source': 'ldap'
|
||||
})
|
||||
|
||||
return True, None
|
||||
|
||||
def search_ldap_user(self):
|
||||
"""Get a list of users from the LDAP server based on config
|
||||
search criteria."""
|
||||
try:
|
||||
self.conn.search(search_base=config.LDAP_SEARCH_BASE_DN,
|
||||
search_filter=config.LDAP_SEARCH_FILTER,
|
||||
search_scope=config.LDAP_SEARCH_SCOPE,
|
||||
attributes=ALL_ATTRIBUTES
|
||||
)
|
||||
|
||||
except LDAPInvalidScopeError as e:
|
||||
current_app.logger.exception(
|
||||
"Error searching the LDAP directory: %s\n" % e)
|
||||
return False, "Error searching the LDAP directory:" \
|
||||
" %s\n" % e.args[0]
|
||||
except LDAPAttributeError as e:
|
||||
current_app.logger.exception("Error searching the LDAP directory:"
|
||||
" %s\n" % e)
|
||||
return False, "Error searching the LDAP directory:" \
|
||||
" %s\n" % e.args[0]
|
||||
except LDAPInvalidFilterError as e:
|
||||
current_app.logger.exception(
|
||||
"Error searching the LDAP directory: %s\n" % e)
|
||||
return False, "Error searching the LDAP directory:" \
|
||||
" %s\n" % e.args[0]
|
||||
|
||||
users = []
|
||||
for entry in self.conn.entries:
|
||||
user_email = None
|
||||
if config.LDAP_USERNAME_ATTRIBUTE in entry and self.username == \
|
||||
entry[config.LDAP_USERNAME_ATTRIBUTE].value:
|
||||
if 'mail' in entry:
|
||||
user_email = entry['mail'].value
|
||||
return True, user_email
|
||||
return False, None
|
65
web/pgadmin/authenticate/registry.py
Normal file
65
web/pgadmin/authenticate/registry.py
Normal file
@ -0,0 +1,65 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""External Authentication Registry."""
|
||||
|
||||
|
||||
from flask_babelex import gettext
|
||||
from abc import ABCMeta
|
||||
|
||||
|
||||
def _decorate_cls_name(module_name):
|
||||
length = len(__package__) + 1
|
||||
|
||||
if len(module_name) > length and module_name.startswith(__package__):
|
||||
return module_name[length:]
|
||||
|
||||
return module_name
|
||||
|
||||
|
||||
class AuthSourceRegistry(ABCMeta):
|
||||
registry = None
|
||||
auth_sources = dict()
|
||||
|
||||
def __init__(cls, name, bases, d):
|
||||
|
||||
# Register this type of auth_sources, based on the module name
|
||||
# Avoid registering the BaseAuthentication itself
|
||||
|
||||
AuthSourceRegistry.registry[_decorate_cls_name(d['__module__'])] = cls
|
||||
ABCMeta.__init__(cls, name, bases, d)
|
||||
|
||||
@classmethod
|
||||
def create(cls, name, **kwargs):
|
||||
|
||||
if name in AuthSourceRegistry.auth_sources:
|
||||
return AuthSourceRegistry.auth_sources[name]
|
||||
|
||||
if name in AuthSourceRegistry.registry:
|
||||
AuthSourceRegistry.auth_sources[name] = \
|
||||
(AuthSourceRegistry.registry[name])(**kwargs)
|
||||
return AuthSourceRegistry.auth_sources[name]
|
||||
|
||||
raise NotImplementedError(
|
||||
gettext(
|
||||
"Authentication source '{0}' has not been implemented."
|
||||
).format(name)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_auth_sources(cls):
|
||||
# Initialize the registry only if it has not yet been initialized
|
||||
if AuthSourceRegistry.registry is None:
|
||||
AuthSourceRegistry.registry = dict()
|
||||
|
||||
from importlib import import_module
|
||||
from werkzeug.utils import find_modules
|
||||
|
||||
for module_name in find_modules(__package__, True):
|
||||
module = import_module(module_name)
|
@ -45,6 +45,7 @@ from pgadmin.browser.register_browser_preferences import \
|
||||
from pgadmin.utils.master_password import validate_master_password, \
|
||||
set_masterpass_check_text, cleanup_master_password, get_crypt_key, \
|
||||
set_crypt_key, process_masterpass_disabled
|
||||
from pgadmin.model import User
|
||||
|
||||
try:
|
||||
import urllib.request as urlreq
|
||||
@ -580,12 +581,24 @@ def index():
|
||||
|
||||
flash(msg, 'warning')
|
||||
|
||||
auth_only_internal = False
|
||||
auth_source = []
|
||||
|
||||
if config.SERVER_MODE:
|
||||
if len(config.AUTHENTICATION_SOURCES) == 1\
|
||||
and 'internal' in config.AUTHENTICATION_SOURCES:
|
||||
auth_only_internal = True
|
||||
auth_source = session['_auth_source_manager_obj'][
|
||||
'source_friendly_name']
|
||||
|
||||
response = Response(render_template(
|
||||
MODULE_NAME + "/index.html",
|
||||
username=current_user.email,
|
||||
username=current_user.username,
|
||||
auth_source=auth_source,
|
||||
is_admin=current_user.has_role("Administrator"),
|
||||
logout_url=_get_logout_url(),
|
||||
_=gettext
|
||||
_=gettext,
|
||||
auth_only_internal=auth_only_internal
|
||||
))
|
||||
|
||||
# Set the language cookie after login, so next time the user will have that
|
||||
@ -994,43 +1007,60 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
||||
form = form_class()
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
send_reset_password_instructions(form.user)
|
||||
except SOCKETErrorException as e:
|
||||
# Handle socket errors which are not covered by SMTPExceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(u'SMTP Socket error: {}\n'
|
||||
u'Your password has not been changed.'
|
||||
).format(e),
|
||||
'danger')
|
||||
has_error = True
|
||||
except (SMTPConnectError, SMTPResponseException,
|
||||
SMTPServerDisconnected, SMTPDataError, SMTPHeloError,
|
||||
SMTPException, SMTPAuthenticationError, SMTPSenderRefused,
|
||||
SMTPRecipientsRefused) as e:
|
||||
# Check the Authentication source of the User
|
||||
user = User.query.filter_by(
|
||||
email=form.data['email'],
|
||||
auth_source=current_app.PGADMIN_DEFAULT_AUTH_SOURCE
|
||||
).first()
|
||||
|
||||
# Handle smtp specific exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(u'SMTP error: {}\n'
|
||||
u'Your password has not been changed.'
|
||||
).format(e),
|
||||
'danger')
|
||||
has_error = True
|
||||
except Exception as e:
|
||||
# Handle other exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(u'Error: {}\n'
|
||||
u'Your password has not been changed.'
|
||||
).format(e),
|
||||
if user is None:
|
||||
# If the user is not an internal user, raise the exception
|
||||
flash(gettext('Your account is authenticated using an '
|
||||
'external {} source. '
|
||||
'Please contact the administrators of this '
|
||||
'service if you need to reset your password.'
|
||||
).format(form.user.auth_source),
|
||||
'danger')
|
||||
has_error = True
|
||||
if not has_error:
|
||||
try:
|
||||
send_reset_password_instructions(form.user)
|
||||
except SOCKETErrorException as e:
|
||||
# Handle socket errors which are not
|
||||
# covered by SMTPExceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(u'SMTP Socket error: {}\n'
|
||||
u'Your password has not been changed.'
|
||||
).format(e),
|
||||
'danger')
|
||||
has_error = True
|
||||
except (SMTPConnectError, SMTPResponseException,
|
||||
SMTPServerDisconnected, SMTPDataError, SMTPHeloError,
|
||||
SMTPException, SMTPAuthenticationError,
|
||||
SMTPSenderRefused, SMTPRecipientsRefused) as e:
|
||||
|
||||
# Handle smtp specific exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(u'SMTP error: {}\n'
|
||||
u'Your password has not been changed.'
|
||||
).format(e),
|
||||
'danger')
|
||||
has_error = True
|
||||
except Exception as e:
|
||||
# Handle other exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(u'Error: {}\n'
|
||||
u'Your password has not been changed.'
|
||||
).format(e),
|
||||
'danger')
|
||||
has_error = True
|
||||
|
||||
if request.json is None and not has_error:
|
||||
do_flash(*get_message('PASSWORD_RESET_REQUEST',
|
||||
email=form.user.email))
|
||||
|
||||
if request.json and not has_error:
|
||||
return _render_json(form, include_user=False)
|
||||
return default_render_json(form, include_user=False)
|
||||
|
||||
return _security.render_template(
|
||||
config_value('FORGOT_PASSWORD_TEMPLATE'),
|
||||
|
@ -142,6 +142,7 @@ window.onload = function(e){
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown"
|
||||
role="button" aria-expanded="false" id="navbar-user"></a>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
{% if auth_only_internal %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="pgAdmin.Browser.UserManagement.change_password(
|
||||
'{{ url_for('browser.change_password') }}'
|
||||
@ -150,6 +151,7 @@ window.onload = function(e){
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
{% endif %}
|
||||
{% if is_admin %}
|
||||
<li><a class="dropdown-item" href="#" onclick="pgAdmin.Browser.UserManagement.show_users()">{{ _('Users') }}</a></li>
|
||||
<li class="dropdown-divider"></li>
|
||||
|
@ -4,5 +4,5 @@ we will not associate our application with Gravatar module which will make
|
||||
'gravatar' filter unavailable in Jinja templates
|
||||
###########################################################################}
|
||||
{% macro PREPARE_HTML() -%}
|
||||
'<img src = "{{ username | gravatar }}" width = "18" height = "18" alt = "Gravatar image for {{ username }}" > {{ username }} <span class="caret"></span>';
|
||||
'<img src = "{{ username | gravatar }}" width = "18" height = "18" alt = "Gravatar image for {{ username }}" > {{ username }} ({{auth_source}}) <span class="caret"></span>';
|
||||
{%- endmacro %}
|
||||
|
@ -95,6 +95,7 @@ class ChangePasswordTestCase(BaseTestGenerator):
|
||||
response = self.tester.post(
|
||||
'/user_management/user/',
|
||||
data=json.dumps(dict(
|
||||
username=self.username,
|
||||
email=self.username,
|
||||
newPassword=self.password,
|
||||
confirmPassword=self.password,
|
||||
|
89
web/pgadmin/browser/tests/test_ldap_login.py
Normal file
89
web/pgadmin/browser/tests/test_ldap_login.py
Normal file
@ -0,0 +1,89 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import config as app_config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression.test_setup import config_data
|
||||
|
||||
|
||||
class LDAPLoginTestCase(BaseTestGenerator):
|
||||
"""
|
||||
This class checks ldap login functionality
|
||||
by validating different scenarios.
|
||||
"""
|
||||
|
||||
scenarios = [
|
||||
('LDAP Authentication', dict(
|
||||
config_key_param='ldap',
|
||||
is_gravtar_image_check=False)),
|
||||
('LDAP With SSL Authentication', dict(
|
||||
config_key_param='ldap_with_ssl',
|
||||
is_gravtar_image_check=False)),
|
||||
('LDAP With TLS Authentication', dict(
|
||||
config_key_param='ldap_with_tls',
|
||||
is_gravtar_image_check=False)),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
We need to logout the test client
|
||||
as we are testing ldap login scenarios.
|
||||
"""
|
||||
cls.tester.logout()
|
||||
|
||||
def setUp(self):
|
||||
if 'ldap_config' in config_data and \
|
||||
type(config_data['ldap_config']) is list and\
|
||||
len(config_data['ldap_config']) > 0 and\
|
||||
self.config_key_param in config_data['ldap_config'][0]:
|
||||
ldap_config = config_data['ldap_config'][0][self.config_key_param]
|
||||
|
||||
app_config.AUTHENTICATION_SOURCES = ['ldap']
|
||||
app_config.LDAP_AUTO_CREATE_USER = True
|
||||
app_config.LDAP_SERVER_URI = ldap_config['uri']
|
||||
app_config.LDAP_BASE_DN = ldap_config['base_dn']
|
||||
app_config.LDAP_USERNAME_ATTRIBUTE = ldap_config[
|
||||
'username_atr']
|
||||
app_config.LDAP_SEARCH_BASE_DN = ldap_config[
|
||||
'search_base_dn']
|
||||
app_config.LDAP_SEARCH_FILTER = ldap_config['search_filter']
|
||||
app_config.LDAP_USE_STARTTLS = ldap_config['use_starttls']
|
||||
app_config.LDAP_CA_CERT_FILE = ldap_config['ca_cert_file']
|
||||
app_config.LDAP_CERT_FILE = ldap_config['cert_file']
|
||||
app_config.LDAP_KEY_FILE = ldap_config['key_file']
|
||||
else:
|
||||
self.skipTest(
|
||||
"LDAP config not set."
|
||||
)
|
||||
|
||||
def runTest(self):
|
||||
"""This function checks login functionality."""
|
||||
username = config_data['pgAdmin4_ldap_credentials']['login_username']
|
||||
password = config_data['pgAdmin4_ldap_credentials']['login_password']
|
||||
|
||||
res = self.tester.login(username, password, True)
|
||||
|
||||
respdata = 'Gravatar image for %s' %\
|
||||
config_data['pgAdmin4_ldap_credentials']['login_username']
|
||||
self.assertTrue(respdata in res.data.decode('utf8'))
|
||||
|
||||
def tearDown(self):
|
||||
self.tester.logout()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""
|
||||
We need to again login the test client as soon as test scenarios
|
||||
finishes.
|
||||
"""
|
||||
cls.tester.logout()
|
||||
app_config.AUTHENTICATION_SOURCES = ['internal']
|
||||
utils.login_tester_account(cls.tester)
|
84
web/pgadmin/browser/tests/test_ldap_with_mocking.py
Normal file
84
web/pgadmin/browser/tests/test_ldap_with_mocking.py
Normal file
@ -0,0 +1,84 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import sys
|
||||
import config as app_config
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
from regression.python_test_utils import test_utils as utils
|
||||
from regression.test_setup import config_data
|
||||
from pgadmin.authenticate.registry import AuthSourceRegistry
|
||||
|
||||
if sys.version_info < (3, 3):
|
||||
from mock import patch
|
||||
else:
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class LDAPLoginMockTestCase(BaseTestGenerator):
|
||||
"""
|
||||
This class checks ldap login functionality by mocking
|
||||
ldap connection and ldap search functionality.
|
||||
"""
|
||||
|
||||
scenarios = [
|
||||
('LDAP Authentication with Auto Create User', dict(
|
||||
auth_source=['ldap'],
|
||||
auto_create_user=True,
|
||||
username='ldap_user',
|
||||
password='ldap_pass')),
|
||||
('LDAP Authentication without Auto Create User', dict(
|
||||
auth_source=['ldap'],
|
||||
auto_create_user=False,
|
||||
username='ldap_user',
|
||||
password='ldap_pass')),
|
||||
('LDAP + Internal Authentication', dict(
|
||||
auth_source=['ldap', 'internal'],
|
||||
auto_create_user=False,
|
||||
username=config_data[
|
||||
'pgAdmin4_login_credentials']['login_username'],
|
||||
password=config_data[
|
||||
'pgAdmin4_login_credentials']['login_password']
|
||||
))
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
We need to logout the test client as we are testing
|
||||
ldap login scenarios.
|
||||
"""
|
||||
cls.tester.logout()
|
||||
|
||||
def setUp(self):
|
||||
app_config.AUTHENTICATION_SOURCES = self.auth_source
|
||||
app_config.LDAP_AUTO_CREATE_USER = self.auto_create_user
|
||||
|
||||
@patch.object(AuthSourceRegistry.registry['ldap'], 'connect',
|
||||
return_value=[True, "Done"])
|
||||
@patch.object(AuthSourceRegistry.registry['ldap'], 'search_ldap_user',
|
||||
return_value=[True, ''])
|
||||
def runTest(self, conn_mock_obj, search_mock_obj):
|
||||
"""This function checks ldap login functionality."""
|
||||
|
||||
res = self.tester.login(self.username, self.password, True)
|
||||
respdata = 'Gravatar image for %s' % self.username
|
||||
self.assertTrue(respdata in res.data.decode('utf8'))
|
||||
|
||||
def tearDown(self):
|
||||
self.tester.logout()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""
|
||||
We need to again login the test client as soon as test scenarios
|
||||
finishes.
|
||||
"""
|
||||
cls.tester.logout()
|
||||
app_config.AUTHENTICATION_SOURCES = ['internal']
|
||||
utils.login_tester_account(cls.tester)
|
@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
SCHEMA_VERSION = 24
|
||||
SCHEMA_VERSION = 25
|
||||
|
||||
##########################################################################
|
||||
#
|
||||
@ -66,13 +66,15 @@ class User(db.Model, UserMixin):
|
||||
"""Define a user object"""
|
||||
__tablename__ = 'user'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(256), unique=True, nullable=False)
|
||||
email = db.Column(db.String(256), nullable=True)
|
||||
username = db.Column(db.String(64), unique=True, nullable=False)
|
||||
password = db.Column(db.String(256))
|
||||
active = db.Column(db.Boolean(), nullable=False)
|
||||
confirmed_at = db.Column(db.DateTime())
|
||||
masterpass_check = db.Column(db.String(256))
|
||||
roles = db.relationship('Role', secondary=roles_users,
|
||||
backref=db.backref('users', lazy='dynamic'))
|
||||
auth_source = db.Column(db.String(16), unique=True, nullable=False)
|
||||
|
||||
|
||||
class Setting(db.Model):
|
||||
|
@ -9,3 +9,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
{% macro render_username_with_errors(field, type) %}
|
||||
<div class="form-group mb-3 {% if field.errors %} has-error{% endif %}">
|
||||
<input class="form-control" placeholder="{{ field.label.text }} / Username" name="{{ field.name }}"
|
||||
type="{% if type %}{{ type }}{% else %}{{ field.type }}{% endif %}" autofocus>
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
<span class="form-text">{{ error }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
@ -7,10 +7,10 @@
|
||||
{% block panel_title %}{{ _('Login') }}{% endblock %}
|
||||
{% block panel_body %}
|
||||
{% if config.SERVER_MODE %}
|
||||
<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
|
||||
<form action="{{ url_for('authenticate.login') }}" method="POST" name="login_user_form">
|
||||
{{ login_user_form.hidden_tag() }}
|
||||
{% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %}
|
||||
{{ render_field_with_errors(login_user_form.email, "text") }}
|
||||
{{ render_username_with_errors(login_user_form.email, "text") }}
|
||||
{{ render_field_with_errors(login_user_form.password, "password") }}
|
||||
<button class="btn btn-primary btn-block btn-login" type="submit" value="{{ _('Login') }}">{{ _('Login') }}</button>
|
||||
<div class="form-group row mb-3 c user-language">
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "security/fields.html" import render_field_with_errors %}
|
||||
{% from "security/fields.html" import render_field_with_errors, render_username_with_errors %}
|
||||
{% block body %}
|
||||
<div class="container-fluid h-100 login_page">
|
||||
{% if config.LOGIN_BANNER is defined and config.LOGIN_BANNER != "" %}
|
||||
|
@ -74,7 +74,8 @@ class UserManagementModule(PgAdminModule):
|
||||
'user_management.roles', 'user_management.role',
|
||||
'user_management.update_user', 'user_management.delete_user',
|
||||
'user_management.create_user', 'user_management.users',
|
||||
'user_management.user', current_app.login_manager.login_view
|
||||
'user_management.user', current_app.login_manager.login_view,
|
||||
'user_management.auth_sources', 'user_management.auth_sources'
|
||||
]
|
||||
|
||||
|
||||
@ -100,7 +101,7 @@ def validate_user(data):
|
||||
else:
|
||||
raise Exception(_("Passwords do not match."))
|
||||
|
||||
if 'email' in data and data['email'] != "":
|
||||
if 'email' in data and data['email'] and data['email'] != "":
|
||||
if email_filter.match(data['email']):
|
||||
new_data['email'] = data['email']
|
||||
else:
|
||||
@ -112,6 +113,12 @@ def validate_user(data):
|
||||
if 'active' in data and data['active'] != "":
|
||||
new_data['active'] = data['active']
|
||||
|
||||
if 'username' in data and data['username'] != "":
|
||||
new_data['username'] = data['username']
|
||||
|
||||
if 'auth_source' in data and data['auth_source'] != "":
|
||||
new_data['auth_source'] = data['auth_source']
|
||||
|
||||
return new_data
|
||||
|
||||
|
||||
@ -140,6 +147,7 @@ def script():
|
||||
@pgCSRFProtect.exempt
|
||||
@login_required
|
||||
def current_user_info():
|
||||
|
||||
return Response(
|
||||
response=render_template(
|
||||
"user_management/js/current_user.js",
|
||||
@ -148,13 +156,15 @@ def current_user_info():
|
||||
user_id=current_user.id,
|
||||
email=current_user.email,
|
||||
name=(
|
||||
current_user.email.split('@')[0] if config.SERVER_MODE is True
|
||||
current_user.username.split('@')[0] if
|
||||
config.SERVER_MODE is True
|
||||
else 'postgres'
|
||||
),
|
||||
allow_save_password='true' if config.ALLOW_SAVE_PASSWORD
|
||||
else 'false',
|
||||
allow_save_tunnel_password='true'
|
||||
if config.ALLOW_SAVE_TUNNEL_PASSWORD else 'false',
|
||||
auth_sources=config.AUTHENTICATION_SOURCES,
|
||||
),
|
||||
status=200,
|
||||
mimetype="application/javascript"
|
||||
@ -180,9 +190,11 @@ def user(uid):
|
||||
u = User.query.get(uid)
|
||||
|
||||
res = {'id': u.id,
|
||||
'username': u.username,
|
||||
'email': u.email,
|
||||
'active': u.active,
|
||||
'role': u.roles[0].id
|
||||
'role': u.roles[0].id,
|
||||
'auth_source': u.auth_source
|
||||
}
|
||||
else:
|
||||
users = User.query.all()
|
||||
@ -190,9 +202,11 @@ def user(uid):
|
||||
users_data = []
|
||||
for u in users:
|
||||
users_data.append({'id': u.id,
|
||||
'username': u.username,
|
||||
'email': u.email,
|
||||
'active': u.active,
|
||||
'role': u.roles[0].id
|
||||
'role': u.roles[0].id,
|
||||
'auth_source': u.auth_source
|
||||
})
|
||||
|
||||
res = users_data
|
||||
@ -215,11 +229,29 @@ def create():
|
||||
request.data, encoding='utf-8'
|
||||
)
|
||||
|
||||
for f in ('email', 'role', 'active', 'newPassword', 'confirmPassword'):
|
||||
status, res = create_user(data)
|
||||
|
||||
if not status:
|
||||
return internal_server_error(errormsg=res)
|
||||
|
||||
return ajax_response(
|
||||
response=res,
|
||||
status=200
|
||||
)
|
||||
|
||||
|
||||
def create_user(data):
|
||||
if 'auth_source' in data and data['auth_source'] != 'internal':
|
||||
req_params = ('username', 'role', 'active', 'auth_source')
|
||||
else:
|
||||
req_params = ('email', 'role', 'active', 'newPassword',
|
||||
'confirmPassword')
|
||||
|
||||
for f in req_params:
|
||||
if f in data and data[f] != '':
|
||||
continue
|
||||
else:
|
||||
return bad_request(errormsg=_("Missing field: '{0}'".format(f)))
|
||||
return False, _("Missing field: '{0}'".format(f))
|
||||
|
||||
try:
|
||||
new_data = validate_user(data)
|
||||
@ -228,13 +260,23 @@ def create():
|
||||
new_data['roles'] = [Role.query.get(new_data['roles'])]
|
||||
|
||||
except Exception as e:
|
||||
return bad_request(errormsg=_(str(e)))
|
||||
return False, str(e)
|
||||
|
||||
try:
|
||||
usr = User(email=new_data['email'],
|
||||
|
||||
username = new_data['username'] if 'username' in new_data \
|
||||
else new_data['email']
|
||||
email = new_data['email'] if 'email' in new_data else None
|
||||
password = new_data['password'] if 'password' in new_data else None
|
||||
auth_source = new_data['auth_source'] if 'auth_source' in new_data \
|
||||
else current_app.PGADMIN_DEFAULT_AUTH_SOURCE
|
||||
|
||||
usr = User(username=username,
|
||||
email=email,
|
||||
roles=new_data['roles'],
|
||||
active=new_data['active'],
|
||||
password=new_data['password'])
|
||||
password=password,
|
||||
auth_source=auth_source)
|
||||
db.session.add(usr)
|
||||
db.session.commit()
|
||||
# Add default server group for new user.
|
||||
@ -242,18 +284,15 @@ def create():
|
||||
db.session.add(server_group)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
return internal_server_error(errormsg=str(e))
|
||||
return False, str(e)
|
||||
|
||||
res = {'id': usr.id,
|
||||
'email': usr.email,
|
||||
'active': usr.active,
|
||||
'role': usr.roles[0].id
|
||||
}
|
||||
|
||||
return ajax_response(
|
||||
response=res,
|
||||
status=200
|
||||
)
|
||||
return True, {
|
||||
'id': usr.id,
|
||||
'username': usr.username,
|
||||
'email': usr.email,
|
||||
'active': usr.active,
|
||||
'role': usr.roles[0].id
|
||||
}
|
||||
|
||||
|
||||
@blueprint.route(
|
||||
@ -337,9 +376,11 @@ def update(uid):
|
||||
db.session.commit()
|
||||
|
||||
res = {'id': usr.id,
|
||||
'username': usr.username,
|
||||
'email': usr.email,
|
||||
'active': usr.active,
|
||||
'role': usr.roles[0].id
|
||||
'role': usr.roles[0].id,
|
||||
'auth_source': usr.auth_source
|
||||
}
|
||||
|
||||
return ajax_response(
|
||||
@ -384,3 +425,17 @@ def role(rid):
|
||||
response=res,
|
||||
status=200
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route(
|
||||
'/auth_sources/', methods=['GET'], endpoint='auth_sources'
|
||||
)
|
||||
def auth_sources():
|
||||
sources = []
|
||||
for source in current_app.PGADMIN_SUPPORTED_AUTH_SOURCE:
|
||||
sources.append({'label': source, 'value': source})
|
||||
|
||||
return ajax_response(
|
||||
response=sources,
|
||||
status=200
|
||||
)
|
||||
|
@ -9,12 +9,12 @@
|
||||
|
||||
define([
|
||||
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'pgadmin.alertifyjs',
|
||||
'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node',
|
||||
'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node', 'pgadmin.backform',
|
||||
'pgadmin.user_management.current_user',
|
||||
'backgrid.select.all', 'backgrid.filter',
|
||||
], function(
|
||||
gettext, url_for, $, _, alertify, pgBrowser, Backbone, Backgrid, Backform,
|
||||
pgNode, userInfo
|
||||
pgNode, pgBackform, userInfo
|
||||
) {
|
||||
|
||||
// if module is already initialized, refer to that.
|
||||
@ -24,6 +24,8 @@ define([
|
||||
|
||||
var USERURL = url_for('user_management.users'),
|
||||
ROLEURL = url_for('user_management.roles'),
|
||||
SOURCEURL = url_for('user_management.auth_sources'),
|
||||
AUTH_ONLY_INTERNAL = (userInfo['auth_sources'].length == 1 && userInfo['auth_sources'].includes('internal')) ? true : false,
|
||||
userFilter = function(collection) {
|
||||
return (new Backgrid.Extension.ClientSideFilter({
|
||||
collection: collection,
|
||||
@ -33,6 +35,41 @@ define([
|
||||
}));
|
||||
};
|
||||
|
||||
// Integer Cell for Columns Length and Precision
|
||||
var PasswordDepCell = Backgrid.Extension.PasswordDepCell =
|
||||
Backgrid.Extension.PasswordCell.extend({
|
||||
initialize: function() {
|
||||
Backgrid.Extension.PasswordCell.prototype.initialize.apply(this, arguments);
|
||||
Backgrid.Extension.DependentCell.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
dependentChanged: function () {
|
||||
this.$el.empty();
|
||||
var model = this.model,
|
||||
column = this.column,
|
||||
editable = this.column.get('editable'),
|
||||
is_editable = _.isFunction(editable) ? !!editable.apply(column, [model]) : !!editable;
|
||||
|
||||
if (is_editable){ this.$el.addClass('editable'); }
|
||||
else { this.$el.removeClass('editable'); }
|
||||
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
render: function() {
|
||||
Backgrid.NumberCell.prototype.render.apply(this, arguments);
|
||||
|
||||
var model = this.model,
|
||||
column = this.column,
|
||||
editable = this.column.get('editable'),
|
||||
is_editable = _.isFunction(editable) ? !!editable.apply(column, [model]) : !!editable;
|
||||
|
||||
if (is_editable){ this.$el.addClass('editable'); }
|
||||
else { this.$el.removeClass('editable'); }
|
||||
return this;
|
||||
},
|
||||
remove: Backgrid.Extension.DependentCell.prototype.remove,
|
||||
});
|
||||
|
||||
pgBrowser.UserManagement = {
|
||||
init: function() {
|
||||
if (this.initialized)
|
||||
@ -235,20 +272,67 @@ define([
|
||||
// Callback to draw User Management Dialog.
|
||||
show_users: function() {
|
||||
if (!userInfo['is_admin']) return;
|
||||
var Roles = [];
|
||||
var Roles = [],
|
||||
Sources = [];
|
||||
|
||||
var UserModel = pgBrowser.Node.Model.extend({
|
||||
idAttribute: 'id',
|
||||
urlRoot: USERURL,
|
||||
defaults: {
|
||||
id: undefined,
|
||||
username: undefined,
|
||||
email: undefined,
|
||||
active: true,
|
||||
role: undefined,
|
||||
newPassword: undefined,
|
||||
confirmPassword: undefined,
|
||||
auth_source: 'internal',
|
||||
authOnlyInternal: AUTH_ONLY_INTERNAL,
|
||||
},
|
||||
schema: [{
|
||||
id: 'auth_source',
|
||||
label: gettext('Authentication Source'),
|
||||
type: 'text',
|
||||
control: 'Select2',
|
||||
url: url_for('user_management.auth_sources'),
|
||||
cellHeaderClasses: 'width_percent_30',
|
||||
visible: function(m) {
|
||||
if (m.get('authOnlyInternal')) return false;
|
||||
return true;
|
||||
},
|
||||
disabled: false,
|
||||
cell: 'Select2',
|
||||
select2: {
|
||||
allowClear: false,
|
||||
openOnEnter: false,
|
||||
first_empty: false,
|
||||
},
|
||||
options: function() {
|
||||
return Sources;
|
||||
},
|
||||
editable: function(m) {
|
||||
if (m instanceof Backbone.Collection) {
|
||||
return true;
|
||||
}
|
||||
if (m.isNew() && !m.get('authOnlyInternal')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}, {
|
||||
id: 'username',
|
||||
label: gettext('Username'),
|
||||
type: 'text',
|
||||
cell: Backgrid.Extension.StringDepCell,
|
||||
cellHeaderClasses: 'width_percent_30',
|
||||
deps: ['auth_source'],
|
||||
editable: function(m) {
|
||||
if (m.get('authOnlyInternal') || m.get('auth_source') == 'internal') return false;
|
||||
return true;
|
||||
},
|
||||
disabled: false,
|
||||
}, {
|
||||
id: 'email',
|
||||
label: gettext('Email'),
|
||||
type: 'text',
|
||||
@ -256,6 +340,8 @@ define([
|
||||
cellHeaderClasses: 'width_percent_30',
|
||||
deps: ['id'],
|
||||
editable: function(m) {
|
||||
if (!m.get('authOnlyInternal')) return true;
|
||||
|
||||
if (m instanceof Backbone.Collection) {
|
||||
return false;
|
||||
}
|
||||
@ -328,23 +414,39 @@ define([
|
||||
type: 'password',
|
||||
disabled: false,
|
||||
control: 'input',
|
||||
cell: 'password',
|
||||
cell: PasswordDepCell,
|
||||
cellHeaderClasses: 'width_percent_20',
|
||||
deps: ['auth_source'],
|
||||
editable: function(m) {
|
||||
if (m.get('auth_source') == 'internal') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}, {
|
||||
id: 'confirmPassword',
|
||||
label: gettext('Confirm password'),
|
||||
type: 'password',
|
||||
disabled: false,
|
||||
control: 'input',
|
||||
cell: 'password',
|
||||
cell: PasswordDepCell,
|
||||
cellHeaderClasses: 'width_percent_20',
|
||||
deps: ['auth_source'],
|
||||
editable: function(m) {
|
||||
if (m.get('auth_source') == 'internal') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}],
|
||||
validate: function() {
|
||||
var errmsg = null,
|
||||
changedAttrs = this.changed || {},
|
||||
email_filter = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
|
||||
if (('email' in changedAttrs || !this.isNew()) && (_.isUndefined(this.get('email')) ||
|
||||
if (this.get('auth_source') == 'internal' && ('email' in changedAttrs || !this.isNew()) && (_.isUndefined(this.get('email')) ||
|
||||
_.isNull(this.get('email')) ||
|
||||
String(this.get('email')).replace(/^\s+|\s+$/g, '') == '')) {
|
||||
errmsg = gettext('Email address cannot be empty.');
|
||||
@ -358,9 +460,8 @@ define([
|
||||
this.errorModel.set('email', errmsg);
|
||||
return errmsg;
|
||||
} else if (!!this.get('email') && this.collection.where({
|
||||
'email': this.get('email'),
|
||||
'email': this.get('email'), 'auth_source': 'internal',
|
||||
}).length > 1) {
|
||||
|
||||
errmsg = gettext('The email address %s already exists.',
|
||||
this.get('email')
|
||||
);
|
||||
@ -385,111 +486,113 @@ define([
|
||||
this.errorModel.unset('role');
|
||||
}
|
||||
|
||||
if (this.isNew()) {
|
||||
// Password is compulsory for new user.
|
||||
if ('newPassword' in changedAttrs && (_.isUndefined(this.get('newPassword')) ||
|
||||
_.isNull(this.get('newPassword')) ||
|
||||
this.get('newPassword') == '')) {
|
||||
if (this.get('auth_source') == 'internal') {
|
||||
if (this.isNew()) {
|
||||
// Password is compulsory for new user.
|
||||
if ('newPassword' in changedAttrs && (_.isUndefined(this.get('newPassword')) ||
|
||||
_.isNull(this.get('newPassword')) ||
|
||||
this.get('newPassword') == '')) {
|
||||
|
||||
errmsg = gettext('Password cannot be empty for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
errmsg = gettext('Password cannot be empty for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
this.errorModel.set('newPassword', errmsg);
|
||||
return errmsg;
|
||||
} else if (!_.isUndefined(this.get('newPassword')) &&
|
||||
!_.isNull(this.get('newPassword')) &&
|
||||
this.get('newPassword').length < 6) {
|
||||
this.errorModel.set('newPassword', errmsg);
|
||||
return errmsg;
|
||||
} else if (!_.isUndefined(this.get('newPassword')) &&
|
||||
!_.isNull(this.get('newPassword')) &&
|
||||
this.get('newPassword').length < 6) {
|
||||
|
||||
errmsg = gettext('Password must be at least 6 characters for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
errmsg = gettext('Password must be at least 6 characters for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
this.errorModel.set('newPassword', errmsg);
|
||||
return errmsg;
|
||||
} else {
|
||||
this.errorModel.unset('newPassword');
|
||||
}
|
||||
|
||||
if ('confirmPassword' in changedAttrs && (_.isUndefined(this.get('confirmPassword')) ||
|
||||
_.isNull(this.get('confirmPassword')) ||
|
||||
this.get('confirmPassword') == '')) {
|
||||
|
||||
errmsg = gettext('Confirm Password cannot be empty for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
this.errorModel.set('confirmPassword', errmsg);
|
||||
return errmsg;
|
||||
} else {
|
||||
this.errorModel.unset('confirmPassword');
|
||||
}
|
||||
|
||||
if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
|
||||
this.get('newPassword') != this.get('confirmPassword')) {
|
||||
|
||||
errmsg = gettext('Passwords do not match for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
this.errorModel.set('confirmPassword', errmsg);
|
||||
return errmsg;
|
||||
} else {
|
||||
this.errorModel.unset('confirmPassword');
|
||||
}
|
||||
|
||||
this.errorModel.set('newPassword', errmsg);
|
||||
return errmsg;
|
||||
} else {
|
||||
this.errorModel.unset('newPassword');
|
||||
}
|
||||
if ((_.isUndefined(this.get('newPassword')) || _.isNull(this.get('newPassword')) ||
|
||||
this.get('newPassword') == '') &&
|
||||
((_.isUndefined(this.get('confirmPassword')) || _.isNull(this.get('confirmPassword')) ||
|
||||
this.get('confirmPassword') == ''))) {
|
||||
|
||||
if ('confirmPassword' in changedAttrs && (_.isUndefined(this.get('confirmPassword')) ||
|
||||
this.errorModel.unset('newPassword');
|
||||
if (this.get('newPassword') == '') {
|
||||
this.set({
|
||||
'newPassword': undefined,
|
||||
});
|
||||
}
|
||||
|
||||
this.errorModel.unset('confirmPassword');
|
||||
if (this.get('confirmPassword') == '') {
|
||||
this.set({
|
||||
'confirmPassword': undefined,
|
||||
});
|
||||
}
|
||||
} else if (!_.isUndefined(this.get('newPassword')) &&
|
||||
!_.isNull(this.get('newPassword')) &&
|
||||
!this.get('newPassword') == '' &&
|
||||
this.get('newPassword').length < 6) {
|
||||
|
||||
errmsg = gettext('Password must be at least 6 characters for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
this.errorModel.set('newPassword', errmsg);
|
||||
return errmsg;
|
||||
} else if (_.isUndefined(this.get('confirmPassword')) ||
|
||||
_.isNull(this.get('confirmPassword')) ||
|
||||
this.get('confirmPassword') == '')) {
|
||||
this.get('confirmPassword') == '') {
|
||||
|
||||
errmsg = gettext('Confirm Password cannot be empty for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
errmsg = gettext('Confirm Password cannot be empty for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
this.errorModel.set('confirmPassword', errmsg);
|
||||
return errmsg;
|
||||
} else {
|
||||
this.errorModel.unset('confirmPassword');
|
||||
}
|
||||
this.errorModel.set('confirmPassword', errmsg);
|
||||
return errmsg;
|
||||
} else if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
|
||||
this.get('newPassword') != this.get('confirmPassword')) {
|
||||
|
||||
if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
|
||||
this.get('newPassword') != this.get('confirmPassword')) {
|
||||
errmsg = gettext('Passwords do not match for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
errmsg = gettext('Passwords do not match for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
this.errorModel.set('confirmPassword', errmsg);
|
||||
return errmsg;
|
||||
} else {
|
||||
this.errorModel.unset('confirmPassword');
|
||||
}
|
||||
|
||||
} else {
|
||||
if ((_.isUndefined(this.get('newPassword')) || _.isNull(this.get('newPassword')) ||
|
||||
this.get('newPassword') == '') &&
|
||||
((_.isUndefined(this.get('confirmPassword')) || _.isNull(this.get('confirmPassword')) ||
|
||||
this.get('confirmPassword') == ''))) {
|
||||
|
||||
this.errorModel.unset('newPassword');
|
||||
if (this.get('newPassword') == '') {
|
||||
this.set({
|
||||
'newPassword': undefined,
|
||||
});
|
||||
this.errorModel.set('confirmPassword', errmsg);
|
||||
return errmsg;
|
||||
} else {
|
||||
this.errorModel.unset('newPassword');
|
||||
this.errorModel.unset('confirmPassword');
|
||||
}
|
||||
|
||||
this.errorModel.unset('confirmPassword');
|
||||
if (this.get('confirmPassword') == '') {
|
||||
this.set({
|
||||
'confirmPassword': undefined,
|
||||
});
|
||||
}
|
||||
} else if (!_.isUndefined(this.get('newPassword')) &&
|
||||
!_.isNull(this.get('newPassword')) &&
|
||||
!this.get('newPassword') == '' &&
|
||||
this.get('newPassword').length < 6) {
|
||||
|
||||
errmsg = gettext('Password must be at least 6 characters for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
this.errorModel.set('newPassword', errmsg);
|
||||
return errmsg;
|
||||
} else if (_.isUndefined(this.get('confirmPassword')) ||
|
||||
_.isNull(this.get('confirmPassword')) ||
|
||||
this.get('confirmPassword') == '') {
|
||||
|
||||
errmsg = gettext('Confirm Password cannot be empty for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
this.errorModel.set('confirmPassword', errmsg);
|
||||
return errmsg;
|
||||
} else if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
|
||||
this.get('newPassword') != this.get('confirmPassword')) {
|
||||
|
||||
errmsg = gettext('Passwords do not match for user %s.',
|
||||
(this.get('email') || '')
|
||||
);
|
||||
|
||||
this.errorModel.set('confirmPassword', errmsg);
|
||||
return errmsg;
|
||||
} else {
|
||||
this.errorModel.unset('newPassword');
|
||||
this.errorModel.unset('confirmPassword');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@ -716,7 +819,10 @@ define([
|
||||
saveUser: function(m) {
|
||||
var d = m.toJSON(true);
|
||||
|
||||
if (m.isNew() && (!m.get('email') || !m.get('role') ||
|
||||
if(m.isNew() && m.get('authOnlyInternal') === false &&
|
||||
(!m.get('username') || !m.get('auth_source') || !m.get('role')) ) {
|
||||
return false;
|
||||
} else if (m.isNew() && m.get('authOnlyInternal') === true && (!m.get('email') || !m.get('role') ||
|
||||
!m.get('newPassword') || !m.get('confirmPassword') ||
|
||||
m.get('newPassword') != m.get('confirmPassword'))) {
|
||||
// New user model is valid but partially filled so return without saving.
|
||||
@ -741,7 +847,7 @@ define([
|
||||
|
||||
m.startNewSession();
|
||||
alertify.success(gettext('User \'%s\' saved.',
|
||||
m.get('email')
|
||||
m.get('username')
|
||||
));
|
||||
},
|
||||
error: function(res, jqxhr) {
|
||||
@ -797,6 +903,23 @@ define([
|
||||
}, 100);
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: SOURCEURL,
|
||||
method: 'GET',
|
||||
async: false,
|
||||
})
|
||||
.done(function(res) {
|
||||
Sources = res;
|
||||
})
|
||||
.fail(function() {
|
||||
setTimeout(function() {
|
||||
alertify.alert(
|
||||
gettext('Error'),
|
||||
gettext('Cannot load user Sources.')
|
||||
);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
var view = this.view = new Backgrid.Grid({
|
||||
row: UserRow,
|
||||
columns: gridSchema.columns,
|
||||
|
@ -14,6 +14,7 @@ define('pgadmin.user_management.current_user', [], function() {
|
||||
'is_admin': {{ is_admin }},
|
||||
'name': '{{ name }}',
|
||||
'allow_save_password': {{ allow_save_password }},
|
||||
'allow_save_tunnel_password': {{ allow_save_tunnel_password }}
|
||||
'allow_save_tunnel_password': {{ allow_save_tunnel_password }},
|
||||
'auth_sources': {{ auth_sources }}
|
||||
}
|
||||
});
|
||||
|
@ -109,7 +109,7 @@ class TestClient(testing.FlaskClient):
|
||||
csrf_token = self.generate_csrf_token()
|
||||
|
||||
res = self.post(
|
||||
'/login', data=dict(
|
||||
'/authenticate/login', data=dict(
|
||||
email=email, password=password,
|
||||
csrf_token=csrf_token,
|
||||
),
|
||||
@ -120,5 +120,5 @@ class TestClient(testing.FlaskClient):
|
||||
return res
|
||||
|
||||
def logout(self):
|
||||
res = self.get('/logout', follow_redirects=False)
|
||||
res = self.get('/logout?next=/browser/', follow_redirects=False)
|
||||
self.csrf_token = None
|
||||
|
@ -118,6 +118,11 @@ app.PGADMIN_RUNTIME = True
|
||||
if config.SERVER_MODE is True:
|
||||
app.PGADMIN_RUNTIME = False
|
||||
app.config['WTF_CSRF_ENABLED'] = True
|
||||
|
||||
# Authentication sources
|
||||
app.PGADMIN_DEFAULT_AUTH_SOURCE = 'internal'
|
||||
app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'
|
||||
|
||||
app.test_client_class = TestClient
|
||||
test_client = app.test_client()
|
||||
test_client.setApp(app)
|
||||
@ -195,6 +200,8 @@ def get_test_modules(arguments):
|
||||
"browser.tests.test_login",
|
||||
"browser.tests.test_logout",
|
||||
"browser.tests.test_reset_password",
|
||||
"browser.tests.test_ldap_login",
|
||||
"browser.tests.test_ldap_with_mocking",
|
||||
])
|
||||
if arguments['exclude'] is not None:
|
||||
exclude_pkgs += arguments['exclude'].split(',')
|
||||
|
@ -11,6 +11,49 @@
|
||||
"login_password": "PASSWORD",
|
||||
"login_username": "USER@EXAMPLE.COM"
|
||||
},
|
||||
"pgAdmin4_ldap_credentials": {
|
||||
"login_password": "PASSWORD",
|
||||
"login_username": "USERNAME"
|
||||
},
|
||||
"ldap_config": [
|
||||
{
|
||||
"ldap": {
|
||||
"name": "Ldap scenario name"
|
||||
"uri": "ldap://IP-ADDRESS/HOSTNAME:389",
|
||||
"base_dn": "BASE-DN",
|
||||
"search_base_dn": "SEARCH-BASE-DN",
|
||||
"username_atr": "UID",
|
||||
"search_filter": "(objectclass=*)",
|
||||
"use_starttls": false,
|
||||
"ca_cert_file": "",
|
||||
"cert_file": "",
|
||||
"key_file": ""
|
||||
},
|
||||
"ldap_with_ssl": {
|
||||
"name": "Ldap scenario name"
|
||||
"uri": "ldaps://IP-ADDRESS/HOSTNAME:636",
|
||||
"base_dn": "BASE-DN",
|
||||
"search_base_dn": "SEARCH-BASE-DN",
|
||||
"username_atr": "UID",
|
||||
"search_filter": "(objectclass=*)",
|
||||
"use_starttls": false,
|
||||
"ca_cert_file": "",
|
||||
"cert_file": "",
|
||||
"key_file": ""
|
||||
},
|
||||
"ldap_with_tls": {
|
||||
"name": "Ldap scenario name"
|
||||
"uri": "ldap://IP-ADDRESS/HOSTNAME:389",
|
||||
"base_dn": "BASE-DN",
|
||||
"search_base_dn": "SEARCH-BASE-DN",
|
||||
"username_atr": "UID",
|
||||
"search_filter": "(objectclass=*)",
|
||||
"use_starttls": true,
|
||||
"ca_cert_file": "",
|
||||
"cert_file": "",
|
||||
"key_file": ""
|
||||
}
|
||||
}],
|
||||
"server_group": 1,
|
||||
"server_credentials": [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user