Browse Source

Show LDAP attribute values

isundil 4 years ago
parent
commit
441df97463
12 changed files with 366 additions and 38 deletions
  1. 4 0
      LDAPManager.njsproj
  2. 2 0
      config.js.sample
  3. 18 0
      public/stylesheets/main.css
  4. 80 2
      routes/entity.ts
  5. 4 6
      routes/index.ts
  6. 2 8
      routes/login.ts
  7. 5 0
      src/ConfigLoader.ts
  8. 10 15
      src/LDAPEntry.ts
  9. 145 0
      src/LDAPSchema.ts
  10. 7 0
      src/Security.ts
  11. 78 6
      src/ldapInterface.ts
  12. 11 1
      views/entity.pug

+ 4 - 0
LDAPManager.njsproj

@@ -51,6 +51,7 @@
     <Content Include="package.json" />
     <Content Include="public\stylesheets\main.css" />
     <Content Include="README.md" />
+    <Content Include="views\entity.pug" />
     <Content Include="views\login.pug" />
     <Content Include="views\index.pug" />
     <Content Include="views\layout.pug" />
@@ -77,6 +78,9 @@
     <TypeScriptCompile Include="src\LDAPInterface.ts">
       <SubType>Code</SubType>
     </TypeScriptCompile>
+    <TypeScriptCompile Include="src\LDAPSchema.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
     <TypeScriptCompile Include="src\LDAPTree.ts">
       <SubType>Code</SubType>
     </TypeScriptCompile>

+ 2 - 0
config.js.sample

@@ -10,6 +10,8 @@ module.exports = {
 	LDAP_ROOT: "dc=foo,dc=bar",
 	LDAP_MAP_ADDRESS: "dn",
 
+	NOLOGIN: false,
+
 	LDAP_CATEGORIES: [
 		{
 			name: "",

+ 18 - 0
public/stylesheets/main.css

@@ -41,4 +41,22 @@ iframe#page {
 }
 div.page {
     margin: 0 1em;
+}
+fieldset.LDAPClass {
+    margin-bottom: 1em;
+}
+fieldset.LDAPClass > legend {
+    padding: 0 1em;
+}
+label.LDAPAttribute {
+    display: block;
+    margin: 3px;
+}
+label.LDAPAttribute>span {
+    display: inline-block;
+    width: 250px;
+}
+label.LDAPAttribute > input {
+    display: inline-block;
+    width: 450px;
 }

+ 80 - 2
routes/entity.ts

@@ -5,12 +5,90 @@ import express = require('express');
 const router = express.Router();
 import Security from '../src/Security';
 import { ILDAPManager } from '../src/ldapInterface';
+import { LDAPSchemaObjectClass } from '../src/LDAPSchema';
+
+function LDAPEntryToAttributes(entry: Map<string, Array<string>>): any {
+    let result: any = [];
+    for (let [key, val] of entry)
+        val.forEach(v => result.push({ key: key, val: v }));
+    return result;
+}
+
+class AttributesByClasses {
+    public constructor(entry: Map<string, string[]>, classes: Map<string, LDAPSchemaObjectClass>) {
+        let oClasses: LDAPSchemaObjectClass[] = new Array();
+
+        for (let eClass of entry.get("objectClass") || []) {
+            this.fEntries.set(eClass, new Map());
+            let cl = classes.get(eClass);
+            cl && oClasses.push(cl);
+		}
+        for (let [i, j] of entry) {
+            let found = false;
+            for (let oc of oClasses) {
+                if (oc.HasAttribute(i)) {
+                    this.fEntries.get(oc.GetName())?.set(i, j);
+                    found = true;
+                    break;
+				}
+            }
+            if (!found)
+                this.fUnmapped.set(i, j);
+        }
+        for (let klass of oClasses) {
+            let className = klass.GetName();
+            let classContent = this.fEntries.get(className);
+
+            if (!classContent) {
+                classContent = new Map<string, string[]>();
+                this.fEntries.set(className, classContent);
+            }
+            for (let attr of klass.ListAttributes())
+                !classContent.has(attr) && classContent.set(attr, new Array());
+            this.fEntries.set(className, classContent);
+		}
+    }
+
+    public ToMap(): any {
+        let result: any = {};
+        for (let [ocKey, vals] of this.fEntries) {
+            let classContent: any = {};
+            for (let [i, j] of vals) {
+                classContent[i] = j.length ? j : [""];
+            }
+            result[ocKey] = classContent;
+        }
+        return result;
+    }
+
+    public GetObjectClasses(): any {
+        let result: any = [];
+        for (let [ocKey, val] of this.fEntries)
+            result.push(ocKey);
+        result.sort();
+        return result;
+	}
+
+    private fEntries: Map<string, Map<string, string[]>> = new Map();
+    private fUnmapped: Map<string, string[]> = new Map();
+}
 
 router.get('/:dn', (req: express.Request, res: express.Response) => {
     if (!Security.requireLoggedUser(req, res))
         return;
-    res.render('entity', {
-        dn: req.params.dn
+    let ldapManager: ILDAPManager;
+    let entry: Map<string, string[]>;
+    req.ldapManager.GetInstance()
+        .then(_ldapManager => { ldapManager = _ldapManager; return ldapManager.GetEntry(req.params.dn) })
+        .then(_entry => { entry = _entry; return ldapManager.GetSchema() }).then(schema => {
+            let classes: AttributesByClasses = new AttributesByClasses(entry, schema);
+            let dn = (entry.get("dn") || [])[0];
+
+            res.render('entity', {
+                dn: dn || req.params.dn,
+                attributes: classes.ToMap(),
+                classes: classes.GetObjectClasses()
+            });
     });
 });
 

+ 4 - 6
routes/index.ts

@@ -30,12 +30,10 @@ function DnToLink(dn: string): string {
 router.get('/', (req: express.Request, res: express.Response) => {
     if (!Security.requireLoggedUser(req, res))
         return;
-    req.ldapManager.GetInstance().then((ldap: ILDAPManager): void => {
-        ldap.GetTree().then(root => {
-            res.render("index", {
-                tree: root.GetChildren()[0],
-                DnToLnk: DnToLink
-            });
+    req.ldapManager.GetInstance().then(ldap => ldap.GetTree()).then(root => {
+        res.render("index", {
+            tree: root.GetChildren()[0],
+            DnToLnk: DnToLink
         });
     });
 });

+ 2 - 8
routes/login.ts

@@ -36,11 +36,7 @@ function ManageLogin(req: express.Request, res: express.Response, ldap: ILDAPMan
 }
 
 route_login.get('/', (req: express.Request, res: express.Response) => {
-	req.ldapManager.GetInstance().then(man => {
-		man.GetTree().then(tree => {
-			ManageLogin(req, res, null, null);
-		});
-	});
+	ManageLogin(req, res, null, null);
 });
 route_login.post('/', (req: express.Request, res: express.Response) => {
 	req.ldapManager.GetInstance().then(ldap => {
@@ -48,9 +44,7 @@ route_login.post('/', (req: express.Request, res: express.Response) => {
 	});
 });
 route_logout.get('/', (req: express.Request, res: express.Response) => {
-	let session = Security.GetSession(req);
-	if (session)
-		session.Logout();
+	Security.GetSession(req)?.Logout();
 	res.clearCookie("sessId");
 	RouterUtils.Redirect(res, "/");
 });

+ 5 - 0
src/ConfigLoader.ts

@@ -43,6 +43,7 @@ export interface IConfigLoader {
 	GetLDAPUrls(): Array<string>;
 	GetBindDn(): string;
 	GetBindPassword(): string;
+	GetNoLogin(): boolean;
 	GetLoginBase(): string;
 	GetLoginScope(): string;
 	GetLoginFilter(username: string): string;
@@ -62,6 +63,8 @@ class ConfigLoader implements IConfigLoader {
 		this.fLogin.fBase = config["LDAP_LOGIN_BASE"];
 		this.fLogin.fScope = config["LDAP_LOGIN_SCOPE"];
 		this.fLogin.fFilter = config["LDAP_LOGIN_FILTER"];
+
+		this.fNoLogin = !!config["NOLOGIN"];
 	}
 
 	public GetLDAPRoot(): string { return this.fServer.root; }
@@ -80,8 +83,10 @@ class ConfigLoader implements IConfigLoader {
 	public GetLoginBase(): string { return this.fLogin.fBase; }
 	public GetLoginScope(): string { return this.fLogin.fScope; }
 	public GetLoginFilter(login: string): string { return this.fLogin.fFilter(login); }
+	public GetNoLogin(): boolean { return this.fNoLogin; }
 
 	private fCategories: Array<LDAPCategory>;
+	private fNoLogin: boolean = false;
 	private fServer = {
 		urls: [],
 		root: "",

+ 10 - 15
src/LDAPEntry.ts

@@ -3,27 +3,19 @@ import { LDAPAttribute } from './ConfigLoader';
 
 class AttributeValue {
 	public stringValue: string|null = null;
-	public stringArrayValue: Array<string>|null = null;
+	public stringArrayValue: string[]|null = null;
 
-	public constructor(def: LDAPAttribute, item: any) {
+	public constructor(def: LDAPAttribute, item: string[]) {
 		if (def.type === "stringArray" || def.type.startsWith("entryArray#")) {
 			this.stringArrayValue = new Array();
-			if (item instanceof Array)
+			if (item instanceof Array) {
 				for (let i of item) {
 					if (!this.IsFiltered(def, i))
 						this.stringArrayValue.push(i);
 				}
-			else if (typeof (item) === "string") {
-				if (!this.IsFiltered(def, item))
-					this.stringArrayValue.push(item);
 			}
 		} else if (def.type === "string") {
-			if (item instanceof Array) {
-				this.stringValue = item[0];
-			}
-			else if (typeof (item) === "string") {
-				this.stringValue = item;
-			}
+			this.stringValue = item[0];
 		}
 	}
 
@@ -36,11 +28,14 @@ export default class LDAPEntry {
 	protected address: string;
 	protected attributes: Map<LDAPAttribute, AttributeValue|null>;
 
-	public constructor(attributes: Iterable<LDAPAttribute>, addr: string, userdata: Map<string, string>) {
+	public constructor(attributes: Iterable<LDAPAttribute>, addr: string, userdata: Map<string, string[]>) {
 		this.address = addr;
 		this.attributes = new Map<LDAPAttribute, AttributeValue>();
-		for (var i of attributes)
-			this.attributes.set(i, new AttributeValue(i, userdata.get(i.mapped)));
+		for (var i of attributes) {
+			let val:string[]|undefined = userdata.get(i.mapped);
+			if (val)
+				this.attributes.set(i, new AttributeValue(i, val));
+		}
 	}
 
 	private PutAttributeByName(attrName: string, attrValue: any): boolean {

+ 145 - 0
src/LDAPSchema.ts

@@ -0,0 +1,145 @@
+import { findSourceMap } from "node:module";
+import { markAsUntransferable } from "node:worker_threads";
+import { LDAPAttribute } from "./ConfigLoader";
+
+enum ClassType {
+	eUnknown,
+	eAbstract,
+	eStructural,
+	eAuxiliary
+}
+
+abstract class LDAPSchemaItem {
+	public constructor(def: string) {
+		this.fDefinition = def;
+		this.fName = def.match(/NAME[\s\(]+'([^']+)'/)?.pop() || "";
+		this.fDescription = def.match(/DESC\s+'([^']+)'/)?.pop() || "";
+		this.fAliases = (def.match(/NAME\s*\(\s*([^\)]+)\)/)?.pop() || "").split(/\s+/).map(i => i.startsWith("'") ? i.substr(1, i.length - 2) : i).filter(i => i.length);
+		if (this.fAliases.indexOf(this.fName) === -1)
+			this.fAliases.push(this.fName);
+	}
+
+	public GetName(): string {
+		return this.fName;
+	}
+
+	public GetDescription(): string {
+		return this.fDescription;
+	}
+
+	public Match(str: string): boolean {
+		for (let i of this.fAliases)
+			if (i.toLowerCase() == str)
+				return true;
+		return false;
+	}
+
+	protected readonly fDefinition: string;
+	private fName: string;
+	private fAliases: Array<string>;
+	private fDescription: string;
+}
+
+function Find(key: string, arr: Iterable<LDAPSchemaItem>): LDAPSchemaItem | null {
+	key = key.toLowerCase();
+	for (let i of arr)
+		if (i.Match(key))
+			return i;
+	return null;
+}
+
+export class LDAPSchemaAttribute extends LDAPSchemaItem{
+	public constructor(def: string) {
+		super(def);
+		this.fSyntax = def.match(/SYNTAX\s+([0-9\.]+)/)?.pop() || null;
+	}
+
+	private fSyntax: string|null;
+}
+
+export class LDAPSchemaObjectClass extends LDAPSchemaItem {
+	public constructor(def: string, attributeDic: Map<string, LDAPSchemaAttribute>) {
+		super(def);
+		const abstract = /\s+ABSTRACT\s+/.test(def) ? 1 : 0;
+		const aux = /\s+AUXILIARY\s+/.test(def) ? 1 : 0;
+		const structural = /\s+STRUCTURAL\s+/.test(def) ? 1 : 0;
+		if (abstract + aux + structural !== 1)
+			this.fType = ClassType.eUnknown;
+		else if (abstract)
+			this.fType = ClassType.eAbstract;
+		else if (aux)
+			this.fType = ClassType.eAuxiliary;
+		else if (structural)
+			this.fType = ClassType.eStructural;
+		else
+			this.fType = ClassType.eUnknown;
+		this.fParent = def.match(/\s+SUP\s+([^\s]+)\s/)?.pop() || null;
+	}
+
+	private defToAttributes(type: string): string[] {
+		const reg = type === "MUST" ? /\s+MUST\s+(\w+|\([^\)]+\))/ : /\s+MAY\s+(\w+|\([^\)]+\))/;
+		const res = (this.fDefinition.match(reg) || [])[1];
+		if (!res)
+			return new Array();
+		let result = new Array();
+
+		if (!res.startsWith('(')) {
+			result.push(res.trim());
+			return result;
+		}
+		for (let i of res.substr(1, res.length -2).split("$"))
+			result.push(i.trim());
+		return result;
+	}
+
+	public Consolidate(classes: Map<string, LDAPSchemaObjectClass>, attributes: Map<string, LDAPSchemaAttribute>) {
+		if (this.fFinalized)
+			return;
+		this.fFinalized = true;
+		let parent = this.fParent ? (Find(this.fParent, classes.values()) as LDAPSchemaObjectClass|null) : null;
+		if (parent) {
+			parent.Consolidate(classes, attributes);
+			for (let [key, attr] of parent.fMayAttributes) {
+				if (!this.fMustAttributes.has(key))
+					this.fMayAttributes.set(key, attr);
+			}
+			for (let [key, attr] of parent.fMustAttributes) {
+				this.fMustAttributes.set(key, attr);
+				if (this.fMayAttributes.has(key))
+					this.fMayAttributes.delete(key);
+			}
+		}
+		for (let mustAttr of this.defToAttributes("MUST")) {
+			if (this.fMustAttributes.has(mustAttr))
+				continue;
+			if (this.fMayAttributes.has(mustAttr))
+				this.fMayAttributes.delete(mustAttr);
+			let attr = Find(mustAttr, attributes.values()) as LDAPSchemaAttribute|null;
+			attr && this.fMustAttributes.set(mustAttr, attr);
+		}
+		for (let mayAttr of this.defToAttributes("MAY")) {
+			if (this.fMayAttributes.has(mayAttr) || this.fMustAttributes.has(mayAttr))
+				continue;
+			let attr = Find(mayAttr, attributes.values()) as LDAPSchemaAttribute | null;
+			attr && this.fMayAttributes.set(mayAttr, attr);
+		}
+		return;
+	}
+
+	public HasAttribute(key: string): boolean {
+		return !!(Find(key, this.fMayAttributes.values()) || Find(key, this.fMustAttributes.values()));
+	}
+
+	public ListAttributes(): string[] {
+		let result = new Array();
+		for (let [key, _] of this.fMustAttributes) result.push(key);
+		for (let [key, _] of this.fMayAttributes) result.push(key);
+		return result;
+	}
+
+	private fParent: string|null;
+	private fType: ClassType;
+	private fFinalized: boolean = false;
+	private fMayAttributes: Map<string, LDAPSchemaAttribute> = new Map();
+	private fMustAttributes: Map<string, LDAPSchemaAttribute> = new Map();
+}

+ 7 - 0
src/Security.ts

@@ -2,6 +2,7 @@
 import express = require('express');
 import RouterUtils from './RouterUtils';
 import { ILDAPManager } from './ldapInterface';
+import ConfigManager from './ConfigLoader';
 const crypto = require('crypto');
 
 const START_DATE_TS = (new Date()).getTime();
@@ -84,6 +85,12 @@ class Security {
 
 	public IsUserLogged(req: express.Request): boolean {
 		let session = this.GetSession(req);
+		if (ConfigManager.GetInstance().GetNoLogin()) {
+			if (session === null)
+				session = this.CreateSession(req);
+			if (!session.IsLoggedIn())
+				session.Login("NoLogin", "NoLogin");
+		}
 		return session !== null && session.IsLoggedIn();
 	}
 

+ 78 - 6
src/ldapInterface.ts

@@ -1,5 +1,6 @@
 const ldapjs = require("ldapjs");
-import ConfigManager, { LDAPCategory } from "./ConfigLoader";
+import ConfigManager, { LDAPAttribute, LDAPCategory } from "./ConfigLoader";
+import { LDAPSchemaAttribute, LDAPSchemaObjectClass } from './LDAPSchema';
 import LDAPEntry from "./LDAPEntry";
 import LDAPTree from "./LDAPTree";
 
@@ -12,10 +13,12 @@ interface ILDAPManager {
 	 */
 	TryBind(dn: string, password: string): Promise<boolean>;
 	CheckLoginExists(user: string): Promise<string>;
-	ListEntries(category: LDAPCategory): Promise<Array<LDAPEntry>>;
+	ListEntries(category: LDAPCategory): Promise<LDAPEntry[]>;
 	/** @return all dn */
 	GetTree(): Promise<LDAPTree>;
+	GetEntry(dn: string): Promise<Map<string, string[]>>;
 	Remove(dn: string): Promise<void>;
+	GetSchema(): Promise<Map<string, LDAPSchemaObjectClass>>;
 }
 
 class LDAPManager implements ILDAPManager {
@@ -66,7 +69,7 @@ class LDAPManager implements ILDAPManager {
 		});
 	}
 
-	public Search(base: string, scope: string, filter: string|undefined, attributes: Array<string>): Promise<Map<string, Map<string, any>>> {
+	private Search(base: string, scope: string, filter: string|undefined =undefined, attributes: string[]|undefined =undefined): Promise<Map<string, Map<string, string[]>>> {
 		return new Promise((ok, ko) => {
 			this.cli.search(base, { scope: scope, filter: filter, attributes: attributes, paged: false }, (err: any, res: any) => {
 				if (err) {
@@ -79,9 +82,19 @@ class LDAPManager implements ILDAPManager {
 
 				res.on('searchEntry', (i: any) => {
 					if (error) return;
-					let LDAPEntry = new Map<string, any>();
+					let LDAPEntry = new Map<string, string[]>();
 					for (let attr in i.object) {
 						let value = i.object[attr];
+						if (Array.isArray(value)) {
+							let arr = new Array();
+							for (let j of value)
+								arr.push(j);
+							value = arr;
+						} else {
+							let arr = new Array();
+							arr.push(value);
+							value = arr;
+						}
 						LDAPEntry.set(attr, value);
 					}
 					result.set(i.dn, LDAPEntry);
@@ -99,6 +112,19 @@ class LDAPManager implements ILDAPManager {
 		});
 	}
 
+	public GetEntry(dn: string): Promise<Map<string, string[]>> {
+		return new Promise((ok, ko) => {
+			this.Search(dn, "sub").then(result => {
+				for (let [key, val] of result) {
+					ok(val);
+					return;
+				}
+			}).catch(err => {
+				ko(err);
+			});
+		});
+	}
+
 	public CheckLoginExists(user: string): Promise<string> {
 		return new Promise((ok, ko) => {
 			let config = ConfigManager.GetInstance();
@@ -117,13 +143,16 @@ class LDAPManager implements ILDAPManager {
 			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")));
+				searchDns.forEach((val, dn) => {
+					let classes = val.get("objectClass");
+					classes && result.push(dn, classes);
+				});
 				ok(result.Compress());
 			});
 		});
 	}
 
-	public ListEntries(category: LDAPCategory): Promise<Array<LDAPEntry>> {
+	public ListEntries(category: LDAPCategory): Promise<LDAPEntry[]> {
 		return new Promise((ok, ko) => {
 			this.Search(category.GetBaseDn(), category.GetScope(), category.GetFilter() || undefined, category.GetAttributes().map(i => i.mapped)).then(result => {
 				let userArray = new Array();
@@ -154,6 +183,49 @@ class LDAPManager implements ILDAPManager {
 			});
 		});
 	}
+
+	public GetSchema(): Promise<Map<string, LDAPSchemaObjectClass>> {
+		return new Promise((ok, ko) => {
+			const rootDn = ConfigManager.GetInstance().GetLDAPRoot();
+			let subschema: string;
+			this.Search(rootDn, "base", undefined, ["subschemaSubentry"]).then(result => {
+				const baseItem = result?.get(rootDn)?.get("subschemaSubentry");
+				const _subschema = baseItem && baseItem.length ? baseItem[0] : null;
+				if (!_subschema) {
+					console.error("Cannot find schema for base " + rootDn);
+					ko("Schema not found");
+					return;
+				}
+				subschema = _subschema;
+				return this.Search(subschema, "base", undefined, ["attributeTypes", "objectClasses"]);
+			}).then(result => {
+				const attributesArr = result?.get(subschema)?.get("attributeTypes");
+				const classesArr = result?.get(subschema)?.get("objectClasses");
+				if (!attributesArr || !classesArr) {
+					console.error("Cannot find schema definition for " + rootDn);
+					ko("Schema definition not found");
+					return;
+				}
+				let attributes: Map<string, LDAPSchemaAttribute> = new Map();
+				let schemas: Map<string, LDAPSchemaObjectClass> = new Map();
+
+				for (let i of attributesArr) {
+					let attr = new LDAPSchemaAttribute(i);
+					attributes.set(attr.GetName(), attr);
+				}
+				for (let i of classesArr) {
+					let schema = new LDAPSchemaObjectClass(i, attributes);
+					schemas.set(schema.GetName(), schema);
+				}
+				for (let [_, schema] of schemas)
+					schema.Consolidate(schemas, attributes);
+				ok(schemas);
+			}).catch(err => {
+				console.error("Failed to retreive schema: ", err);
+				ko(err);
+			});
+		});
+	}
 }
 
 export default class LDAPFactory {

+ 11 - 1
views/entity.pug

@@ -2,5 +2,15 @@ extends layout
 
 block content
   div(class="page")
-    input(type="hidden" id="csrf" value=csrf)
     h1=dn
+    for className in classes
+      fieldset(class="LDAPClass")
+        legend
+          h3=className
+        ul
+          each valArr, key in attributes[className]
+            for val in valArr
+              li
+                label(class="LDAPAttribute")
+                  span=key
+                  input(type="text",value=val)