Prechádzať zdrojové kódy

Menu with all entries in a tree

isundil 4 rokov pred
rodič
commit
6bb161cf65

+ 9 - 0
LDAPManager.njsproj

@@ -33,6 +33,10 @@
     <Content Include="public\javascripts\category.js">
       <SubType>Code</SubType>
     </Content>
+    <Content Include="public\javascripts\tree.js">
+      <SubType>Code</SubType>
+    </Content>
+    <Content Include="templates\tree.pug" />
     <Content Include="views\category.pug">
       <SubType>Code</SubType>
     </Content>
@@ -59,9 +63,11 @@
     <Folder Include="public\stylesheets\" />
     <Folder Include="routes\" />
     <Folder Include="src\" />
+    <Folder Include="templates\" />
     <Folder Include="views\" />
   </ItemGroup>
   <ItemGroup>
+    <TypeScriptCompile Include="routes\entity.ts" />
     <TypeScriptCompile Include="routes\login.ts">
       <SubType>Code</SubType>
     </TypeScriptCompile>
@@ -71,6 +77,9 @@
     <TypeScriptCompile Include="src\LDAPInterface.ts">
       <SubType>Code</SubType>
     </TypeScriptCompile>
+    <TypeScriptCompile Include="src\LDAPTree.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
     <TypeScriptCompile Include="src\RouterUtils.ts" />
     <TypeScriptCompile Include="src\Security.ts" />
     <TypeScriptCompile Include="src\LDAPEntry.ts">

+ 2 - 0
app.ts

@@ -3,6 +3,7 @@ import { AddressInfo } from "net";
 import * as path from 'path';
 import * as bodyParser from 'body-parser';
 import route_category from './routes/category';
+import route_entity from './routes/entity';
 import { route_login, route_logout } from './routes/login';
 import LDAPFactory from './src/ldapInterface';
 
@@ -35,6 +36,7 @@ app.use((req, res, next) => {
 
 //app.use('/', route_index);
 app.use('/', route_category);
+app.use('/entity', route_entity);
 app.use('/login', route_login);
 app.use('/logout', route_logout);
 

+ 1 - 0
config.js.sample

@@ -7,6 +7,7 @@ module.exports = {
 	LDAP_LOGIN_SCOPE: "one",
 	LDAP_LOGIN_FILTER: (user) => `(|(mail=${user})(cn=${user}))`,
 
+	LDAP_ROOT: "dc=foo,dc=bar",
 	LDAP_MAP_ADDRESS: "dn",
 
 	LDAP_CATEGORIES: [

+ 29 - 1
public/javascripts/category.js

@@ -5,4 +5,32 @@
 		(selection = document.getElementById(document.location.hash.replace("#", ""))) !== null) {
 		selection.classList.add("selected");
 	}
-})();
+
+	function removeEntry(dn) {
+		let csrfToken = document.getElementById("csrf").value;
+		if (!csrfToken)
+			return;
+		let req = new XMLHttpRequest();
+		req.onreadystatechange = () => {
+			if (req.readyState === 4) {
+				if (req.status === 203)
+					document.location.reload();
+				else
+					alert("Error: " +req.statusText);
+			}
+		}
+		req.open("DELETE", "/entity?dn=" + encodeURIComponent(dn) + "&csrf=" + encodeURIComponent(csrfToken));
+		req.send();
+	}
+
+	document.querySelectorAll('.action-remove').forEach(i => {
+		i.addEventListener('click', (e) => {
+			let dn = e.target.dataset["dn"] || e.currentTarget.dataset["dn"];
+			if (!dn)
+				return;
+			if (!window.confirm("Really remove entry " + dn + " ?"))
+				return;
+			removeEntry(dn);
+		});
+	});
+})();

+ 25 - 0
public/javascripts/tree.js

@@ -0,0 +1,25 @@
+window["makeTree"] = function (ulRoot) {
+	let makeTree = (li) => {
+		let button = li.children[0];
+		let list = li.children[1];
+		if (button && list && button.nodeName == 'SPAN' && list.nodeName == 'UL') {
+			button.innerText = "-" + button.dataset["name"];
+			list.style.display = 'block';
+			button.addEventListener("click", (e) => {
+				let visible = list.style.display === 'block';
+				if (visible) {
+					button.innerText = "+" + button.dataset["name"];
+					list.style.display = "none";
+				} else {
+					button.innerText = "-" + button.dataset["name"];
+					list.style.display = "block";
+				}
+			});
+
+			for (var i = 0; i < list.children.length; ++i)
+				makeTree(list.children[i]);
+		}
+	};
+	for (var i = 0; i < ulRoot.children.length; ++i)
+		makeTree(ulRoot.children[i]);
+}

+ 34 - 1
public/stylesheets/main.css

@@ -1,5 +1,6 @@
 body {
-    padding: 50px;
+    margin: 0;
+    padding: 0;
     font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
 }
 
@@ -8,4 +9,36 @@ a {
 }
 .category-list .category.selected {
     background-color: #dbfffd;
+}
+
+#menuBar {
+    display: inline-block;
+    position: fixed;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    max-width: 250px;
+    width: 100%;
+    border-right: 1px solid gray;
+    overflow: auto;
+}
+#menuBar > div {
+    margin: 2em;
+}
+.treeroot {
+    padding: 0 1.5em;
+}
+iframe#page {
+    display: inline-block;
+    position: fixed;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    max-width: calc(100% - 250px);
+    height: 100%;
+    width: 100%;
+    border: none;
+}
+div.page {
+    margin: 0 1em;
 }

+ 36 - 23
routes/category.ts

@@ -8,9 +8,9 @@ import { ILDAPManager } from '../src/ldapInterface';
 import LDAPEntry from '../src/LDAPEntry';
 import ConfigManager, { LDAPAttribute, LDAPCategory } from '../src/ConfigLoader';
 
-function GetCurrentCategory(req: express.Request, defaultResult: LDAPCategory): LDAPCategory {
+function GetCurrentCategory(req: express.Request, defaultResult: LDAPCategory): LDAPCategory|null {
     if (!req.query["category"])
-        return defaultResult;
+        return null;
     let query = Array.isArray(req.query["category"]) ? req.query["category"][0] : req.query["category"];
     return LinkToCategory(query.toString()) || defaultResult;
 }
@@ -31,27 +31,40 @@ router.get('/', (req: express.Request, res: express.Response) => {
     if (!Security.requireLoggedUser(req, res))
         return;
     req.ldapManager.GetInstance().then((ldap: ILDAPManager): void => {
-        let categories = ConfigManager.GetInstance().GetCategories();
-        let category = GetCurrentCategory(req, categories[0]);
-        ldap.ListEntries(category).then(items => {
-            res.render("category", {
-                categoryName: category.GetName(),
-                allCategories: categories.map((i) => { return { name: i.GetName(), lnk: "?category="+CategoryToLink(i) }}),
-                items: items,
-                attributes: category.GetAttributes(),
-                DnToLnk: DnToLink,
-                GetAttributeLink: function (attr: LDAPAttribute, entityDn: string) {
-                    if (attr.type.startsWith("entry")) {
-                        let link = attr.type.split("#", 2)[1];
-                        if (link)
-                            return "?category=" + link + "#" + DnToLink(entityDn);
-                    }
-                    return null;
-				},
-                GetAttributeValue: function (user: LDAPEntry, attribute: LDAPAttribute) {
-                    return user.GetAttributeByName(attribute.name);
-				}
-            });
+        ldap.GetTree().then(root => {
+            let categories = ConfigManager.GetInstance().GetCategories();
+            let category = GetCurrentCategory(req, categories[0]);
+
+            if (category == null) {
+                res.render("index", {
+                    tree: root.GetChildren()[0],
+                    rootSrc: "?category=" + CategoryToLink(categories[0])
+                });
+            } else {
+                let session = Security.GetSession(req);
+
+                ldap.ListEntries(category).then(items => {
+                    res.render("category", {
+                        csrf: session ? session.GetCSRFToken() : "",
+                        categoryName: category?.GetName(),
+                        allCategories: categories.map((i) => { return { name: i.GetName(), lnk: "?category=" + CategoryToLink(i) } }),
+                        items: items,
+                        attributes: category?.GetAttributes(),
+                        DnToLnk: DnToLink,
+                        GetAttributeLink: function (attr: LDAPAttribute, entityDn: string) {
+                            if (attr.type.startsWith("entry")) {
+                                let link = attr.type.split("#", 2)[1];
+                                if (link)
+                                    return "?category=" + link + "#" + DnToLink(entityDn);
+                            }
+                            return null;
+                        },
+                        GetAttributeValue: function (user: LDAPEntry, attribute: LDAPAttribute) {
+                            return user.GetAttributeByName(attribute.name);
+                        }
+                    });
+                });
+			}
         });
     });
 });

+ 32 - 0
routes/entity.ts

@@ -0,0 +1,32 @@
+/*
+ * GET users listing.
+ */
+import express = require('express');
+const router = express.Router();
+import Security from '../src/Security';
+import { ILDAPManager } from '../src/ldapInterface';
+
+router.delete('/', (req: express.Request, res: express.Response) => {
+    const session = Security.GetSession(req);
+    if (!req.query["csrf"] || !req.query["dn"] || Array.isArray(req.query["csrf"]) || Array.isArray(req.query["dn"])) {
+        res.sendStatus(400);
+        return;
+	}
+    if (!session || !req.query["csrf"] || req.query["csrf"] !== session.GetCSRFToken()) {
+        res.sendStatus(403);
+        return;
+    }
+    req.ldapManager.GetInstance().then((ldap: ILDAPManager): void => {
+        ldap.Remove(req.query["dn"]?.toString() || "")
+            .then(() => {
+                res.sendStatus(203);
+            })
+            .catch(err => {
+                res.statusCode = 500;
+                res.statusMessage = err;
+                res.send();
+            });
+    });
+});
+
+export default router;

+ 5 - 1
routes/login.ts

@@ -36,7 +36,11 @@ function ManageLogin(req: express.Request, res: express.Response, ldap: ILDAPMan
 }
 
 route_login.get('/', (req: express.Request, res: express.Response) => {
-	ManageLogin(req, res, null, null);
+	req.ldapManager.GetInstance().then(man => {
+		man.GetTree().then(tree => {
+			ManageLogin(req, res, null, null);
+		});
+	});
 });
 route_login.post('/', (req: express.Request, res: express.Response) => {
 	req.ldapManager.GetInstance().then(ldap => {

+ 4 - 0
src/ConfigLoader.ts

@@ -39,6 +39,7 @@ export class LDAPCategory {
 export interface IConfigLoader {
 	GetCategories(): Array<LDAPCategory>;
 	GetCategoryByName(name: string): LDAPCategory | null;
+	GetLDAPRoot(): string;
 	GetLDAPUrls(): Array<string>;
 	GetBindDn(): string;
 	GetBindPassword(): string;
@@ -53,6 +54,7 @@ class ConfigLoader implements IConfigLoader {
 		for (let i of config.LDAP_CATEGORIES) {
 			this.fCategories.push(new LDAPCategory(i));
 		}
+		this.fServer.root = config["LDAP_ROOT"];
 		this.fServer.urls = typeof config["LDAP_URL"] === "string" ? [config["LDAP_URL"]] : config["LDAP_URL"];
 		this.fServer.bindDn = config["LDAP_BIND_DN"];
 		this.fServer.bindPassword = config["LDAP_BIND_PASSWD"];
@@ -62,6 +64,7 @@ class ConfigLoader implements IConfigLoader {
 		this.fLogin.fFilter = config["LDAP_LOGIN_FILTER"];
 	}
 
+	public GetLDAPRoot(): string { return this.fServer.root; }
 	public GetLDAPUrls(): Array<string> { return this.fServer.urls; }
 	public GetBindDn(): string { return this.fServer.bindDn; }
 	public GetBindPassword(): string { return this.fServer.bindPassword; }
@@ -81,6 +84,7 @@ class ConfigLoader implements IConfigLoader {
 	private fCategories: Array<LDAPCategory>;
 	private fServer = {
 		urls: [],
+		root: "",
 		bindDn: "",
 		bindPassword: ""
 	};

+ 76 - 0
src/LDAPTree.ts

@@ -0,0 +1,76 @@
+
+export default class LDAPTree {
+
+	private constructor(parent: LDAPTree|null, name: string) {
+		this.fParent = parent || this;
+		this.fName = name;
+	}
+
+	public static CreateRoot(): LDAPTree {
+		return new LDAPTree(null, "");
+	}
+
+	private PushPath(path: Array<string>, type: Array<string>): LDAPTree {
+		if (path.length) {
+			let child = this.fChildren.get(path[0]);
+			if (!child) {
+				child = new LDAPTree(this, path[0]);
+				this.fChildren.set(path[0], child);
+			}
+			child.PushPath(path.slice(1), type);
+		} else {
+			this.fTypes = type;
+		}
+		return this;
+	}
+
+	public push(dn: string, type: Array<string>): LDAPTree {
+		let path = dn.split(",").reverse();
+		return this.PushPath(path, type);
+	}
+
+	public GetName(): string {
+		return this.fName;
+	}
+
+	public IsRoot(): boolean {
+		return this.fParent === this;
+	}
+
+	public Compress(): LDAPTree {
+		for (let [_, j] of this.fChildren)
+			j.Compress();
+		if (this.fChildren.size == 1 && !this.IsRoot()) {
+			for (let [_, child] of this.fChildren) {
+				if (child.fChildren.size == 0)
+					break;
+				this.fTypes = child.fTypes;
+				this.fName = child.fName + "," + this.fName;
+				this.fChildren = child.fChildren;
+				for (let [_, i] of this.fChildren)
+					i.fParent = this;
+			}
+		}
+		return this;
+	}
+
+	public GetChildren(): Array<LDAPTree> {
+		let result = new Array();
+		for (let [_, val] of this.fChildren)
+			result.push(val);
+		return result;
+	}
+
+	public HasChildren(): boolean {
+		return this.fChildren.size > 0;
+	}
+
+	public FullName(): string {
+		return this.fParent.IsRoot() ? this.fName : (this.fName + "," + this.fParent.FullName());
+	}
+
+	protected fParent: LDAPTree;
+	protected fTypes: Array<string>|null = null;
+	protected fName: string;
+	protected fChildren: Map<string, LDAPTree> = new Map<string, LDAPTree>();
+}

+ 7 - 1
src/Security.ts

@@ -10,11 +10,13 @@ class Session {
 	protected loggedAddress: string|null;
 	protected previousUsername: string|null;
 	protected key: string;
+	protected CSRFToken: string;
 
 	constructor(req: express.Request) {
 		let key = (new Date()).toISOString() + "__salt__" + req.connection.remoteAddress + "" + req.connection.remotePort + "" + START_DATE_TS;
 		this.key = crypto.createHash('sha1').update(key).digest('hex');
 		this.loggedAddress = this.previousUsername = null;
+		this.CSRFToken = crypto.createHash('sha1').update(key + '_rd=' + (Math.round(Math.random() * 99999))).digest('hex');
 	}
 
 	public Login(loggedAddress: string, username: string) {
@@ -34,6 +36,10 @@ class Session {
 		return this.key;
 	}
 
+	public GetCSRFToken(): string {
+		return this.CSRFToken;
+	}
+
 	public Logout() {
 		this.loggedAddress = null;
 	}
@@ -77,7 +83,7 @@ class Security {
 	}
 
 	public IsUserLogged(req: express.Request): boolean {
-		let session = this.GetOrCreateSession(req);
+		let session = this.GetSession(req);
 		return session !== null && session.IsLoggedIn();
 	}
 

+ 26 - 0
src/ldapInterface.ts

@@ -1,6 +1,7 @@
 const ldapjs = require("ldapjs");
 import ConfigManager, { LDAPCategory } from "./ConfigLoader";
 import LDAPEntry from "./LDAPEntry";
+import LDAPTree from "./LDAPTree";
 
 interface ILDAPManager {
 	Release(): Promise<void>;
@@ -12,6 +13,9 @@ interface ILDAPManager {
 	TryBind(dn: string, password: string): Promise<boolean>;
 	CheckLoginExists(user: string): Promise<string>;
 	ListEntries(category: LDAPCategory): Promise<Array<LDAPEntry>>;
+	/** @return all dn */
+	GetTree(): Promise<LDAPTree>;
+	Remove(dn: string): Promise<void>;
 }
 
 class LDAPManager implements ILDAPManager {
@@ -108,6 +112,17 @@ class LDAPManager implements ILDAPManager {
 		});
 	}
 
+	public GetTree(): Promise<LDAPTree> {
+		return new Promise(ok => {
+			const rootDn: string = ConfigManager.GetInstance().GetLDAPRoot();
+			this.Search(rootDn, "sub", undefined, ["dn", "ObjectClass"]).then(searchDns => {
+				let result = LDAPTree.CreateRoot();
+				searchDns.forEach((val, dn) => result.push(dn, val.get("objectClass")));
+				ok(result.Compress());
+			});
+		});
+	}
+
 	public ListEntries(category: LDAPCategory): Promise<Array<LDAPEntry>> {
 		return new Promise((ok, ko) => {
 			this.Search(category.GetBaseDn(), category.GetScope(), category.GetFilter() || undefined, category.GetAttributes().map(i => i.mapped)).then(result => {
@@ -128,6 +143,17 @@ class LDAPManager implements ILDAPManager {
 			});
 		});
 	}
+
+	public Remove(dn: string): Promise<void> {
+		return new Promise((ok, ko) => {
+			this.cli.del(dn, (err: any) => {
+				if (err)
+					ko(err["message"]);
+				else
+					ok();
+			});
+		});
+	}
 }
 
 export default class LDAPFactory {

+ 14 - 0
templates/tree.pug

@@ -0,0 +1,14 @@
+
+mixin tree(item)
+  li(class="treeitem",alt=item.FullName())
+    if item.HasChildren()
+      span(data-name=item.GetName())
+      ul(class="treebranch")
+        for i in item.GetChildren()
+          +tree(i)
+    else
+      span=item.GetName()
+
+mixin treeroot(tree)
+  ul(class="treeroot")
+    +tree(tree)

+ 34 - 29
views/category.pug

@@ -1,33 +1,38 @@
 extends layout
 
 block content
-  h1= categoryName
-  ul
-    for i in allCategories
-      li
-        a(href=i.lnk)= i.name
-  table(class='category-list')
-    tr
-      th Address
-      for i in attributes
-        th= i.name
-    for item in items
-      tr(id=DnToLnk(item.GetAddress()) class='category')
-        td= item.GetAddress()
+  div(class="page")
+    input(type="hidden" id="csrf" value=csrf)
+    h1= categoryName
+    ul
+      for i in allCategories
+        li
+          a(href=i.lnk)= i.name
+    table(class='category-list')
+      tr
+        th Address
         for i in attributes
-          - var value = GetAttributeValue(item, i)
-          if value && value.stringValue
-              td= value.stringValue
-          else if value && value.stringArrayValue && value.stringArrayValue.length > 0
-              td
-                ul
-                  for j in value.stringArrayValue
-                    - var link = GetAttributeLink(i, j)
-                    if link
-                      li
-                        a(href=link)= j
-                    else
-                      li= j
-          else
-              td (empty)
-  script(src='/javascripts/category.js', language='javascript')
+          th= i.name
+        th Actions
+      for item in items
+        tr(id=DnToLnk(item.GetAddress()) class='category')
+          td= item.GetAddress()
+          for i in attributes
+            - var value = GetAttributeValue(item, i)
+            if value && value.stringValue
+                td= value.stringValue
+            else if value && value.stringArrayValue && value.stringArrayValue.length > 0
+                td
+                  ul
+                    for j in value.stringArrayValue
+                      - var link = GetAttributeLink(i, j)
+                      if link
+                        li
+                          a(href=link)= j
+                      else
+                        li= j
+            else
+                td (empty)
+          td
+            a(data-dn=item.GetAddress() href="#" class="action-remove") del
+    script(src='/javascripts/category.js', language='javascript')

+ 8 - 2
views/index.pug

@@ -1,5 +1,11 @@
 extends layout
+include ../templates/tree.pug
 
 block content
-  h1= title
-  p Welcome to #{title}
+    div(id="menuBar")
+      div
+        +treeroot(tree)
+        script(src='/javascripts/tree.js', language='javascript')
+        script.
+          makeTree(document.querySelectorAll("#menuBar>div>ul.treeroot")[0]);
+    iframe(id="page",src=rootSrc)