I wanted to automate certain operations that we do very often, and so I decided to do a PoC of how handy will it be to create GUI applications that can automate tasks.
As locating information in several repositories of information (ldap, databases, websites, etc…) can be tedious I decided to create a small program that queries LDAP for the information I’m interested, in this case a Location. This small program can very easily escalated to launch the VPN, to query a Database after querying LDAP if no results are found, etc…
I share with you the basic application as you may find interesting to create GUI applications in Python, compatible with Windows, Linux and Mac.
I’m super Linux fan but this is important, as many multinationals still use Windows or Mac even for Engineers and SRE positions.
With the article I provide a Dockerfile and a docker-compose.yml file that will launch an OpenLDAP Docker Container preloaded with very basic information and a PHPLDAPMIN Container.
Installation of the dependencies
Ubuntu:
sudo apt-get install python3.8 python3-tk pip install ldap3
Windows and Mac:
Install Python3.6 or greater and from command line.
For Mac install pip if you don’t have it.
pip install ldap3
Python Code
#!/bin/env python3
import tkinter as tk
from ldap3 import Server, Connection, MODIFY_ADD, MODIFY_REPLACE, ALL_ATTRIBUTES, ObjectDef, Reader
from ldap3.utils.dn import safe_rdn
from lib.fileutils import FileUtils
# sudo apt-get install python3.8 python3-tk
# pip install ldap3
class LDAPGUI:
s_config_file = "config.cfg"
s_ldap_server = "ldapslave01"
s_connection = "uid=%USERNAME%, cn=users, cn=accounts, dc=demo1, dc=carlesmateo, dc=com"
s_username = "carlesmateo"
s_password = "Secret123"
s_query = "location=%LOCATION%,dc=demo1,dc=carlesmateo,dc=com"
i_window_width = 610
i_window_height = 650
i_frame_width = i_window_width-5
i_frame_height = 30
i_frame_results_height = 400
# Graphical objects
o_window = None
o_entry_ldap = None
o_entry_connection = None
o_entry_results = None
o_entry_username = None
o_entry_location = None
o_button_search = None
def __init__(self, o_fileutils):
self.o_fileutils = o_fileutils
def replace_connection_with_username(self):
s_connection_raw = self.o_entry_username.get()
s_connection = self.s_connection.replace("%USERNAME%", s_connection_raw)
return s_connection
def replace_query_with_location(self):
s_query_raw = self.o_entry_location.get()
s_query = self.s_query.replace("%LOCATION%", s_query_raw)
return s_query
def clear_results(self):
self.o_entry_results.delete("1.0", tk.END)
def enable_button(self):
self.o_button_search["state"] = tk.NORMAL
def disable_button(self):
self.o_button_search["state"] = tk.DISABLED
def ldap_run_query(self):
self.disable_button()
self.clear_results()
s_ldap_server = self.o_entry_ldap.get()
self.o_entry_results.insert(tk.END, "Connecting to: " + s_ldap_server + "...\n")
try:
o_server = Server(s_ldap_server)
o_conn = Connection(o_server,
self.replace_connection_with_username(),
self.s_password,
auto_bind=False)
o_conn.bind()
if isinstance(o_conn.last_error, str):
s_last_error = o_conn.last_error
self.o_entry_results.insert(tk.END, "Last error: " + s_last_error + "\n")
if isinstance(o_conn.result, str):
s_conn_result = o_conn.result
self.o_entry_results.insert(tk.END, "Connection result: " + s_conn_result + "\n")
s_query = self.replace_query_with_location()
self.o_entry_results.insert(tk.END, "Performing Query: " + s_query + "\n")
obj_location = ObjectDef('organizationalUnit', o_conn)
r = Reader(o_conn, obj_location, s_query)
r.search()
self.o_entry_results.insert(tk.END, "Results: \n" + str(r) + "\n")
#print(r)
#print(0, o_conn.extend.standard.who_am_i())
# https://github.com/cannatag/ldap3/blob/master/tut1.py
# https://github.com/cannatag/ldap3/blob/master/tut2.py
except:
self.o_entry_results.insert(tk.END, "There has been a problem" + "\n")
if isinstance(o_conn.last_error, str):
s_last_error = o_conn.last_error
self.o_entry_results.insert(tk.END, "Last error: " + s_last_error + "\n")
try:
self.o_entry_results.insert(tk.END, "Closing connection\n")
o_conn.unbind()
except:
self.o_entry_results.insert(tk.END, "Problem closing connection" + "\n")
self.enable_button()
def create_frame_location(self, o_window, i_width, i_height):
o_frame = tk.Frame(master=o_window, width=i_width, height=i_height)
o_frame.pack()
o_lbl_location = tk.Label(master=o_frame,
text="Location:",
width=10,
height=1)
o_lbl_location.place(x=0, y=0)
o_entry = tk.Entry(master=o_frame, fg="yellow", bg="blue", width=50)
o_entry.insert(tk.END, "")
o_entry.place(x=100, y=0)
return o_frame, o_entry
def create_frame_results(self, o_window, i_width, i_height):
o_frame = tk.Frame(master=o_window, width=i_width, height=i_height)
o_frame.pack()
o_entry = tk.Text(master=o_frame, fg="grey", bg="white", width=75, height=20)
o_entry.insert(tk.END, "")
o_entry.place(x=0, y=0)
return o_frame, o_entry
def create_frame_username(self, o_window, i_width, i_height, s_username):
o_frame = tk.Frame(master=o_window, width=i_width, height=i_height)
o_frame.pack()
o_lbl_location = tk.Label(master=o_frame,
text="Username:",
width=10,
height=1)
o_lbl_location.place(x=0, y=0)
o_entry_username = tk.Entry(master=o_frame, fg="yellow", bg="blue", width=50)
o_entry_username.insert(tk.END, s_username)
o_entry_username.place(x=100, y=0)
return o_frame, o_entry_username
def create_frame_ldapserver(self, o_window, i_width, i_height, s_server):
o_frame = tk.Frame(master=o_window, width=i_width, height=i_height)
o_frame.pack()
o_lbl_ldapserver = tk.Label(master=o_frame,
text="LDAP Server:",
width=10,
height=1)
o_lbl_ldapserver.place(x=0, y=0)
# We don't need to pack the label, as it is inside a Frame, packet
# o_lbl_ldapserver.pack()
o_entry = tk.Entry(master=o_frame, fg="yellow", bg="blue", width=50)
o_entry.insert(tk.END, s_server)
o_entry.place(x=100, y=0)
return o_frame, o_entry
def create_frame_connection(self, o_window, i_width, i_height, s_connection):
o_frame = tk.Frame(master=o_window, width=i_width, height=i_height)
o_frame.pack()
o_lbl_connection = tk.Label(master=o_frame,
text="Connection:",
width=10,
height=1)
o_lbl_connection.place(x=0, y=0)
# We don't need to pack the label, as it is inside a Frame, packet
# o_lbl_ldapserver.pack()
o_entry_connection = tk.Entry(master=o_frame, fg="yellow", bg="blue", width=50)
o_entry_connection.insert(tk.END, s_connection)
o_entry_connection.place(x=100, y=0)
return o_frame, o_entry_connection
def create_frame_query(self, o_window, i_width, i_height, s_query):
o_frame = tk.Frame(master=o_window, width=i_width, height=i_height)
o_frame.pack()
o_lbl_query = tk.Label(master=o_frame,
text="Connection:",
width=10,
height=1)
o_lbl_query.place(x=0, y=0)
o_entry_query = tk.Entry(master=o_frame, fg="yellow", bg="blue", width=50)
o_entry_query.insert(tk.END, s_query)
o_entry_query.place(x=100, y=0)
return o_frame, o_entry_query
def create_button(self):
o_button = tk.Button(
text="Search",
width=25,
height=1,
bg="blue",
fg="yellow",
command=self.ldap_run_query
)
o_button.pack()
return o_button
def render_screen(self):
o_window = tk.Tk()
o_window.title("LDAPGUI by Carles Mateo")
self.o_window = o_window
self.o_window.geometry(str(self.i_window_width) + 'x' + str(self.i_window_height))
o_frame_ldap, o_entry_ldap = self.create_frame_ldapserver(o_window=o_window,
i_width=self.i_frame_width,
i_height=self.i_frame_height,
s_server=self.s_ldap_server)
self.o_entry_ldap = o_entry_ldap
o_frame_connection, o_entry_connection = self.create_frame_connection(o_window=o_window,
i_width=self.i_frame_width,
i_height=self.i_frame_height,
s_connection=self.s_connection)
self.o_entry_connection = o_entry_connection
o_frame_user, o_entry_user = self.create_frame_username(o_window=o_window,
i_width=self.i_frame_width,
i_height=self.i_frame_height,
s_username=self.s_username)
self.o_entry_username = o_entry_user
o_frame_query, o_entry_query = self.create_frame_query(o_window=o_window,
i_width=self.i_frame_width,
i_height=self.i_frame_height,
s_query=self.s_query)
self.o_entry_query = o_entry_query
o_frame_location, o_entry_location = self.create_frame_location(o_window=o_window,
i_width=self.i_frame_width,
i_height=self.i_frame_height)
self.o_entry_location = o_entry_location
o_button_search = self.create_button()
self.o_button_search = o_button_search
o_frame_results, o_entry_results = self.create_frame_results(o_window=o_window,
i_width=self.i_frame_width,
i_height=self.i_frame_results_height)
self.o_entry_results = o_entry_results
o_window.mainloop()
def load_config_values(self):
b_success, d_s_config = self.o_fileutils.read_config_file_values(self.s_config_file)
if b_success is True:
if 'server' in d_s_config:
self.s_ldap_server = d_s_config['server']
if 'connection' in d_s_config:
self.s_connection = d_s_config['connection']
if 'username' in d_s_config:
self.s_username = d_s_config['username']
if 'password' in d_s_config:
self.s_password = d_s_config['password']
if 'query' in d_s_config:
self.s_query = d_s_config['query']
def main():
o_fileutils = FileUtils()
o_ldapgui = LDAPGUI(o_fileutils=o_fileutils)
o_ldapgui.load_config_values()
o_ldapgui.render_screen()
if __name__ == "__main__":
main()
Dockerfile
FROM osixia/openldap
# Note: Docker compose will generate the Docker Image.
# Run: sudo docker-compose up -d
LABEL maintainer="Carles Mateo"
ENV LDAP_ORGANISATION="Carles Mateo Test Org" \
LDAP_DOMAIN="carlesmateo.com"
COPY bootstrap.ldif /container/service/slapd/assets/config/bootstrap/ldif/50-bootstrap.ldif
docker-compose.yml
version: '3.3'
services:
ldap_server:
build:
context: .
dockerfile: Dockerfile
environment:
LDAP_ADMIN_PASSWORD: test1234
LDAP_BASE_DN: dc=carlesmateo,dc=com
ports:
- 389:389
volumes:
- ldap_data:/var/lib/ldap
- ldap_config:/etc/ldap/slapd.d
ldap_server_admin:
image: osixia/phpldapadmin:0.7.2
ports:
- 8090:80
environment:
PHPLDAPADMIN_LDAP_HOSTS: ldap_server
PHPLDAPADMIN_HTTPS: 'false'
volumes:
ldap_data:
ldap_config:
config.cfg
# Config File for LDAPGUI by Carles Mateo
# https://blog.carlesmateo.com
#
# Configuration for working with the prepared Docker
server=127.0.0.1:389
connection=cn=%USERNAME%,dc=carlesmateo,dc=com
username=admin_gh
password=admin_gh_pass
query=cn=%LOCATION%,dc=carlesmateo,dc=com
# Carles other tests
#server=ldapslave.carlesmateo.com
#connection=uid=%USERNAME%, cn=users, cn=accounts, dc=demo1, dc=carlesmateo, dc=com
#username=carlesmateo
#password=Secret123
#query=location=%LOCATION%,dc=demo1,dc=carlesmateo,dc=com
bootstrap.ldif
In order to bootstrap the Data on the LDAP Server I use a bootstrap.ldif file copied into the Server.
Credits, I learned this trick in this page: https://medium.com/better-programming/ldap-docker-image-with-populated-users-3a5b4d090aa4
dn: cn=developer,dc=carlesmateo,dc=com
changetype: add
objectclass: Engineer
cn: developer
givenname: developer
sn: Developer
displayname: Developer User
mail: developer@carlesmateo.com
userpassword: developer_pass
dn: cn=maintainer,dc=carlesmateo,dc=com
changetype: add
objectclass: Engineer
cn: maintainer
givenname: maintainer
sn: Maintainer
displayname: Maintainer User
mail: maintainer@carlesmateo.com
userpassword: maintainer_pass
dn: cn=admin,dc=carlesmateo,dc=com
changetype: add
objectclass: Engineer
cn: admin
givenname: admin
sn: Admin
displayname: Admin
mail: admin@carlesmateo.com
userpassword: admin_pass
dn: ou=Groups,dc=carlesmateo,dc=com
changetype: add
objectclass: organizationalUnit
ou: Groups
dn: ou=Users,dc=carlesmateo,dc=com
changetype: add
objectclass: organizationalUnit
ou: Users
dn: cn=Admins,ou=Groups,dc=carlesmateo,dc=com
changetype: add
cn: Admins
objectclass: groupOfUniqueNames
uniqueMember: cn=admin,dc=carlesmateo,dc=com
dn: cn=Maintaners,ou=Groups,dc=carlesmateo,dc=com
changetype: add
cn: Maintaners
objectclass: groupOfUniqueNames
uniqueMember: cn=maintainer,dc=carlesmateo,dc=com
uniqueMember: cn=developer,dc=carlesmateo,dc=com
lib/fileutils.py
You can download this file from the lib folder in my project CTOP.py
More information about programming with tkinter:
https://realpython.com/python-gui-tkinter/#getting-user-input-with-entry-widgets
https://www.tutorialspoint.com/python/python_gui_programming.htm
https://effbot.org/tkinterbook/entry.htm
About LDAP: