Parcourir la source

Fixes #31 LDAP login and access

isundil il y a 1 mois
Parent
commit
03d403caaa

+ 19 - 0
router/api.js

@@ -77,6 +77,25 @@ module.exports = { register: app => {
         app.routerUtils.onApiRequest(req, res);
         app.routerUtils.jsonResponse(res, await accessListToJson(app, req));
     });
+    app.router.post("/api/access/login", async (req, res) => { // /api/access/login, post: { username: string, password: string }
+        app.routerUtils.onApiRequest(req, res);
+        if (!req.post?.username?.length || !req.post?.password?.length)
+            return app.routerUtils.httpResponse(res, 400, "Missing argument");
+        try {
+            const access = await app.databaseHelper.fetch(AccessModel, { type: ACCESS_TYPE.ldapAccount, typeData: req.post.username });
+            if (access?.length) {
+                await Security.tryLoginIntoSession(req, access, req.post.username, req.post.password);
+                if (access.find(x => x.accessTo == ACCESS_TO.admin))
+                    Security.setAdmin(req, true);
+            } else {
+                return app.routerUtils.onBadRequest(res, "Invalid Credentials");
+            }
+        }
+        catch (err) {
+            return app.routerUtils.onBadRequest(res, err);
+        }
+        app.routerUtils.jsonResponse(res, await accessListToJson(app, req));
+    });
     app.router.post("/api/access/link", async (req, res) => { // /api/access/link, post: { linkIds: [string] (JSON) }
         app.routerUtils.onApiRequest(req, res);
         if (!req.post?.linkIds?.length)

+ 2 - 3
src/config.js

@@ -12,9 +12,8 @@ let configEntries = CONFIG;
             port: { value: 80, valid: CONFIG.validNumber },
             instanceHostname: { value: require('os').hostname(), valid: CONFIG.validNotEmptyString },
             ldapUrl: { value: "", valid: CONFIG.validNotEmptyString },
-            ldapBindDN: { value: "", valid: CONFIG.validNotEmptyString },
-            ldapBindPwd: { value: "", valid: CONFIG.validNotEmptyString },
-            ldapBase: { value: "", valid: CONFIG.validNotEmptyString },
+            ldapBindAttribute: { value: "", valid: CONFIG.validNotEmptyString },
+            ldapBindBase: { value: "", valid: CONFIG.validNotEmptyString },
             database: { value: "", valid: CONFIG.validNotEmptyString }
         });
 

+ 37 - 1
src/security.js

@@ -2,6 +2,8 @@
 const crypto = require('crypto');
 const MD5 = require('craftlabhttpserver/src/md5sum.js').string;
 const { AccessModel, ACCESS_TO, ACCESS_TYPE } = require('../model/access.js');
+const ldapjs = require('ldapjs');
+const CONFIG = require('./config.js');
 
 module.exports = require('craftlabhttpserver/src/security.js');
 
@@ -25,6 +27,14 @@ function LinkAccess(linkId, linkLabel) {
 LinkAccess.prototype = Object.create(Access.prototype);
 LinkAccess.prototype.id = function() { return "LINK_"+this.linkId; }
 
+function LdapAccess(dbId, username) {
+    Access.call(this);
+    this.dbId = dbId;
+    this.ldapDn = username;
+}
+LdapAccess.prototype = Object.create(Access.prototype);
+LdapAccess.prototype.id = function() { return `LDAP_${this.ldapDn}_${this.dbId}`; }
+
 module.exports.getAccessList = getAccessList;
 module.exports.createSession = req => {
     const now = Date.now();
@@ -67,6 +77,33 @@ module.exports.addLinkToSession = (req, dbId, linkId, linkLabel) => {
     session.accessList[accessItem.id()] = accessItem;
     return session.accessList;
 };
+module.exports.tryLoginIntoSession = (req, accessModels, username, password) => {
+    let session = module.exports.getSessionObj(req.cookies);
+    if (!session)
+        return Promise.resolve(undefined);
+    return new Promise((ok, ko) => {
+        const ldapClient = ldapjs.createClient({ url: CONFIG.ldapUrl });
+        ldapClient.on('error', err => {
+            console.error(err);
+            ko(err.lde_message);
+            ok = null;
+            ldapClient.unbind();
+        });
+        ldapClient.once('connect', () => {
+            ldapClient.bind(`${CONFIG.ldapBindAttribute}=${username},${CONFIG.ldapBindBase}`, password, err => {
+                if (err) {
+                    console.error(`${CONFIG.ldapBindAttribute}=${username},${CONFIG.ldapBindBase}: `, err.lde_message);
+                    ko(err.lde_message);
+                }
+                else for (let i of accessModels) {
+                    let accessItem = new LdapAccess(i.id, username);
+                    session.accessList[accessItem.id()] = accessItem;
+                    ok?.(session.accessList);
+                }
+            });
+        });
+    });
+};
 module.exports.removeFromSession = (req, accessId) => {
     let session = module.exports.getSessionObj(req.cookies);
     if (!session)
@@ -78,4 +115,3 @@ module.exports.removeFromSession = (req, accessId) => {
 module.exports.setAdminFlat = () => {
     session.accessList.isAdmin_ = true;
 }
-

BIN
static/db.sqlite


+ 3 - 1
static/public/css/style.css

@@ -372,7 +372,9 @@ body.login-visible #pch-page > .login-wrapper {
     margin-left: auto;
     margin-right: auto;
 }
-
+.login-wrapper #login-userpass +.error {
+    margin-top: 1em;
+}
 .share-wrapper.hidden {
     display: none;
 }

+ 22 - 5
static/public/js/access.js

@@ -23,16 +23,15 @@ $(() => {
         }
 
         Logout(code) {
-            let value = null;
+            let storedLink = false;
             this.#linkStoredAccesses.forEach(i => {
                 if (i.key === code) {
                     this.#linkStoredAccesses.delete(i);
-                    value = i.value;
+                    storedLink = true;
                 }
             });
-            if (!value)
-                return;
-            this.#UpdateStorage();
+            if (storedLink)
+                this.#UpdateStorage();
             return LoadingTasks.push(() => {
                 return new Promise(ok => {
                     $.ajax({
@@ -73,6 +72,24 @@ $(() => {
             });
         }
 
+        LoginUserPass(username, password) {
+            return new Promise((ok, ko) => {
+                $.ajax({
+                    url: "/api/access/login",
+                    type: "POST",
+                    data: { username: username, password: password },
+                    success: async data => {
+                        this.#isAdmin = data.isAdmin;
+                        window.ReloadAccessList(data);
+                        ok();
+                    },
+                    error: err => {
+                        ko(err.responseText);
+                    }
+                });
+            });
+        }
+
         async RebuildAccess() {
             if (this.#linkStoredAccesses.size) {
                 return LoadingTasks.push(() => {

+ 27 - 9
static/public/js/uiAccess.js

@@ -15,8 +15,11 @@ function closeLoginPopin() {
     document.body.classList.remove("login-visible");
     document.body.classList.remove("overlay-visible");
     let inputFields = document.querySelectorAll(".login-wrapper .input-group input");
-    for (let i =0; i < inputFields.length; ++i)
-        inputFields[i].value = "";
+    for (let i =0; i < inputFields.length; ++i) {
+        if (inputFields[i].type !== 'submit')
+            inputFields[i].value = "";
+    }
+    document.querySelector("#login-userpass + .error").classList.add("hidden");
 }
 
 document.onClosePopinRequested(() => {
@@ -28,10 +31,23 @@ document.querySelector(".login-wrapper .modal-footer button").addEventListener("
 let loginUserPass = document.getElementById("login-userpass");
 let loginCode = document.getElementById("login-code");
 
-loginUserPass.querySelector("button").addEventListener("click", () => {
+loginUserPass.parentElement.addEventListener("submit", e => {
+    e.preventDefault();
     let user = loginUserPass.querySelector("input[type='text']").value;
     let pass = loginUserPass.querySelector("input[type='password']").value;
-    console.log([user, pass]);
+    LoadingTasks.push(async () => {
+        try {
+            await AccessManager.LoginUserPass(user, pass);
+        }
+        catch (err) {
+            let errDiv = loginUserPass.parentElement.querySelector(".error");
+            errDiv.textContent = err;
+            errDiv.classList.remove("hidden");
+            return;
+        }
+        await MediaStorage.Instance.rebuildMetaList();
+        closeLoginPopin();
+    });
 });
 
 loginCode.querySelector("button").addEventListener("click", () => {
@@ -45,7 +61,7 @@ loginCode.querySelector("button").addEventListener("click", () => {
     });
 });
 
-async function logout(accessId, linkId) {
+async function logout(accessId) {
     await AccessManager.Logout(accessId);
     await MediaStorage.Instance.rebuildMetaList();
     closeLoginPopin();
@@ -63,6 +79,7 @@ window.ReloadAccessList = function(accessList) {
     }
 
     let getIconForType = type => {
+        if (!!type.ldapDn) return "bi-database";
         if (!!type.linkId) return "bi-link-45deg";
         if (!!type.userName) return "bi-person";
         return "bi-question-octagon";
@@ -73,6 +90,7 @@ window.ReloadAccessList = function(accessList) {
     for (let i =0; i < items.length; ++i)
         items[i].remove();
     delete accessList.isAdmin;
+    delete accessList.isAdmin_;
     if (Object.keys(accessList||{}).length) {
         let li = document.createElement("li");
         li.classList.add("accessItem");
@@ -83,7 +101,7 @@ window.ReloadAccessList = function(accessList) {
         rootNode.appendChild(li);
     }
     for (let i in accessList) {
-        const accessTextValue = accessList[i].linkLabel || accessList[i].linkId;
+        const accessTextValue = accessList[i].linkLabel || accessList[i].linkId || accessList[i].ldapDn;
 
         let li = document.createElement("li");
         li.classList.add("accessItem");
@@ -102,11 +120,11 @@ window.ReloadAccessList = function(accessList) {
         a.appendChild(logoutIcon);
         li.appendChild(a);
         rootNode.appendChild(li);
-        a.addEventListener("click", e => {
+        a.addEventListener("click", async e => {
             e.preventDefault();
-            if (!window.confirm("Logout account " +accessTextValue +" ?"))
+            if (!await window.confirm("Logout account " +accessTextValue +" ?"))
                 return;
-            logout(i, accessList[i].linkId);
+            logout(i);
         });
     }
 }

+ 33 - 2
static/public/js/uiShare.js

@@ -174,6 +174,32 @@ function buildShareItemRevokeButton(data, container) {
     return deleteButtonRow;
 }
 
+function buildShareItemLdapSpecific(htmlId, data) {
+        if (data.typeId === 1) { // Link type
+        let typeDivRow = document.createDocumentFragment();
+        let linkContainer = document.createElement("div");
+        linkContainer.className = "container";
+        let input = document.createElement("input");
+        input.className = "form-control";
+        input.value = data.typeLabel;
+        input.type = "text";
+        input.id = `${htmlId}-linkLabel`;
+        let label = document.createElement("label");
+        label.textContent = "LDAP username";
+        label.className = "form-label"
+        label.setAttribute("for", input.id);
+        linkContainer.appendChild(label);
+        linkContainer.appendChild(input);
+        input.addEventListener("change", async () => {
+            data.typeLabel = input.value;
+            data.typeData = input.value;
+            await updateData(data);
+        });
+        typeDivRow.appendChild(linkContainer);
+        return typeDivRow;
+    }
+}
+
 function buildShareItemLinkSpecific(htmlId, data) {
     if (data.typeId === 3) { // Link type
         let typeDivRow = document.createDocumentFragment();
@@ -332,7 +358,8 @@ async function buildShareItem(data) {
 
     container.appendChild(buildShareItemHeader(htmlId, data, accordionBody));
     {
-        const div = buildShareItemLinkSpecific(htmlId, data);
+        const div = buildShareItemLinkSpecific(htmlId, data)
+            || buildShareItemLdapSpecific(htmlId, data);
         div && accordionBody.appendChild(div);
     }
     accordionBody.appendChild(await buildShareItemType(typeDepandentDiv, grantDiv, data));
@@ -388,7 +415,11 @@ async function createShareData(typeId, typeData) {
     return share;
 }
 
-document.getElementById("pch-share-addLdap").addEventListener("click", () => {});
+document.getElementById("pch-share-addLdap").addEventListener("click", async () => {
+    if (!windowDisplayed)
+        return;
+    document.getElementById('pch-share-container').querySelector("ul").appendChild(await buildShareItem(await createShareData(1, "")));
+});
 document.getElementById("pch-share-addEmail").addEventListener("click", () => {});
 document.getElementById("pch-share-addLink").addEventListener("click", async () => {
     if (!windowDisplayed)

+ 8 - 5
templates/_login.js

@@ -9,11 +9,14 @@ module.exports = `
                 </div>
                 <div class="modal-body">
                     <h6>Using Account</h6>
-                    <div class="input-group" id="login-userpass">
-                        <input class="form-control" type="text" placeholder="User" />
-                        <input class="form-control" type="password" placeholder="xxxxxx" />
-                        <button type="button" class="btn btn-primary" data-dismiss="modal">Login</button>
-                    </div>
+                    <form method="POST" action="#">
+                        <div class="input-group" id="login-userpass">
+                            <input class="form-control" name="user" type="text" placeholder="User" />
+                            <input class="form-control" name="pass" type="password" placeholder="xxxxxx" />
+                            <input type="submit" class="btn btn-primary" data-dismiss="modal"/>
+                        </div>
+                    <form>
+                    <div class="error alert alert-danger hidden"></div>
                 </div>
                 <hr/>
                 <div class="modal-body">