import datetime import hashlib import random import uuid import bcrypt import pyotp import sqlalchemy as db import requests from .ConnectionString import ConnectionString, DEFAULT_DB from .DashboardClientsPeerAssignment import DashboardClientsPeerAssignment from .DashboardClientsTOTP import DashboardClientsTOTP from .DashboardOIDC import DashboardOIDC from .Utilities import ValidatePasswordStrength from .DashboardLogger import DashboardLogger from flask import session class DashboardClients: def __init__(self, wireguardConfigurations): self.logger = DashboardLogger() self.engine = db.create_engine(ConnectionString(DEFAULT_DB)) self.metadata = db.MetaData() self.OIDC = DashboardOIDC("Client") self.dashboardClientsTable = db.Table( 'DashboardClients', self.metadata, db.Column('ClientID', db.String(255), nullable=False, primary_key=True), db.Column('Email', db.String(255), nullable=False, index=True), db.Column('Password', db.String(500)), db.Column('TotpKey', db.String(500)), db.Column('TotpKeyVerified', db.Integer), db.Column('CreatedDate', (db.DATETIME if 'sqlite:///' in ConnectionString(DEFAULT_DB) else db.TIMESTAMP), server_default=db.func.now()), db.Column('DeletedDate', (db.DATETIME if 'sqlite:///' in ConnectionString(DEFAULT_DB) else db.TIMESTAMP)), extend_existing=True, ) self.dashboardOIDCClientsTable = db.Table( 'DashboardOIDCClients', self.metadata, db.Column('ClientID', db.String(255), nullable=False, primary_key=True), db.Column('Email', db.String(255), nullable=False, index=True), db.Column('ProviderIssuer', db.String(500), nullable=False, index=True), db.Column('ProviderSubject', db.String(500), nullable=False, index=True), db.Column('CreatedDate', (db.DATETIME if 'sqlite:///' in ConnectionString(DEFAULT_DB) else db.TIMESTAMP), server_default=db.func.now()), db.Column('DeletedDate', (db.DATETIME if 'sqlite:///' in ConnectionString(DEFAULT_DB) else db.TIMESTAMP)), extend_existing=True, ) self.dashboardClientsInfoTable = db.Table( 'DashboardClientsInfo', self.metadata, db.Column('ClientID', db.String(255), nullable=False, primary_key=True), db.Column('Name', db.String(500)), extend_existing=True, ) self.dashboardClientsPasswordResetLinkTable = db.Table( 'DashboardClientsPasswordResetLinks', self.metadata, db.Column('ResetToken', db.String(255), nullable=False, primary_key=True), db.Column('ClientID', db.String(255), nullable=False), db.Column('CreatedDate', (db.DATETIME if 'sqlite:///' in ConnectionString(DEFAULT_DB) else db.TIMESTAMP), server_default=db.func.now()), db.Column('ExpiryDate', (db.DATETIME if 'sqlite:///' in ConnectionString(DEFAULT_DB) else db.TIMESTAMP)), extend_existing=True ) self.metadata.create_all(self.engine) self.Clients = {} self.ClientsRaw = [] self.__getClients() self.DashboardClientsTOTP = DashboardClientsTOTP() self.DashboardClientsPeerAssignment = DashboardClientsPeerAssignment(wireguardConfigurations) def __getClients(self): with self.engine.connect() as conn: localClients = db.select( self.dashboardClientsTable.c.ClientID, self.dashboardClientsTable.c.Email, db.literal_column("'Local'").label("ClientGroup") ).where( self.dashboardClientsTable.c.DeletedDate.is_(None) ) oidcClients = db.select( self.dashboardOIDCClientsTable.c.ClientID, self.dashboardOIDCClientsTable.c.Email, self.dashboardOIDCClientsTable.c.ProviderIssuer.label("ClientGroup"), ).where( self.dashboardOIDCClientsTable.c.DeletedDate.is_(None) ) union = db.union(localClients, oidcClients).alias("U") self.ClientsRaw = conn.execute( db.select( union, self.dashboardClientsInfoTable.c.Name ).outerjoin(self.dashboardClientsInfoTable, union.c.ClientID == self.dashboardClientsInfoTable.c.ClientID) ).mappings().fetchall() groups = set(map(lambda c: c.get('ClientGroup'), self.ClientsRaw)) gr = {} for g in groups: gr[(g if g == 'Local' else self.OIDC.GetProviderNameByIssuer(g))] = [ dict(x) for x in list( filter(lambda c: c.get('ClientGroup') == g, self.ClientsRaw) ) ] self.Clients = gr def GetAllClients(self): self.__getClients() return self.Clients def GetAllClientsRaw(self): self.__getClients() return self.ClientsRaw def GetClient(self, ClientID) -> dict[str, str] | None: c = filter(lambda x: x['ClientID'] == ClientID, self.ClientsRaw) client = next((dict(client) for client in c), None) if client is not None: client['ClientGroup'] = self.OIDC.GetProviderNameByIssuer(client['ClientGroup']) return client def GetClientProfile(self, ClientID): with self.engine.connect() as conn: return dict(conn.execute( db.select( *[c for c in self.dashboardClientsInfoTable.c if c.name != 'ClientID'] ).where( self.dashboardClientsInfoTable.c.ClientID == ClientID ) ).mappings().fetchone()) def SignIn_ValidatePassword(self, Email, Password) -> bool: if not all([Email, Password]): return False existingClient = self.SignIn_UserExistence(Email) if existingClient: return bcrypt.checkpw(Password.encode("utf-8"), existingClient.get("Password").encode("utf-8")) return False def SignIn_UserExistence(self, Email): with self.engine.connect() as conn: existingClient = conn.execute( self.dashboardClientsTable.select().where( self.dashboardClientsTable.c.Email == Email ) ).mappings().fetchone() return existingClient def SignIn_OIDC_UserExistence(self, data: dict[str, str]): with self.engine.connect() as conn: existingClient = conn.execute( self.dashboardOIDCClientsTable.select().where( db.and_( self.dashboardOIDCClientsTable.c.ProviderIssuer == data.get('iss'), self.dashboardOIDCClientsTable.c.ProviderSubject == data.get('sub'), ) ) ).mappings().fetchone() return existingClient def SignUp_OIDC(self, data: dict[str, str]) -> tuple[bool, str] | tuple[bool, None]: if not self.SignIn_OIDC_UserExistence(data): with self.engine.begin() as conn: newClientUUID = str(uuid.uuid4()) conn.execute( self.dashboardOIDCClientsTable.insert().values({ "ClientID": newClientUUID, "Email": data.get('email', ''), "ProviderIssuer": data.get('iss', ''), "ProviderSubject": data.get('sub', '') }) ) conn.execute( self.dashboardClientsInfoTable.insert().values({ "ClientID": newClientUUID, "Name": data.get("name") }) ) self.logger.log(Message=f"User {data.get('email', '')} from {data.get('iss', '')} signed up") self.__getClients() return True, newClientUUID return False, "User already signed up" def SignOut_OIDC(self): sessionPayload = session.get('OIDCPayload') status, oidc_config = self.OIDC.GetProviderConfiguration(session.get('SignInPayload').get("Provider")) signOut = requests.get( oidc_config.get("end_session_endpoint"), params={ 'id_token_hint': session.get('SignInPayload').get("Payload").get('sid') } ) return True def SignIn_OIDC(self, **kwargs): status, data = self.OIDC.VerifyToken(**kwargs) if not status: return False, "Sign in failed. Reason: " + data existingClient = self.SignIn_OIDC_UserExistence(data) if not existingClient: status, newClientUUID = self.SignUp_OIDC(data) session['ClientID'] = newClientUUID else: session['ClientID'] = existingClient.get("ClientID") session['SignInMethod'] = 'OIDC' session['SignInPayload'] = { "Provider": kwargs.get('provider'), "Payload": data } return True, data def SignIn(self, Email, Password) -> tuple[bool, str]: if not all([Email, Password]): return False, "Please fill in all fields" existingClient = self.SignIn_UserExistence(Email) if existingClient: checkPwd = self.SignIn_ValidatePassword(Email, Password) if checkPwd: session['SignInMethod'] = 'local' session['Email'] = Email session['ClientID'] = existingClient.get("ClientID") return True, self.DashboardClientsTOTP.GenerateToken(existingClient.get("ClientID")) return False, "Email or Password is incorrect" def SignIn_GetTotp(self, Token: str, UserProvidedTotp: str = None) -> tuple[bool, str] or tuple[bool, None, str]: status, data = self.DashboardClientsTOTP.GetTotp(Token) if not status: return False, "TOTP Token is invalid" if UserProvidedTotp is None: if data.get('TotpKeyVerified') is None: return True, pyotp.totp.TOTP(data.get('TotpKey')).provisioning_uri(name=data.get('Email'), issuer_name="WGDashboard Client") else: totpMatched = pyotp.totp.TOTP(data.get('TotpKey')).verify(UserProvidedTotp) if not totpMatched: return False, "TOTP is does not match" else: self.DashboardClientsTOTP.RevokeToken(Token) if data.get('TotpKeyVerified') is None: with self.engine.begin() as conn: conn.execute( self.dashboardClientsTable.update().values({ 'TotpKeyVerified': 1 }).where( self.dashboardClientsTable.c.ClientID == data.get('ClientID') ) ) return True, None def SignUp(self, Email, Password, ConfirmPassword) -> tuple[bool, str] or tuple[bool, None]: try: if not all([Email, Password, ConfirmPassword]): return False, "Please fill in all fields" if Password != ConfirmPassword: return False, "Passwords does not match" existingClient = self.SignIn_UserExistence(Email) if existingClient: return False, "Email already signed up" pwStrength, msg = ValidatePasswordStrength(Password) if not pwStrength: return pwStrength, msg with self.engine.begin() as conn: newClientUUID = str(uuid.uuid4()) totpKey = pyotp.random_base32() encodePassword = Password.encode('utf-8') conn.execute( self.dashboardClientsTable.insert().values({ "ClientID": newClientUUID, "Email": Email, "Password": bcrypt.hashpw(encodePassword, bcrypt.gensalt()).decode("utf-8"), "TotpKey": totpKey }) ) conn.execute( self.dashboardClientsInfoTable.insert().values({ "ClientID": newClientUUID }) ) self.logger.log(Message=f"User {Email} signed up") self.__getClients() except Exception as e: self.logger.log(Status="false", Message=f"Signed up failed, reason: {str(e)}") return False, "Signe up failed." return True, None def GetClientAssignedPeers(self, ClientID): return self.DashboardClientsPeerAssignment.GetAssignedPeers(ClientID) def ResetClientPassword(self, ClientID, NewPassword, ConfirmNewPassword) -> tuple[bool, str] | tuple[bool, None]: c = self.GetClient(ClientID) if c is None: return False, "Client does not exist" if NewPassword != ConfirmNewPassword: return False, "New passwords does not match" pwStrength, msg = ValidatePasswordStrength(NewPassword) if not pwStrength: return pwStrength, msg try: with self.engine.begin() as conn: conn.execute( self.dashboardClientsTable.update().values({ "TotpKeyVerified": None, "TotpKey": pyotp.random_base32(), "Password": bcrypt.hashpw(NewPassword.encode('utf-8'), bcrypt.gensalt()).decode("utf-8"), }).where( self.dashboardClientsTable.c.ClientID == ClientID ) ) self.logger.log(Message=f"User {ClientID} reset password and TOTP") except Exception as e: self.logger.log(Status="false", Message=f"User {ClientID} reset password failed, reason: {str(e)}") return False, "Reset password failed." return True, None def UpdateClientPassword(self, ClientID, CurrentPassword, NewPassword, ConfirmNewPassword) -> tuple[bool, str] | tuple[bool, None]: c = self.GetClient(ClientID) if c is None: return False, "Client does not exist" if not all([CurrentPassword, NewPassword, ConfirmNewPassword]): return False, "Please fill in all fields" if not self.SignIn_ValidatePassword(c.get('Email'), CurrentPassword): return False, "Current password does not match" if NewPassword != ConfirmNewPassword: return False, "New passwords does not match" pwStrength, msg = ValidatePasswordStrength(NewPassword) if not pwStrength: return pwStrength, msg try: with self.engine.begin() as conn: conn.execute( self.dashboardClientsTable.update().values({ "Password": bcrypt.hashpw(NewPassword.encode('utf-8'), bcrypt.gensalt()).decode("utf-8"), }).where( self.dashboardClientsTable.c.ClientID == ClientID ) ) self.logger.log(Message=f"User {ClientID} updated password") except Exception as e: self.logger.log(Status="false", Message=f"User {ClientID} update password failed, reason: {str(e)}") return False, "Update password failed." return True, None def UpdateClientProfile(self, ClientID, Name): try: with self.engine.begin() as conn: conn.execute( self.dashboardClientsInfoTable.update().values({ "Name": Name }).where( self.dashboardClientsInfoTable.c.ClientID == ClientID ) ) self.logger.log(Message=f"User {ClientID} updated name to {Name}") except Exception as e: self.logger.log(Status="false", Message=f"User {ClientID} updated name to {Name} failed") return False return True def DeleteClient(self, ClientID): try: with self.engine.begin() as conn: client = self.GetClient(ClientID) if client.get("ClientGroup") == "Local": conn.execute( self.dashboardClientsTable.delete().where( self.dashboardClientsTable.c.ClientID == ClientID ) ) else: conn.execute( self.dashboardOIDCClientsTable.delete().where( self.dashboardOIDCClientsTable.c.ClientID == ClientID ) ) conn.execute( self.dashboardClientsInfoTable.delete().where( self.dashboardClientsInfoTable.c.ClientID == ClientID ) ) self.DashboardClientsPeerAssignment.UnassignPeers(ClientID) self.__getClients() except Exception as e: self.logger.log(Status="false", Message=f"Failed to delete {ClientID}") return False return True ''' For WGDashboard Admin to Manage Clients ''' def GenerateClientPasswordResetToken(self, ClientID) -> bool | str: c = self.GetClient(ClientID) if c is None: return False newToken = str(random.randint(0, 999999)).zfill(6) with self.engine.begin() as conn: conn.execute( self.dashboardClientsPasswordResetLinkTable.update().values({ "ExpiryDate": datetime.datetime.now() }).where( db.and_( self.dashboardClientsPasswordResetLinkTable.c.ClientID == ClientID, self.dashboardClientsPasswordResetLinkTable.c.ExpiryDate > db.func.now() ) ) ) conn.execute( self.dashboardClientsPasswordResetLinkTable.insert().values({ "ResetToken": newToken, "ClientID": ClientID, "CreatedDate": datetime.datetime.now(), "ExpiryDate": datetime.datetime.now() + datetime.timedelta(minutes=30) }) ) return newToken def ValidateClientPasswordResetToken(self, ClientID, Token): c = self.GetClient(ClientID) if c is None: return False with self.engine.connect() as conn: t = conn.execute( self.dashboardClientsPasswordResetLinkTable.select().where( db.and_(self.dashboardClientsPasswordResetLinkTable.c.ClientID == ClientID, self.dashboardClientsPasswordResetLinkTable.c.ResetToken == Token, self.dashboardClientsPasswordResetLinkTable.c.ExpiryDate > datetime.datetime.now()) ) ).mappings().fetchone() return t is not None def RevokeClientPasswordResetToken(self, ClientID, Token): with self.engine.begin() as conn: conn.execute( self.dashboardClientsPasswordResetLinkTable.update().values({ "ExpiryDate": datetime.datetime.now() }).where( db.and_(self.dashboardClientsPasswordResetLinkTable.c.ClientID == ClientID, self.dashboardClientsPasswordResetLinkTable.c.ResetToken == Token) ) ) return True def GetAssignedPeerClients(self, ConfigurationName, PeerID): c = self.DashboardClientsPeerAssignment.GetAssignedClients(ConfigurationName, PeerID) for a in c: client = self.GetClient(a.ClientID) if client is not None: a.Client = self.GetClient(a.ClientID) return c def GetClientAssignedPeersGrouped(self, ClientID): client = self.GetClient(ClientID) if client is not None: p = self.DashboardClientsPeerAssignment.GetAssignedPeers(ClientID) configs = set(map(lambda x : x['configuration_name'], p)) d = {} for i in configs: d[i] = list(filter(lambda x : x['configuration_name'] == i, p)) return d return None def AssignClient(self, ConfigurationName, PeerID, ClientID) -> tuple[bool, dict[str, str]] | tuple[bool, None]: return self.DashboardClientsPeerAssignment.AssignClient(ClientID, ConfigurationName, PeerID) def UnassignClient(self, AssignmentID): return self.DashboardClientsPeerAssignment.UnassignClients(AssignmentID)