diff --git a/.dockerignore b/.dockerignore index d259ec7..153fb8c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ *.pyc .git/ -Dockerfile \ No newline at end of file +Dockerfile +__pycache__/* +drone.yml \ No newline at end of file diff --git a/.drone.yml b/.drone.yml index 089ad61..979ad14 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,13 +11,13 @@ steps: - name: docker image: plugins/docker settings: + repo: registry.alxczl.fr/ldap-interface username: from_secret: docker_username password: from_secret: docker_password - repo: registry.alxczl.fr/ldap-interface - repository: registry.alxczl.fr - tags: latest + registry: registry.alxczl.fr + auto_tag: true when: branch: - master \ No newline at end of file diff --git a/.gitignore b/.gitignore index 224ddc7..5634b05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ -*.pyc +*pyc +instance/* +__pycache__/* +*.log* +.idea/* +*.swp +#~ +~# venv \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e73b6e0..3ed6954 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,9 @@ FROM python:3.9-alpine -COPY requirements.txt / +COPY . / RUN pip3 install -r /requirements.txt -COPY ./app/ /app - -WORKDIR /app - EXPOSE 80/tcp ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..2409ddb --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,14 @@ +from flask import Flask +from . import reset + +def create_app(): + app = Flask(__name__, instance_relative_config=True, template_folder="ui/templates", static_folder="ui/static") + #app.config.from_object("instance.config.DebugConfig") + app.config.from_envvar("LDAP_ADDR") + app.config.from_envvar("LDAP_PORT") + app.config.from_envvar("LDAP_TLS") + app.config.from_envvar("BASE_DN") + + app.register_blueprint(reset.bp) + + return app \ No newline at end of file diff --git a/app/ldap_client.py b/app/ldap_client.py new file mode 100644 index 0000000..976c67a --- /dev/null +++ b/app/ldap_client.py @@ -0,0 +1,60 @@ +import ldap3 +from typing import Tuple + +class Client(): + def __init__(self, address: str, port: int, base_dn: str, primary_attribute: str = "uid", tls: bool = False): + self.server = ldap3.Server(host=address, port=port, use_ssl=tls) + self.base_dn = base_dn + self.address = address + self.port = port + self.tls = tls + self.primary_attribute = primary_attribute + + def bind(self, user: str, bind_passwd: str) -> Tuple[bool, str]: + user_dn = f"{self.primary_attribute}={user},{self.base_dn}" + + self.link = ldap3.Connection(self.server, user=user_dn, password=bind_passwd) + + try: + status = self.link.bind() + except Exception as _: + status = False + + if status == False: + print(f"[!!] Could not bind {user_dn} to the LDAP directory: {self.link.last_error}") + return (status, "") + + return (status, user_dn) + + def unbind(self) -> bool: + if self.link.bound != True: + return False + + try: + self.link.unbind() + except Exception as e: + pass + + return True + + def change_pwd(self, user_dn: str, new_password: str) -> bool: + if self.link.bound == False: + print("[!!] Can't change the password: not bound to the server") + return False + + status = self.link.modify(user_dn, {'userPassword': [(ldap3.MODIFY_REPLACE, [new_password])]}) + if status == True: + print(f"[++] Changed password of user {user_dn}") + else: + print(f"[!!] Could not change password of user {user_dn}: {self.link.last_error}") + + return status + +if __name__ == "__main__": + client = Client("dc01.lan.alxczl.fr", 636, "cn=users,cn=accounts,dc=lan,dc=alxczl,dc=fr", True) + client_dn = "uid=alexandre,cn=users,cn=accounts,dc=lan,dc=alxczl,dc=fr" + res = client.bind(client_dn, "Getshrektm8") + if res[0] == False: + print(client.link.result["description"]) + + #client.link.unbind() \ No newline at end of file diff --git a/app/reset.py b/app/reset.py new file mode 100644 index 0000000..f6036fd --- /dev/null +++ b/app/reset.py @@ -0,0 +1,72 @@ +from . import ldap_client + +from flask import ( + Blueprint, render_template, flash, + current_app +) +from flask_wtf import FlaskForm +from wtforms import ( + StringField, PasswordField, + SubmitField +) +from wtforms.validators import ( + ValidationError, DataRequired, + EqualTo, Length, Regexp +) + +bp = Blueprint('reset', __name__, url_prefix='/reset') + +class ResetPasswordForm(FlaskForm): + # Minimal password length + minlength = 9 + + # Form + username = StringField(label=('Login'), + validators=[DataRequired(), + Length(max=64)], + render_kw={"onkeyup": "validate_username()"}) + currentpassword = PasswordField(label=('Current password'), + validators=[DataRequired()]) + newpassword = PasswordField(label=('New password'), + validators=[DataRequired(), + Length(min=minlength), + Regexp("^(?=.*[a-z])"), + Regexp("^(?=.*[A-Z])"), + Regexp("^(?=.*\\d)"), + #Regexp( + # "(?=.*[@$!%*#?&])", message="Password must contain a special character" + #),], + ], + render_kw={"onkeyup": f"validate_form({minlength})"}) + confirm_password = PasswordField( + label=('Confirm Password'), + validators=[DataRequired(message='* Required'), + EqualTo('newpassword')], + render_kw={"onkeyup": f"validate_confirm({minlength})"}) + + submit = SubmitField(label=('Change my password'), render_kw={"onclick": f"validate_form({minlength})"}) + + # Validators + def validate_username(self, username): + excluded_chars = " *?!'^+%&/()=}][{$#;\\\"" + for char in self.username.data: + if char in excluded_chars: + raise ValidationError( + f"Character {char} is not allowed in an username.") + +@bp.route('/', methods=('GET', 'POST')) +def reset(): + form = ResetPasswordForm() + if form.validate_on_submit(): + client = ldap_client.Client(address=current_app.config["LDAP_ADDR"], port=current_app.config["LDAP_PORT"], base_dn=current_app.config["BASE_DN"], tls=current_app.config["LDAP_TLS"]) + bind_status = client.bind(form.username._value(), form.currentpassword._value()) + if bind_status[0] == False: + flash(f"Connection failed, are you sure that your login and password are correct ? ({client.link.last_error})") + elif client.change_pwd(bind_status[1], form.newpassword._value()) == False: + flash(f"An error occured and your password was not changed, sorry. ({client.link.last_error})") + client.unbind() + else: + flash('Your password has been changed !') + client.unbind() + + return render_template('reset.html', form=form) \ No newline at end of file diff --git a/app/ui/static/css/main.css b/app/ui/static/css/main.css new file mode 100644 index 0000000..b0faa5f --- /dev/null +++ b/app/ui/static/css/main.css @@ -0,0 +1,99 @@ +html, +body { + margin: 0; + height: 100%; + min-width: 310px; +} + +body { + min-height: 100%; + font-family: 'Roboto Mono', monospace; + color: white; + background: rgb(0, 0, 0); + background: linear-gradient(8deg, rgb(35, 155, 21) 0%, rgba(14, 47, 11, 1) 40%, rgba(0, 0, 0, 1) 70%); + background-repeat: no-repeat; + background-attachment: fixed; +} + +.vcenter { + position: absolute; + left: 50%; + top: 50%; + -webkit-transform: translateX(-50%) translateY(-50%); + transform: translateX(-50%) translateY(-50%); +} + +.container { + animation: fadein ease 3s; + animation-iteration-count: 1; + animation-fill-mode: forwards; +} + +a { + text-decoration: none; +} + +a:link>span { + color: rgb(216, 216, 216); +} + +a:visited>span { + color: rgb(216, 216, 216); +} + +a:hover>span { + color: rgb(255, 255, 255); +} + +.icon2x { + height: 32px; + width: 32px; + font-size: 32px; +} + +#username-msg, +#confirm-msg, +#password-msg { + color: #d4d4d4; + font-size: small; + transition: color 225ms cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +#password-msg li { + list-style: none; + content: "" +} + +#password-msg li::before { + content: "☑ "; +} + +.errorinput { + border-color: #f44246 !important; +} + +.errormsg { + color: #f44246 !important; + +} + +li.errormsg::before { + content: "☒ " !important; +} + + +.errorinput:focus { + box-shadow: 0 0 0 .10rem rgba(244, 66, 70, 0.50) !important; + -webkit-box-shadow: 0 0 0 .10rem rgba(244, 66, 70, 0.50) !important; +} + +.form-control:focus { + border-color: #5cb85c; + box-shadow: 0 0 0 .10rem rgba(92, 184, 92, 0.50); + -webkit-box-shadow: 0 0 0 .10rem rgba(92, 184, 92, 0.50); +} + +#reset-form { + background: #4e4e4e; + border-radius: .50rem; +} diff --git a/app/ui/static/images/apple-touch-icon.png b/app/ui/static/images/apple-touch-icon.png new file mode 100644 index 0000000..59a51f1 Binary files /dev/null and b/app/ui/static/images/apple-touch-icon.png differ diff --git a/app/ui/static/images/favicon-16x16.png b/app/ui/static/images/favicon-16x16.png new file mode 100644 index 0000000..176d796 Binary files /dev/null and b/app/ui/static/images/favicon-16x16.png differ diff --git a/app/ui/static/images/favicon-32x32.png b/app/ui/static/images/favicon-32x32.png new file mode 100644 index 0000000..2d5f612 Binary files /dev/null and b/app/ui/static/images/favicon-32x32.png differ diff --git a/app/ui/static/js/validate.js b/app/ui/static/js/validate.js new file mode 100644 index 0000000..8d7feeb --- /dev/null +++ b/app/ui/static/js/validate.js @@ -0,0 +1,83 @@ +function validate_form(minlength) { + var user = validate_username(); + var pass = validate_password(minlength); + + return validate_confirm() && pass && user; +} + +function validate_confirm() { + var password = document.getElementById("newpassword"); + var confirm = document.getElementById("confirm_password"); + + if (password.value != confirm.value) { + confirm.classList.add("errorinput"); + document.getElementById("confirm-msg").classList.add("errormsg"); + return false; + } + + confirm.classList.remove("errorinput"); + document.getElementById("confirm-msg").classList.remove("errormsg"); + + return true; +} + +function validate_username() { + var username = document.getElementById("username"); + var forbidden = /[*?!'\^+%\&/()=}{\$#;,\\"]+/; + + if (username.value.length > 64 || forbidden.test(username.value) == true) + { + document.getElementById("username-msg").classList.add("errormsg"); + username.classList.add("errorinput"); + return false; + } + + document.getElementById("username-msg").classList.remove("errormsg"); + username.classList.remove("errorinput"); + return true; +} + +function validate_password(minlength) { + // Did the checks pass ? + var status = true; + // Target element + var password = document.getElementById("newpassword"); + // Check the length + if (password.value.length < minlength) + { + status = false; + document.getElementById("minlen").classList.add("errormsg"); + } + else + document.getElementById("minlen").classList.remove("errormsg"); + // Look for a digit + if (/.*\d/.test(password.value) != true) { + status = false; + document.getElementById("digit").classList.add("errormsg"); + } + else + document.getElementById("digit").classList.remove("errormsg"); + // Look for a lowercase char + if (/.*[a-z]/.test(password.value) != true) { + status = false; + document.getElementById("lower").classList.add("errormsg"); + } + else + document.getElementById("lower").classList.remove("errormsg"); + // Look for an uppercase char + if (/.*[A-Z]/.test(password.value) != true) { + status = false; + document.getElementById("upper").classList.add("errormsg"); + } + else + document.getElementById("upper").classList.remove("errormsg"); + // Change the color of the inputbox + if (status == false) + { + password.classList.add("errorinput"); + } + else + password.classList.remove("errorinput"); + + return status; +} \ No newline at end of file diff --git a/app/ui/templates/base.html b/app/ui/templates/base.html new file mode 100644 index 0000000..70d2ffb --- /dev/null +++ b/app/ui/templates/base.html @@ -0,0 +1,26 @@ + + +
+ + + + +