Skip to content

Commit

Permalink
Improving explorer & tree behaviors, adding reload keybinding and --s…
Browse files Browse the repository at this point in the history
…chema flag.
  • Loading branch information
Macmod committed Feb 27, 2024
1 parent 2b97fc3 commit aaba6eb
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 110 deletions.
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@

# Summary

* [Screenshots](https://github.com/Macmod/godap?tab=readme-ov-file#screenshots)
* [Features](https://github.com/Macmod/godap?tab=readme-ov-file#features)
* [Installation](https://github.com/Macmod/godap?tab=readme-ov-file#installation)
* [Usage](https://github.com/Macmod/godap?tab=readme-ov-file#usage)
* [Flags](https://github.com/Macmod/godap?tab=readme-ov-file#flags)
* [Keybindings](https://github.com/Macmod/godap?tab=readme-ov-file#keybindings)
* [Contributing](https://github.com/Macmod/godap?tab=readme-ov-file#contributing)
* [Acknowledgements](https://github.com/Macmod/godap?tab=readme-ov-file#acknowledgements)
* [Disclaimers](https://github.com/Macmod/godap?tab=readme-ov-file#disclaimers)
* [Screenshots](https://github.com/Macmod/godap#screenshots)
* [Features](https://github.com/Macmod/godap#features)
* [Installation](https://github.com/Macmod/godap#installation)
* [Usage](https://github.com/Macmod/godap#usage)
* [Flags](https://github.com/Macmod/godap#flags)
* [Keybindings](https://github.com/Macmod/godap#keybindings)
* [Contributing](https://github.com/Macmod/godap#contributing)
* [Acknowledgements](https://github.com/Macmod/godap#acknowledgements)
* [Disclaimers](https://github.com/Macmod/godap#disclaimers)

# Screenshots

Expand Down Expand Up @@ -87,8 +87,6 @@ To connect to LDAP through a SOCKS proxy include the flag `-x schema://ip:port`,

You can also change the address of your proxy using the `l` keybinding.

Note that when using a proxy you might want to consider including the `-M` flag (enable cache) to avoid a terribly slow UI.

## Flags

* `-u`,`--username` - Username for bind
Expand All @@ -102,33 +100,36 @@ Note that when using a proxy you might want to consider including the `-M` flag
* `-A`,`--expand` - Expand multi-value attributes (default: `true`, to change use `-expand=false`)
* `-L`,`--limit` - Number of attribute values to render for multi-value attributes when `-expand` is `true` (default: `20`)
* `-F`,`--format` - Format attributes into human-readable values (default: `true`, to change use `-format=false`)
* `-M`,`--cache` - Keep loaded entries in memory while the program is open and don't query them again (default: `false`)
* `-M`,`--cache` - Keep loaded entries in memory while the program is open and don't query them again (default: `true`)
* `-I`,`--insecure` - Skip TLS verification for LDAPS/StartTLS (default: `false`)
* `-S`,`--ldaps` - Use LDAPS for initial connection (default: `false`)
* `-G`,`--paging` - Paging size for regular queries (default: `800`)
* `-d`,`--domain` - Domain for NTLM bind
* `-H`,`--hashes` - Hashes for NTLM bind
* `--hashfile` - Path to a file containing the hashes for NTLM bind
* `-x`,`--socks` - URI of SOCKS proxy to use for connection (supports `socks4://`, `socks4a://` or `socks5://` schemas)
* `-k`,`--schema` - Load GUIDs from schema on initialization (default: `false`)

## Keybindings

| Keybinding | Context | Action |
| --------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------- |
| `Ctrl` + `J` | Global | Next panel |
| `Ctrl` + `Enter` (or `Ctrl` + `J`) | Global | Next panel |
| `f` / `F` | Global | Toggle attribute formatting |
| `e` / `E` | Global | Toggle emojis |
| `c` / `C` | Global | Toggle colors |
| `a` / `A` | Global | Toggle attribute expansion for multi-value attributes |
| `l` / `L` | Global | Change current server address & credentials |
| `r` / `R` | Global | Reconnect to the server |
| `u` / `U` | Global | Upgrade connection to use TLS (with StartTLS) |
| `Ctrl` + `r / R` | Global | Reconnect to the server |
| `Ctrl` + `u / U` | Global | Upgrade connection to use TLS (with StartTLS) |
| `r` / `R` | Explorer panel | Reload the attributes and children of the selected object |
| `Ctrl` + `n / N` | Explorer panel | Create a new object under the selected object |
| `Ctrl` + `s / S` | Explorer panel | Export all loaded nodes in the selected subtree into a JSON file |
| `Ctrl` + `p / P` | Explorer panel | Change the password of the selected user or computer account |
| `Ctrl` + `a / A` | Explorer panel | Update the userAccountControl of the object interactively |
| `Ctrl` + `l / L` | Explorer panel | Move the selected object to another location |
| `Delete` | Explorer panel | Delete the selected object |
| `r` / `R` | Attributes panel | Reload the attributes for the selected object |
| `Ctrl` + `e / E` | Attributes panel | Edit the selected attribute of the selected object |
| `Ctrl` + `n / N` | Attributes panel | Create a new attribute in the selected object |
| `Delete` | Attributes panel | Delete the selected attribute of the selected object |
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# TODO (priority)

* Feature: Monitor object for real-time changes
* Feature: Keybinding to refresh the page contents
* Feature: Load initial cache from file

# TODO (later)

Expand Down
181 changes: 137 additions & 44 deletions explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io/ioutil"
"sort"
"strconv"
"sync"
"time"

"github.com/Macmod/godap/utils"
Expand All @@ -15,13 +16,50 @@ import (
"github.com/rivo/tview"
)

var explorerPage *tview.Flex
var treePanel *tview.TreeView
var attrsPanel *tview.Table
var rootDNInput *tview.InputField
var searchFilterInput *tview.InputField
type SafeCache struct {
entries map[string]*ldap.Entry
lock sync.Mutex
}

func (sc *SafeCache) Delete(key string) {
sc.lock.Lock()
delete(sc.entries, key)
sc.lock.Unlock()
}

func (sc *SafeCache) Clear() {
sc.lock.Lock()
clear(sc.entries)
sc.lock.Unlock()
}

func (sc *SafeCache) Add(key string, val *ldap.Entry) {
sc.lock.Lock()
sc.entries[key] = val
sc.lock.Unlock()
}

func (sc *SafeCache) Get(key string) (*ldap.Entry, bool) {
sc.lock.Lock()
defer sc.lock.Unlock()
entry, ok := sc.entries[key]
return entry, ok
}

var (
cache SafeCache
explorerPage *tview.Flex
treePanel *tview.TreeView
attrsPanel *tview.Table
rootDNInput *tview.InputField
searchFilterInput *tview.InputField
)

func initExplorerPage() {
cache = SafeCache{
entries: make(map[string]*ldap.Entry),
}

treePanel = tview.NewTreeView()

rootNode = renderPartialTree(rootDN, searchFilter)
Expand Down Expand Up @@ -85,9 +123,20 @@ func initExplorerPage() {

func expandTreeNode(node *tview.TreeNode) {
if !node.IsExpanded() {
//experiment: go loadChildren(node)
loadChildren(node)
node.SetExpanded(true)
if len(node.GetChildren()) == 0 {
go func() {
updateLog("Loading children ("+node.GetReference().(string)+")", "yellow")
loadChildren(node)

node.SetExpanded(true)

updateLog("Loaded children ("+node.GetReference().(string)+")", "green")

app.Draw()
}()
} else {
node.SetExpanded(true)
}
}
}

Expand Down Expand Up @@ -143,10 +192,31 @@ func treePanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {
}

parentNode := getParentNode(currentNode)
baseDN := currentNode.GetReference().(string)

switch event.Rune() {
case 'r', 'R':
go func() {
updateLog("Reloading node "+baseDN, "yellow")

cache.Delete(baseDN)
reloadAttributesPanel(currentNode, false)

unloadChildren(currentNode)
loadChildren(currentNode)

updateLog("Node "+baseDN+" reloaded", "green")

app.Draw()
}()

return event
}

switch event.Key() {
case tcell.KeyRight:
expandTreeNode(currentNode)
return nil
case tcell.KeyLeft:
if currentNode.IsExpanded() { // Collapse current node
collapseTreeNode(currentNode)
Expand All @@ -170,7 +240,7 @@ func treePanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {
if buttonLabel == "Yes" {
err := lc.DeleteObject(baseDN)
if err == nil {
delete(loadedDNs, baseDN)
cache.Delete(baseDN)
updateLog("Object deleted: "+baseDN, "green")

idx := findEntryInChildren(baseDN, parentNode)
Expand All @@ -192,8 +262,6 @@ func treePanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {

app.SetRoot(promptModal, false).SetFocus(promptModal)
case tcell.KeyCtrlN:
baseDN := currentNode.GetReference().(string)

createObjectForm := tview.NewForm().
AddDropDown("Object Type", []string{"OrganizationalUnit", "Container", "User", "Group", "Computer"}, 0, nil).
AddInputField("Object Name", "", 0, nil, nil).
Expand Down Expand Up @@ -259,7 +327,7 @@ func treePanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {
exportMap := make(map[string]*ldap.Entry)
currentNode.Walk(func(node, parent *tview.TreeNode) bool {
nodeDN := node.GetReference().(string)
exportMap[nodeDN] = loadedDNs[nodeDN]
exportMap[nodeDN], _ = cache.Get(nodeDN)
return true
})

Expand All @@ -284,8 +352,9 @@ func treePanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {
updateUacForm.SetItemPadding(0)

var checkboxState int = 0
if loadedDNs[baseDN] != nil {
uacValue, err := strconv.Atoi(loadedDNs[baseDN].GetAttributeValue("userAccountControl"))
obj, _ := cache.Get(baseDN)
if obj != nil {
uacValue, err := strconv.Atoi(obj.GetAttributeValue("userAccountControl"))
if err == nil {
checkboxState = uacValue
} else {
Expand Down Expand Up @@ -355,16 +424,38 @@ func treePanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {
}

func treePanelChangeHandler(node *tview.TreeNode) {
reloadAttributesPanel(node, cacheEntries)
go func() {
// TODO: Implement cancellation
reloadAttributesPanel(node, cacheEntries)
}()
}

func attrsPanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {
currentNode := treePanel.GetCurrentNode()
if currentNode == nil {
return event
}

baseDN := currentNode.GetReference().(string)

switch event.Rune() {
case 'r', 'R':
updateLog("Reloading node "+baseDN, "yellow")

cache.Delete(baseDN)
reloadAttributesPanel(currentNode, false)

updateLog("Node "+baseDN+" reloaded", "green")

go func() {
app.Draw()
}()
return event
}

switch event.Key() {
case tcell.KeyDelete:
currentNode := treePanel.GetCurrentNode()
attrRow, _ := attrsPanel.GetSelection()

baseDN := currentNode.GetReference().(string)
attrName := attrsPanel.GetCell(attrRow, 0).Text

promptModal := tview.NewModal().
Expand All @@ -376,10 +467,10 @@ func attrsPanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {
if err != nil {
updateLog(fmt.Sprint(err), "red")
} else {
delete(loadedDNs, baseDN)
updateLog("Attribute deleted: "+attrName+" from "+baseDN, "green")

cache.Delete(baseDN)
reloadAttributesPanel(currentNode, cacheEntries)

updateLog("Attribute deleted: "+attrName+" from "+baseDN, "green")
}
}

Expand All @@ -388,11 +479,6 @@ func attrsPanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {

app.SetRoot(promptModal, false).SetFocus(promptModal)
case tcell.KeyCtrlN:
currentNode := treePanel.GetCurrentNode()
if currentNode == nil {
return event
}

createAttrForm := tview.NewForm()
createAttrForm.SetInputCapture(handleEscapeToTree)

Expand All @@ -414,10 +500,10 @@ func attrsPanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {
if err != nil {
updateLog(fmt.Sprint(err), "red")
} else {
delete(loadedDNs, baseDN)
updateLog("Attribute added: "+attrName+" to "+baseDN, "green")

cache.Delete(baseDN)
reloadAttributesPanel(currentNode, cacheEntries)

updateLog("Attribute added: "+attrName+" to "+baseDN, "green")
}

app.SetRoot(appPanel, false).SetFocus(treePanel)
Expand All @@ -427,25 +513,32 @@ func attrsPanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {

app.SetRoot(createAttrForm, true).SetFocus(createAttrForm)
case tcell.KeyDown:
selectedRow, _ := attrsPanel.GetSelection()
selectedRow, selectedCol := attrsPanel.GetSelection()
rowCount := attrsPanel.GetRowCount()

s := selectedRow + 1
for s < attrsPanel.GetRowCount() && attrsPanel.GetCell(s, 0).Text == "" {
s = s + 1
}
if selectedCol == 0 {
s := selectedRow + 1
for s < rowCount && attrsPanel.GetCell(s, 0).Text == "" {
s = s + 1
}

if s != selectedRow {
attrsPanel.Select(s-1, 0)
if s == rowCount {
attrsPanel.Select(selectedRow-1, 0)
} else if s != selectedRow {
attrsPanel.Select(s-1, 0)
}
}
case tcell.KeyUp:
selectedRow, _ := attrsPanel.GetSelection()
s := selectedRow - 1
for s > 0 && attrsPanel.GetCell(s, 0).Text == "" {
s = s - 1
}
selectedRow, selectedCol := attrsPanel.GetSelection()
if selectedCol == 0 {
s := selectedRow - 1
for s > 0 && attrsPanel.GetCell(s, 0).Text == "" {
s = s - 1
}

if s != selectedRow {
attrsPanel.Select(s+1, 0)
if s != selectedRow {
attrsPanel.Select(s+1, 0)
}
}
}

Expand Down Expand Up @@ -485,7 +578,7 @@ func explorerPageKeyHandler(event *tcell.EventKey) *tcell.EventKey {
baseDN := currentNode.GetReference().(string)
attrName := attrsPanel.GetCell(attrRow, 0).Text

entry := loadedDNs[baseDN]
entry, _ := cache.Get(baseDN)
attrVals := entry.GetAttributeValues(attrName)
if len(attrVals) == 0 {
return event
Expand Down
2 changes: 1 addition & 1 deletion help.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func initHelpPage() {
| (___) || (___) || (__/ )| ) ( || )
(_______)(_______)(______/ |/ \||/
v2.0.0 - Ace of Spades
v2.1.0
`

keybindings := [][]string{
Expand Down
Loading

0 comments on commit aaba6eb

Please sign in to comment.