diff --git a/README.md b/README.md
index 29b10df..9c33202 100644
--- a/README.md
+++ b/README.md
@@ -20,20 +20,21 @@
# Features
+* 𧊠Supports authentication with password, NTLM hash, Kerberos ticket or PEM/PKCS#12 certificate
* đī¸ Formats date/time, boolean and other categorical attributes into readable text
* đ Pretty colors & cool emojis
* đ LDAPS & StartTLS support
* ⊠Fast explorer that loads objects on demand
* đ Recursive object search bundled with useful saved searches
-* đĨ Group members & user groups lookup
+* đĨ Flexible group members & user groups lookups
* đĄ Supports creation, editing and removal of objects and attributes
* đ Supports moving and renaming objects
* đī¸ Supports searching deleted & recycled objects
* đ Supports exporting specific subtrees of the directory into JSON files
-* đ GPO Viewer
-* đ ADIDNS Viewer
* đšī¸ Interactive userAccountControl editor
-* đĨ Interactive DACL editor
+* đĨ Interactive DACL viewer + editor
+* đ Interactive ADIDNS viewer + editor (basic)
+* đ GPO Viewer
* đ§Ļ SOCKS support
# Installation
@@ -144,48 +145,52 @@ You can also change the address of your proxy using the `l` keybinding.
## Keybindings
-| Keybinding | Context | Action |
-| --------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------|
-| Ctrl + Enter (or Ctrl + J) | Global | Next panel |
-| f | Global | Toggle attribute formatting |
-| e | Global | Toggle emojis |
-| c | Global | Toggle colors |
-| a | Global | Toggle attribute expansion for multi-value attributes |
-| d | Global | Toggle "include deleted objects" flag |
-| l | Global | Change current server address & credentials |
-| Ctrl + r | Global | Reconnect to the server |
-| Ctrl + u | Global | Upgrade connection to use TLS (with StartTLS) |
-| Ctrl + f | Explorer & Search pages | Open the finder to search for cached objects & attributes with regex |
-| Right Arrow | Explorer panel | Expand the children of the selected object |
-| Left Arrow | Explorer panel | Collapse the children of the selected object |
-| r | Explorer panel | Reload the attributes and children of the selected object |
-| Ctrl + n | Explorer panel | Create a new object under the selected object |
-| Ctrl + s | Explorer panel | Export all loaded nodes in the selected subtree into a JSON file |
-| Ctrl + p | Explorer panel | Change the password of the selected user or computer account (requires TLS) |
-| Ctrl + a | Explorer panel | Update the userAccountControl of the object interactively |
-| Ctrl + l | Explorer panel | Move the selected object to another location |
-| Delete | Explorer panel | Delete the selected object |
-| r | Attributes panel | Reload the attributes for the selected object |
-| Ctrl + e | Attributes panel | Edit the selected attribute of the selected object |
-| Ctrl + n | Attributes panel | Create a new attribute in the selected object |
-| Delete | Attributes panel | Delete the selected attribute of the selected object |
-| Enter | Attributes panel (entries hidden) | Expand all hidden entries of an attribute |
-| Delete | Groups panels | Remove the selected member from the searched group or vice-versa |
-| Ctrl + s | Object groups panel | Export the current groups into a JSON file |
-| Ctrl + s | Group members panel | Export the current group members into a JSON file |
-| Ctrl + g | Groups panels / Explorer panel / Obj. Search panel | Add a member to the selected group / add the selected object into a group |
-| Ctrl + d | Groups panels / Explorer panel / Obj. Search panel | Inspect the DACL of the currently selected object |
-| Ctrl + o | DACL page | Change the owner of the current security descriptor |
-| Ctrl + k | DACL page | Change the control flags of the current security descriptor |
-| Ctrl + s | DACL page | Export the current security descriptor into a JSON file |
-| Ctrl + n | DACL entries panel | Create a new ACE in the current DACL |
-| Ctrl + e | DACL entries panel | Edit the selected ACE of the current DACL |
-| Delete | DACL entries panel | Deletes the selected ACE of the current DACL |
-| Ctrl + s | GPO page | Export the current GPOs and their links into a JSON file |
-| Ctrl + s | DNS zones panel | Export the selected zones and their child DNS nodes into a JSON file |
-| r | DNS zones panel | Reload the nodes of the selected zone / the records of the selected node |
-| h | Global | Show/hide headers |
-| q | Global | Exit the program |
+| Keybinding | Context | Action |
+| --------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------|
+| Ctrl + Enter (or Ctrl + J) | Global | Next panel |
+| f | Global | Toggle attribute formatting |
+| e | Global | Toggle emojis |
+| c | Global | Toggle colors |
+| a | Global | Toggle attribute expansion for multi-value attributes |
+| d | Global | Toggle "include deleted objects" flag |
+| l | Global | Change current server address & credentials |
+| Ctrl + r | Global | Reconnect to the server |
+| Ctrl + u | Global | Upgrade connection to use TLS (with StartTLS) |
+| Ctrl + f | Explorer & Search pages | Open the finder to search for cached objects & attributes with regex |
+| Right Arrow | Explorer panel | Expand the children of the selected object |
+| Left Arrow | Explorer panel | Collapse the children of the selected object |
+| r | Explorer panel | Reload the attributes and children of the selected object |
+| Ctrl + n | Explorer panel | Create a new object under the selected object |
+| Ctrl + s | Explorer panel | Export all loaded nodes in the selected subtree into a JSON file |
+| Ctrl + p | Explorer panel | Change the password of the selected user or computer account (requires TLS) |
+| Ctrl + a | Explorer panel | Update the userAccountControl of the object interactively |
+| Ctrl + l | Explorer panel | Move the selected object to another location |
+| Delete | Explorer panel | Delete the selected object |
+| r | Attributes panel | Reload the attributes for the selected object |
+| Ctrl + e | Attributes panel | Edit the selected attribute of the selected object |
+| Ctrl + n | Attributes panel | Create a new attribute in the selected object |
+| Delete | Attributes panel | Delete the selected attribute of the selected object |
+| Enter | Attributes panel (entries hidden) | Expand all hidden entries of an attribute |
+| Delete | Groups panels | Remove the selected member from the searched group or vice-versa |
+| Ctrl + s | Object groups panel | Export the current groups into a JSON file |
+| Ctrl + s | Group members panel | Export the current group members into a JSON file |
+| Ctrl + g | Groups panels / Explorer panel / Obj. Search panel | Add a member to the selected group / add the selected object into a group |
+| Ctrl + d | Groups panels / Explorer panel / Obj. Search panel | Inspect the DACL of the currently selected object |
+| Ctrl + o | DACL page | Change the owner of the current security descriptor |
+| Ctrl + k | DACL page | Change the control flags of the current security descriptor |
+| Ctrl + s | DACL page | Export the current security descriptor into a JSON file |
+| Ctrl + n | DACL entries panel | Create a new ACE in the current DACL |
+| Ctrl + e | DACL entries panel | Edit the selected ACE of the current DACL |
+| Delete | DACL entries panel | Deletes the selected ACE of the current DACL |
+| Ctrl + s | GPO page | Export the current GPOs and their links into a JSON file |
+| Ctrl + s | DNS zones panel | Export the selected zones and their child DNS nodes into a JSON file |
+| r | DNS zones panel | Reload the nodes of the selected zone / the records of the selected node |
+| Ctrl + n | DNS zones panel | Create a new node under the selected zone or a new zone if the root is selected |
+| Ctrl + e | DNS zones panel | Edit the records of the currently selected node |
+| Delete | DNS zones panel | Delete the selected DNS zone or DNS node |
+| Delete | Records Preview (in `ADIDNS Node Editor`) | Delete the selected record of the ADIDNS node |
+| h | Global | Show/hide headers |
+| q | Global | Exit the program |
## Tree Colors
diff --git a/TODO.md b/TODO.md
index 1b81f84..3f9fd4c 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,14 +1,16 @@
# TODO (priority)
-* Feature: Basic page for ADCS enumeration
* Feature: Search history for the current session
* Feature: Pivot to groups search
* Feature: Options to manipulate (edit/create/delete) gpLinks visually
-* Feature: Modify ADIDNS dnsZones and dnsNodes
* Fix: Warn user of wrong KRB5CCNAME formats
+* Feature: Basic page for ADCS enumeration
# TODO (later)
+* Wish: Remove dependency on personal fork of gokrb5 (may be doable with go-ldap's PR537)
+* Wish: Remove dependency on personal fork of go-ldap (may be doable with go-ldap's PR537)
+* Feature: Modify ADIDNS zone properties
* Feature: Improve object creation form (implement customizations)
* Feature: Custom themes
* Feature: Customizable keybindings
@@ -16,6 +18,4 @@
* Wish: Add tests for core functions to make sure everything is in order
* Wish: Mini tool to convert godap exports into bloodhound dumps
* Wish: Monitor object for real-time changes (DirSync/SyncRepl)
-* Wish: Some way to copy data from panels (not implemented in tview, only for the "textarea" primitive)
-* Wish: Remove dependency on personal fork of gokrb5
-* Wish: Remove dependency on personal fork of go-ldap
+* Wish: Some way to copy data from panels (not implemented in tview, only for the "textarea" primitive)
\ No newline at end of file
diff --git a/pkg/adidns/types.go b/pkg/adidns/types.go
index 678ebc9..939150b 100644
--- a/pkg/adidns/types.go
+++ b/pkg/adidns/types.go
@@ -8,6 +8,7 @@ import (
"io/ioutil"
"net"
"reflect"
+ "strconv"
"strings"
"time"
)
@@ -135,7 +136,7 @@ type DNSRecord struct {
Data []byte
}
-func MakeDNSRecord(rec FriendlyRecord, recType uint16, ttl uint32) DNSRecord {
+func MakeDNSRecord(rec RecordData, recType uint16, ttl uint32) DNSRecord {
serial := uint32(1)
msTime := GetCurrentMSTime()
data := rec.Encode()
@@ -400,6 +401,99 @@ func (d *DNSRecord) PrintType() string {
return recordType
}
+func (r *DNSRecord) GetRecordData() RecordData {
+ var parsedRecord RecordData
+
+ switch r.Type {
+ case 0x0000:
+ parsedRecord = new(ZERORecord)
+ case 0x0001:
+ parsedRecord = new(ARecord)
+ case 0x0002:
+ parsedRecord = new(NSRecord)
+ case 0x0003:
+ parsedRecord = new(MDRecord)
+ case 0x0004:
+ parsedRecord = new(MFRecord)
+ case 0x0005:
+ parsedRecord = new(CNAMERecord)
+ case 0x0006:
+ parsedRecord = new(SOARecord)
+ case 0x0007:
+ parsedRecord = new(MBRecord)
+ case 0x0008:
+ parsedRecord = new(MGRecord)
+ case 0x0009:
+ parsedRecord = new(MRRecord)
+ case 0x000A:
+ parsedRecord = new(NULLRecord)
+ case 0x000B:
+ parsedRecord = new(WKSRecord)
+ case 0x000C:
+ parsedRecord = new(PTRRecord)
+ case 0x000D:
+ parsedRecord = new(HINFORecord)
+ case 0x000E:
+ parsedRecord = new(MINFORecord)
+ case 0x000F:
+ parsedRecord = new(MXRecord)
+ case 0x0010:
+ parsedRecord = new(TXTRecord)
+ case 0x0011:
+ parsedRecord = new(RPRecord)
+ case 0x0012:
+ parsedRecord = new(AFSDBRecord)
+ case 0x0013:
+ parsedRecord = new(X25Record)
+ case 0x0014:
+ parsedRecord = new(ISDNRecord)
+ case 0x0015:
+ parsedRecord = new(RTRecord)
+ case 0x0018:
+ parsedRecord = new(SIGRecord)
+ case 0x0019:
+ parsedRecord = new(KEYRecord)
+ case 0x001C:
+ parsedRecord = new(AAAARecord)
+ case 0x001D:
+ parsedRecord = new(LOCRecord)
+ case 0x001E:
+ parsedRecord = new(NXTRecord)
+ case 0x0021:
+ parsedRecord = new(SRVRecord)
+ case 0x0022:
+ parsedRecord = new(ATMARecord)
+ case 0x0023:
+ parsedRecord = new(NAPTRRecord)
+ case 0x0027:
+ parsedRecord = new(DNAMERecord)
+ case 0x002B:
+ parsedRecord = new(DSRecord)
+ case 0x002E:
+ parsedRecord = new(RRSIGRecord)
+ case 0x002F:
+ parsedRecord = new(NSECRecord)
+ case 0x0030:
+ parsedRecord = new(DNSKEYRecord)
+ case 0x0031:
+ parsedRecord = new(DHCIDRecord)
+ case 0x0032:
+ parsedRecord = new(NSEC3Record)
+ case 0x0033:
+ parsedRecord = new(NSEC3PARAMRecord)
+ case 0x0034:
+ parsedRecord = new(TLSARecord)
+ case 0xFF01:
+ parsedRecord = new(WINSRecord)
+ case 0xFF02:
+ parsedRecord = new(WINSRRecord)
+ default:
+ parsedRecord = new(ZERORecord)
+ }
+ parsedRecord.Parse(r.Data)
+ return parsedRecord
+}
+
func (d *DNSRecord) UnixTimestamp() int64 {
msTime := uint64(d.Timestamp) * 3600
return MSTimeToUnixTimestamp(msTime)
@@ -563,7 +657,7 @@ func (p *DNSProperty) Decode(data []byte) error {
// IP addresses (v4 or v6) are stored using their string representations
// Interface to hold the parsed record fields
-type FriendlyRecord interface {
+type RecordData interface {
// Parses a record from its byte array in the Data field of the
// DNSRecord AD attribute
Parse([]byte)
@@ -581,7 +675,7 @@ type Field struct {
Value any
}
-func DumpRecordFields(fr FriendlyRecord) []Field {
+func DumpRecordFields(fr RecordData) []Field {
result := make([]Field, 0)
v := reflect.ValueOf(fr).Elem()
@@ -1350,3 +1444,101 @@ func (r *WINSRRecord) Encode() []byte {
return buf.Bytes()
}
+
+func parseUint(value string) uint64 {
+ parsed, _ := strconv.ParseUint(value, 10, 64)
+ return parsed
+}
+
+func RecordFromInput(recordType string, recordValue any) RecordData {
+ var fRecord RecordData
+
+ switch recordType {
+ case "ZERO":
+ fRecord = new(ZERORecord)
+ case "A":
+ fRecord = new(ARecord)
+ fRecord.(*ARecord).Address = recordValue.(string)
+ case "AAAA":
+ fRecord = new(AAAARecord)
+ fRecord.(*AAAARecord).Address = recordValue.(string)
+ case "CNAME":
+ fRecord = new(CNAMERecord)
+ fRecord.(*CNAMERecord).NameNode = recordValue.(string)
+ case "TXT":
+ fRecord = new(TXTRecord)
+ fRecord.(*TXTRecord).StrData = recordValue.([]string)
+ case "NS":
+ fRecord = new(NSRecord)
+ fRecord.(*NSRecord).NameNode = recordValue.(string)
+ case "PTR":
+ fRecord = new(PTRRecord)
+ fRecord.(*PTRRecord).NameNode = recordValue.(string)
+ case "MD":
+ fRecord = new(MDRecord)
+ fRecord.(*MDRecord).NameNode = recordValue.(string)
+ case "MF":
+ fRecord = new(MFRecord)
+ fRecord.(*MFRecord).NameNode = recordValue.(string)
+ case "MB":
+ fRecord = new(MBRecord)
+ fRecord.(*MBRecord).NameNode = recordValue.(string)
+ case "MG":
+ fRecord = new(MGRecord)
+ fRecord.(*MGRecord).NameNode = recordValue.(string)
+ case "MR":
+ fRecord = new(MRRecord)
+ fRecord.(*MRRecord).NameNode = recordValue.(string)
+ case "DNAME":
+ fRecord = new(DNAMERecord)
+ fRecord.(*DNAMERecord).NameNode = recordValue.(string)
+ case "HINFO":
+ fRecord = new(HINFORecord)
+ fRecord.(*HINFORecord).StrData = recordValue.([]string)
+ case "ISDN":
+ fRecord = new(ISDNRecord)
+ fRecord.(*ISDNRecord).StrData = recordValue.([]string)
+ case "X25":
+ fRecord = new(X25Record)
+ fRecord.(*X25Record).StrData = recordValue.([]string)
+ case "LOC":
+ fRecord = new(LOCRecord)
+ fRecord.(*LOCRecord).StrData = recordValue.([]string)
+ case "MX":
+ valMap := recordValue.(map[string]string)
+ fRecord = new(MXRecord)
+ fRecord.(*MXRecord).Preference = uint16(parseUint(valMap["Preference"]))
+ fRecord.(*MXRecord).Exchange = valMap["Exchange"]
+ case "AFSDB":
+ valMap := recordValue.(map[string]string)
+ fRecord = new(AFSDBRecord)
+ fRecord.(*AFSDBRecord).Preference = uint16(parseUint(valMap["Preference"]))
+ fRecord.(*AFSDBRecord).Exchange = valMap["Exchange"]
+ case "RT":
+ valMap := recordValue.(map[string]string)
+ fRecord = new(RTRecord)
+ fRecord.(*RTRecord).Preference = uint16(parseUint(valMap["Preference"]))
+ fRecord.(*RTRecord).Exchange = valMap["Exchange"]
+ case "SRV":
+ valMap := recordValue.(map[string]string)
+ fRecord = new(SRVRecord)
+ fRecord.(*SRVRecord).Priority = uint16(parseUint(valMap["Priority"]))
+ fRecord.(*SRVRecord).Weight = uint16(parseUint(valMap["Weight"]))
+ fRecord.(*SRVRecord).Port = uint16(parseUint(valMap["Port"]))
+ fRecord.(*SRVRecord).NameTarget = valMap["NameTarget"]
+ case "SOA":
+ valMap := recordValue.(map[string]string)
+ fRecord = new(SOARecord)
+ fRecord.(*SOARecord).Serial = uint32(parseUint(valMap["Serial"]))
+ fRecord.(*SOARecord).Refresh = uint32(parseUint(valMap["Refresh"]))
+ fRecord.(*SOARecord).Retry = uint32(parseUint(valMap["Retry"]))
+ fRecord.(*SOARecord).Expire = uint32(parseUint(valMap["Expire"]))
+ fRecord.(*SOARecord).MinimumTTL = uint32(parseUint(valMap["MinimumTTL"]))
+ fRecord.(*SOARecord).NamePrimaryServer = valMap["NamePrimaryServer"]
+ fRecord.(*SOARecord).ZoneAdminEmail = valMap["ZoneAdminEmail"]
+ default:
+ fRecord = new(ZERORecord)
+ }
+
+ return fRecord
+}
diff --git a/tui/dns.go b/tui/dns.go
index 565bb46..8264b56 100644
--- a/tui/dns.go
+++ b/tui/dns.go
@@ -43,11 +43,14 @@ var forestZones []adidns.DNSZone
var zoneCache = make(map[string]adidns.DNSZone, 0)
var nodeCache = make(map[string]adidns.DNSNode, 0)
-var recordCache = make(map[string][]adidns.FriendlyRecord, 0)
-
func getParentZone(objectDN string) (adidns.DNSZone, error) {
objectDNParts := strings.Split(objectDN, ",")
+ zone, zoneOk := zoneCache[objectDN]
+ if zoneOk {
+ return zone, nil
+ }
+
if len(objectDNParts) > 1 {
parentZoneDN := strings.Join(objectDNParts[1:], ",")
parentZone, zoneOk := zoneCache[parentZoneDN]
@@ -58,7 +61,7 @@ func getParentZone(objectDN string) (adidns.DNSZone, error) {
}
}
- return adidns.DNSZone{}, fmt.Errorf("Object DN too small to contain a parent zone")
+ return adidns.DNSZone{}, fmt.Errorf("Malformed object DN")
}
func exportADIDNSToFile(currentNode *tview.TreeNode, outputFilename string) {
@@ -89,11 +92,11 @@ func exportADIDNSToFile(currentNode *tview.TreeNode, outputFilename string) {
"Nodes": nodesMap,
}
} else if nodeOk {
- records, _ := recordCache[objectDN]
+ records := node.Records
recordsObj := make([]any, 0)
- for idx, rec := range records {
- recordType := node.Records[idx].PrintType()
+ for _, rec := range records {
+ recordType := rec.PrintType()
recordsObj = append(recordsObj, map[string]any{
"Type": recordType,
"Value": rec,
@@ -228,11 +231,10 @@ func reloadADIDNSNode(currentNode *tview.TreeNode) {
updateLog(fmt.Sprint(err), "red")
}
- storeNodeRecords(node)
showDetails(node.DN)
}
-func showNodeDetails(node *adidns.DNSNode, records []adidns.FriendlyRecord, targetTree *tview.TreeView) {
+func showDNSNodeDetails(node *adidns.DNSNode, targetTree *tview.TreeView) {
rootNode := tview.NewTreeNode(node.Name)
for idx, record := range node.Records {
@@ -275,7 +277,7 @@ func showNodeDetails(node *adidns.DNSNode, records []adidns.FriendlyRecord, targ
recordTreeNode := tview.NewTreeNode(recordName).
SetReference(recordRef{node.DN, idx})
- parsedRecord := records[idx]
+ parsedRecord := record.GetRecordData()
recordFields := adidns.DumpRecordFields(parsedRecord)
for idx, field := range recordFields {
fieldName := tview.Escape(fmt.Sprintf("%s=%v", field.Name, field.Value))
@@ -300,7 +302,6 @@ func showDetails(objectDN string) {
node, ok := nodeCache[objectDN]
if ok {
- parsedRecords, _ := recordCache[objectDN]
parentZone, err := getParentZone(objectDN)
if err == nil {
dnsSidePanel.SetTitle(fmt.Sprintf("Records (%s)", parentZone.Name))
@@ -309,108 +310,8 @@ func showDetails(objectDN string) {
}
dnsSidePanel.SwitchToPage("node-records")
- showNodeDetails(&node, parsedRecords, dnsNodeRecords)
- }
-}
-
-func storeNodeRecords(node adidns.DNSNode) {
- records := make([]adidns.FriendlyRecord, 0)
- var fRec adidns.FriendlyRecord
-
- for _, record := range node.Records {
- switch record.Type {
- case 0x0000:
- fRec = new(adidns.ZERORecord)
- case 0x0001:
- fRec = new(adidns.ARecord)
- case 0x0002:
- fRec = new(adidns.NSRecord)
- case 0x0003:
- fRec = new(adidns.MDRecord)
- case 0x0004:
- fRec = new(adidns.MFRecord)
- case 0x0005:
- fRec = new(adidns.CNAMERecord)
- case 0x0006:
- fRec = new(adidns.SOARecord)
- case 0x0007:
- fRec = new(adidns.MBRecord)
- case 0x0008:
- fRec = new(adidns.MGRecord)
- case 0x0009:
- fRec = new(adidns.MRRecord)
- case 0x000A:
- fRec = new(adidns.NULLRecord)
- case 0x000B:
- fRec = new(adidns.WKSRecord)
- case 0x000C:
- fRec = new(adidns.PTRRecord)
- case 0x000D:
- fRec = new(adidns.HINFORecord)
- case 0x000E:
- fRec = new(adidns.MINFORecord)
- case 0x000F:
- fRec = new(adidns.MXRecord)
- case 0x0010:
- fRec = new(adidns.TXTRecord)
- case 0x0011:
- fRec = new(adidns.RPRecord)
- case 0x0012:
- fRec = new(adidns.AFSDBRecord)
- case 0x0013:
- fRec = new(adidns.X25Record)
- case 0x0014:
- fRec = new(adidns.ISDNRecord)
- case 0x0015:
- fRec = new(adidns.RTRecord)
- case 0x0018:
- fRec = new(adidns.SIGRecord)
- case 0x0019:
- fRec = new(adidns.KEYRecord)
- case 0x001C:
- fRec = new(adidns.AAAARecord)
- case 0x001D:
- fRec = new(adidns.LOCRecord)
- case 0x001E:
- fRec = new(adidns.NXTRecord)
- case 0x0021:
- fRec = new(adidns.SRVRecord)
- case 0x0022:
- fRec = new(adidns.ATMARecord)
- case 0x0023:
- fRec = new(adidns.NAPTRRecord)
- case 0x0027:
- fRec = new(adidns.DNAMERecord)
- case 0x002B:
- fRec = new(adidns.DSRecord)
- case 0x002E:
- fRec = new(adidns.RRSIGRecord)
- case 0x002F:
- fRec = new(adidns.NSECRecord)
- case 0x0030:
- fRec = new(adidns.DNSKEYRecord)
- case 0x0031:
- fRec = new(adidns.DHCIDRecord)
- case 0x0032:
- fRec = new(adidns.NSEC3Record)
- case 0x0033:
- fRec = new(adidns.NSEC3PARAMRecord)
- case 0x0034:
- fRec = new(adidns.TLSARecord)
- case 0xFF01:
- fRec = new(adidns.WINSRecord)
- case 0xFF02:
- fRec = new(adidns.WINSRRecord)
- default:
- continue
- }
-
- fRec.Parse(record.Data)
-
- records = append(records, fRec)
+ showDNSNodeDetails(&node, dnsNodeRecords)
}
-
- recordCache[node.DN] = records
}
func loadZoneNodes(zoneNode *tview.TreeNode) int {
@@ -430,16 +331,16 @@ func loadZoneNodes(zoneNode *tview.TreeNode) int {
zoneNode.ClearChildren()
nodeFilter := dnsNodeFilter.GetText()
- nodeRegexp, err := regexp.Compile(nodeFilter)
+ nodeRegexp, _ := regexp.Compile(nodeFilter)
for _, node := range nodes {
+ nodeCache[node.DN] = node
+
nodeMatch := nodeRegexp.FindStringIndex(node.Name)
if nodeMatch == nil {
continue
}
- nodeCache[node.DN] = node
-
nodeName := node.Name
if Emojis {
nodeName = "đ" + nodeName
@@ -451,7 +352,6 @@ func loadZoneNodes(zoneNode *tview.TreeNode) int {
SetExpanded(false)
zoneNode.AddChild(treeNode)
- storeNodeRecords(node)
}
return len(nodes)
@@ -560,7 +460,6 @@ func initADIDNSPage() {
}
openDeleteObjectForm(currentNode, func() {
- level := currentNode.GetLevel()
if level == 1 {
go queryDnsZones(dnsQueryPanel.GetText())
} else if level == 2 {
@@ -578,13 +477,50 @@ func initADIDNSPage() {
outputFilename := fmt.Sprintf("%d_dns.json", unixTimestamp)
exportADIDNSToFile(currentNode, outputFilename)
case tcell.KeyCtrlN:
- /*
- TODO: Create zones or nodes
- */
+ if currentNode == dnsTreePanel.GetRoot() {
+ openCreateZoneForm()
+ } else {
+ if level == 1 {
+ openCreateNodeForm(currentNode)
+ } else if level == 2 {
+ parentZone := getParentNode(currentNode, dnsTreePanel)
+ openCreateNodeForm(parentZone)
+ }
+ }
case tcell.KeyCtrlE:
- /*
- TODO: Edit node records or zone properties
- */
+ if level == 1 {
+ // TODO: Edit zone properties
+ } else if level == 2 {
+ openUpdateNodeForm(currentNode)
+ }
+ }
+
+ return event
+ })
+
+ dnsNodeRecords.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ currentNode := dnsNodeRecords.GetCurrentNode()
+ if currentNode == nil || currentNode.GetReference() == nil {
+ return event
+ }
+
+ switch event.Key() {
+ case tcell.KeyCtrlE:
+ node := dnsTreePanel.GetCurrentNode()
+ openUpdateNodeForm(node)
+ return nil
+ case tcell.KeyDelete:
+ if currentNode.GetLevel() == 1 {
+ openDeleteRecordForm(currentNode)
+ } else if currentNode.GetLevel() == 2 {
+ pathToCurrent := dnsNodeRecords.GetPath(currentNode)
+ if len(pathToCurrent) > 1 {
+ parentNode := pathToCurrent[len(pathToCurrent)-2]
+ openDeleteRecordForm(parentNode)
+ }
+ }
+
+ return nil
}
return event
@@ -710,7 +646,6 @@ func queryDnsZones(targetZone string) {
clear(zoneCache)
clear(domainZones)
clear(forestZones)
- clear(recordCache)
app.QueueUpdateDraw(func() {
updateLog("Querying ADIDNS zones...", "yellow")
diff --git a/tui/dnsmodify.go b/tui/dnsmodify.go
new file mode 100644
index 0000000..bd3a9f9
--- /dev/null
+++ b/tui/dnsmodify.go
@@ -0,0 +1,535 @@
+package tui
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/Macmod/godap/v2/pkg/adidns"
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+)
+
+var supportedRecordTypes []string = []string{
+ "A", "AAAA", "CNAME", "TXT", "NS", "SOA", "SRV", "MX", "PTR",
+ "MD", "MF", "MB", "MG", "MR", "DNAME",
+ "HINFO", "ISDN", "X25", "LOC", "AFSDB", "RT",
+}
+
+func addZoneHandler(zoneForm *XForm, currentFocus tview.Primitive) func() {
+ return func() {
+ zoneName := zoneForm.GetFormItemByLabel("Zone Name").(*tview.InputField).GetText()
+ zoneAllowUpdate, _ := zoneForm.GetFormItemByLabel("Updates").(*tview.DropDown).GetCurrentOption()
+ zoneContainer, _ := zoneForm.GetFormItemByLabel("Container").(*tview.DropDown).GetCurrentOption()
+ zoneNS := zoneForm.GetFormItemByLabel("NameServer").(*tview.InputField).GetText()
+ zoneEmail := zoneForm.GetFormItemByLabel("AdminEmail").(*tview.InputField).GetText()
+
+ propType := adidns.MakeProp(0x1, []byte{1, 0, 0, 0})
+ propAllowUpdate := adidns.MakeProp(0x2, []byte{byte(zoneAllowUpdate)})
+ propNoRefresh := adidns.MakeProp(0x10, []byte{168})
+ propRefresh := adidns.MakeProp(0x20, []byte{168})
+ propAging := adidns.MakeProp(0x40, []byte{0})
+ propScavDa := adidns.MakeProp(0x90, []byte{})
+ propAutoNsDa := adidns.MakeProp(0x92, []byte{})
+
+ defaultProps := []adidns.DNSProperty{
+ propType,
+ propAllowUpdate,
+ propNoRefresh,
+ propRefresh,
+ propAging,
+ propScavDa,
+ propAutoNsDa,
+ }
+
+ isForest := zoneContainer == 1
+ zoneDN, err := lc.AddADIDNSZone(zoneName, defaultProps, isForest)
+
+ if err != nil {
+ updateLog(fmt.Sprint(err), "red")
+ app.SetRoot(appPanel, true).SetFocus(currentFocus)
+ return
+ }
+
+ // Basic records required so that
+ // the DNS will synchronize the zone from
+ // Active Directory
+ recSOA := adidns.MakeDNSRecord(
+ &adidns.SOARecord{1, 900, 600, 86400, 3600, zoneNS, zoneEmail},
+ 0x06,
+ 3600,
+ )
+
+ recNS := adidns.MakeDNSRecord(&adidns.NSRecord{zoneNS}, 0x02, 3600)
+
+ defaultRecords := []adidns.DNSRecord{
+ recSOA,
+ recNS,
+ }
+
+ _, err = lc.AddADIDNSNode("@", zoneDN, defaultRecords)
+ if err == nil {
+ updateLog(fmt.Sprintf("Zone '%s' created successfully!", zoneName), "green")
+ } else {
+ updateLog(fmt.Sprintf("Zone '%s' created without SOA & NS records - a problem might have occurred.", zoneName), "yellow")
+ }
+
+ go queryDnsZones(dnsQueryPanel.GetText())
+ app.SetRoot(appPanel, true).SetFocus(currentFocus)
+ }
+}
+
+func openCreateZoneForm() {
+ currentFocus := app.GetFocus()
+
+ zoneForm := NewXForm().
+ AddInputField("Zone Name", "", 0, nil, nil).
+ AddDropDown("Container", []string{"DomainDnsZones", "ForestDnsZones"}, 0, nil).
+ AddDropDown("Updates", []string{"None", "Nonsecure and secure", "Secure only"}, 0, nil).
+ AddInputField("NameServer", "", 0, nil, nil).
+ AddInputField("AdminEmail", "", 0, nil, nil)
+
+ zoneNameFormItem := zoneForm.GetFormItemByLabel("Zone Name").(*tview.InputField)
+ zoneNameFormItem.SetPlaceholder("example.com")
+ assignInputFieldTheme(zoneNameFormItem)
+
+ zoneForm.SetInputCapture(handleEscape(dnsTreePanel))
+
+ zoneForm.
+ AddButton("Go Back", func() {
+ app.SetRoot(appPanel, true).SetFocus(currentFocus)
+ }).
+ AddButton("Add", addZoneHandler(zoneForm, currentFocus))
+
+ zoneForm.SetTitle("Create ADIDNS Zone").SetBorder(true)
+ app.SetRoot(zoneForm, true).SetFocus(zoneForm)
+}
+
+func openActionNodeForm(target *tview.TreeNode, update bool) {
+ currentFocus := app.GetFocus()
+
+ targetDN := target.GetReference().(string)
+ targetDNParts := strings.Split(targetDN, ",")
+ firstDNComponents := strings.Split(targetDNParts[0], "=")
+ firstDNValue := firstDNComponents[1]
+
+ var (
+ title string
+ )
+
+ if update {
+ title = "Update"
+ } else {
+ title = "Create"
+ }
+
+ // Left panels
+ nodeInfoPanel := NewXForm()
+ nodeInfoPanel.SetTitle("Node")
+ nodeInfoPanel.SetBorder(true)
+
+ recordValuePages := tview.NewPages()
+
+ // Right panels
+ recordsPreview := tview.NewTreeView()
+ recordsPreview.
+ SetRoot(tview.NewTreeNode("")).
+ SetTitle("Records Preview").
+ SetBorder(true)
+
+ nodeNameInput := tview.NewInputField().
+ SetLabel("Node Name").
+ SetChangedFunc(func(text string) {
+ root := recordsPreview.GetRoot()
+ if root != nil {
+ root.SetText(text)
+ }
+ })
+ nodeNameInput.SetPlaceholder("The node name is usually the subdomain you want to create")
+ assignInputFieldTheme(nodeNameInput)
+
+ // Preview area internal structure
+ var stagedParsedRecords []adidns.RecordData
+ var stagedRecords []adidns.DNSRecord
+
+ if update {
+ // Prefill the existing records
+ // of the node into the staging area
+ node, ok := nodeCache[targetDN]
+ if !ok {
+ return
+ }
+ existingRecords := node.Records
+
+ stagedParsedRecords = make([]adidns.RecordData, len(existingRecords))
+ stagedRecords = make([]adidns.DNSRecord, len(existingRecords))
+
+ copy(stagedRecords, existingRecords)
+
+ for idx, record := range existingRecords {
+ parsedRecord := record.GetRecordData()
+ stagedParsedRecords[idx] = parsedRecord
+ }
+
+ // Show the existing records in the preview
+ showDNSNodeDetails(&node, recordsPreview)
+ } else {
+ // Set up an empty staging area
+ stagedParsedRecords = make([]adidns.RecordData, 0)
+ stagedRecords = make([]adidns.DNSRecord, 0)
+ }
+
+ recordsPreview.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ switch event.Key() {
+ case tcell.KeyDelete:
+ currentNode := recordsPreview.GetCurrentNode()
+ if currentNode == nil {
+ return nil
+ }
+
+ level := currentNode.GetLevel()
+
+ var nodeToDelete *tview.TreeNode
+
+ if level == 1 {
+ nodeToDelete = currentNode
+ } else if level == 2 {
+ pathToCurrent := recordsPreview.GetPath(currentNode)
+ if len(pathToCurrent) > 1 {
+ nodeToDelete = pathToCurrent[len(pathToCurrent)-2]
+ }
+ }
+
+ if nodeToDelete != nil {
+ recIdx := -1
+ siblings := recordsPreview.GetRoot().GetChildren()
+ for idx, node := range siblings {
+ if node == nodeToDelete {
+ recIdx = idx
+ }
+ }
+
+ if recIdx != -1 {
+ stagedParsedRecords = append(stagedParsedRecords[:recIdx], stagedParsedRecords[recIdx+1:]...)
+ stagedRecords = append(stagedRecords[:recIdx], stagedRecords[recIdx+1:]...)
+
+ recordsPreview.GetRoot().RemoveChild(nodeToDelete)
+ }
+
+ go func() {
+ app.Draw()
+ }()
+ }
+
+ return nil
+ }
+
+ return event
+ })
+
+ // nodeInfoPanel setup
+ parentZone, err := getParentZone(targetDN)
+ if err == nil {
+ nodeInfoPanel.AddTextView("Zone DN", parentZone.DN, 0, 1, false, true)
+ }
+ if update {
+ nodeInfoPanel.AddTextView("Node Name", firstDNValue, 0, 1, false, true)
+ }
+
+ // recordContent setup
+ recordTypeInput := tview.NewDropDown().
+ SetLabel("Record Type").
+ SetOptions(supportedRecordTypes, func(text string, index int) {
+ switch text {
+ case "HINFO", "ISDN", "TXT", "X25", "LOC":
+ recordValuePages.SwitchToPage("multiple")
+ case "MX", "AFSDB", "RT":
+ recordValuePages.SwitchToPage("namepref")
+ case "SRV":
+ recordValuePages.SwitchToPage("srv")
+ case "SOA":
+ recordValuePages.SwitchToPage("soa")
+ default:
+ recordValuePages.SwitchToPage("default")
+ }
+ })
+ assignDropDownTheme(recordTypeInput)
+
+ recordTypeInput.
+ SetCurrentOption(0).
+ SetLabelWidth(12).
+ SetBorderPadding(1, 0, 1, 1)
+
+ recordTTLInput := tview.NewInputField().SetText("3600")
+ recordTTLInput.
+ SetLabel("Record TTL").
+ SetLabelWidth(12).
+ SetBorderPadding(1, 0, 1, 1)
+ assignInputFieldTheme(recordTTLInput)
+
+ nameprefRecordValueInput := NewXForm().
+ AddInputField("Preference", "", 0, numericAcceptanceFunc, nil).
+ AddInputField("Exchange", "", 0, nil, nil)
+
+ soaRecordValueInput := NewXForm().
+ AddInputField("Serial", "", 0, numericAcceptanceFunc, nil).
+ AddInputField("Refresh", "", 0, numericAcceptanceFunc, nil).
+ AddInputField("Retry", "", 0, numericAcceptanceFunc, nil).
+ AddInputField("Expire", "", 0, numericAcceptanceFunc, nil).
+ AddInputField("MinimumTTL", "", 0, numericAcceptanceFunc, nil).
+ AddInputField("NamePrimaryServer", "", 0, nil, nil).
+ AddInputField("ZoneAdminEmail", "", 0, nil, nil)
+
+ srvRecordValueInput := NewXForm().
+ AddInputField("Priority", "", 0, numericAcceptanceFunc, nil).
+ AddInputField("Weight", "", 0, numericAcceptanceFunc, nil).
+ AddInputField("Port", "", 0, numericAcceptanceFunc, nil).
+ AddInputField("NameTarget", "", 0, nil, nil)
+
+ defaultRecordValueInput := NewXForm().
+ AddInputField("Record Value", "", 0, nil, nil)
+ defaultRecordValueInput.GetFormItem(0).(*tview.InputField).SetPlaceholder("Type the record value and add it to the preview")
+
+ multipleRecordValueInput := NewXForm().
+ AddTextArea("Record Values", "", 0, 0, 0, nil)
+ multipleRecordValueInput.GetFormItem(0).(*tview.TextArea).SetPlaceholder("Type in the values for the record line-by-line\nand add it to the preview")
+
+ cancelBtn := tview.NewButton("Go Back").SetSelectedFunc(func() {
+ app.SetRoot(appPanel, true).SetFocus(currentFocus)
+ })
+ assignButtonTheme(cancelBtn)
+
+ updateBtn := tview.NewButton(title).SetSelectedFunc(func() {
+ var nodeDN string
+ var action string
+ var err error
+ if !update {
+ nodeName := nodeNameInput.GetText()
+ nodeDN, err = lc.AddADIDNSNode(
+ nodeName,
+ targetDN,
+ stagedRecords,
+ )
+ action = "created"
+ } else {
+ nodeDN = targetDN
+ err = lc.ReplaceADIDNSRecords(
+ nodeDN,
+ stagedRecords,
+ )
+ action = "updated"
+ }
+
+ if err != nil {
+ updateLog(fmt.Sprint(err), "red")
+ app.SetRoot(appPanel, true).SetFocus(currentFocus)
+ return
+ }
+
+ go app.QueueUpdateDraw(func() {
+ if !update {
+ reloadADIDNSZone(target)
+ } else {
+ reloadADIDNSNode(target)
+ }
+ })
+
+ updateLog(fmt.Sprintf("Node '%s' %s successfully", nodeDN, action), "green")
+ app.SetRoot(appPanel, true).SetFocus(currentFocus)
+ })
+ assignButtonTheme(updateBtn)
+
+ addToPreview := tview.NewButton("Add To Preview").SetSelectedFunc(func() {
+ _, recordTypeVal := recordTypeInput.GetCurrentOption()
+ recordTTLVal := recordTTLInput.GetText()
+ recordTTLInt, err := strconv.Atoi(recordTTLVal)
+
+ if err != nil {
+ return
+ }
+
+ // Append the new record to the preview area
+ var recordValue any
+
+ switch recordTypeVal {
+ case "HINFO", "ISDN", "TXT", "X25", "LOC":
+ recordValue = strings.Split(multipleRecordValueInput.GetFormItem(0).(*tview.TextArea).GetText(), "\n")
+ case "MX", "AFSDB", "RT":
+ recordValue = map[string]string{
+ "Preference": nameprefRecordValueInput.GetFormItemByLabel("Preference").(*tview.InputField).GetText(),
+ "Exchange": nameprefRecordValueInput.GetFormItemByLabel("Exchange").(*tview.InputField).GetText(),
+ }
+ case "SOA":
+ recordValue = map[string]string{
+ "Serial": soaRecordValueInput.GetFormItemByLabel("Serial").(*tview.InputField).GetText(),
+ "Refresh": soaRecordValueInput.GetFormItemByLabel("Refresh").(*tview.InputField).GetText(),
+ "Retry": soaRecordValueInput.GetFormItemByLabel("Retry").(*tview.InputField).GetText(),
+ "Expire": soaRecordValueInput.GetFormItemByLabel("Expire").(*tview.InputField).GetText(),
+ "MinimumTTL": soaRecordValueInput.GetFormItemByLabel("MinimumTTL").(*tview.InputField).GetText(),
+ "NamePrimaryServer": soaRecordValueInput.GetFormItemByLabel("NamePrimaryServer").(*tview.InputField).GetText(),
+ "ZoneAdminEmail": soaRecordValueInput.GetFormItemByLabel("ZoneAdminEmail").(*tview.InputField).GetText(),
+ }
+ case "SRV":
+ recordValue = map[string]string{
+ "Priority": srvRecordValueInput.GetFormItemByLabel("Priority").(*tview.InputField).GetText(),
+ "Weight": srvRecordValueInput.GetFormItemByLabel("Weight").(*tview.InputField).GetText(),
+ "Port": srvRecordValueInput.GetFormItemByLabel("Port").(*tview.InputField).GetText(),
+ "NameTarget": srvRecordValueInput.GetFormItemByLabel("NameTarget").(*tview.InputField).GetText(),
+ }
+ default:
+ recordValue = defaultRecordValueInput.GetFormItem(0).(*tview.InputField).GetText()
+ }
+
+ record := adidns.RecordFromInput(recordTypeVal, recordValue)
+ stagedParsedRecords = append(stagedParsedRecords, record)
+
+ recordTypeInt := adidns.FindRecordType(recordTypeVal)
+ recordToStore := adidns.MakeDNSRecord(record, recordTypeInt, uint32(recordTTLInt))
+ stagedRecords = append(stagedRecords, recordToStore)
+
+ // Make a new node to add to the preview
+ var newNode adidns.DNSNode
+ if update {
+ node, ok := nodeCache[targetDN]
+ if !ok {
+ return
+ }
+ newNode = adidns.DNSNode{targetDN, node.Name, stagedRecords}
+ } else {
+ nodeName := nodeNameInput.GetText()
+ newNode = adidns.DNSNode{"", nodeName, stagedRecords}
+ }
+
+ // Show preview
+ showDNSNodeDetails(&newNode, recordsPreview)
+ })
+ assignButtonTheme(addToPreview)
+
+ // Page setup
+ actionNodePanel := tview.NewFlex()
+ actionNodePanel.
+ SetInputCapture(handleEscape(dnsTreePanel)).
+ SetTitle(fmt.Sprintf("%s ADIDNS Node", title)).
+ SetBorder(true)
+
+ actionNodePanel.SetDirection(tview.FlexRow)
+
+ recordValuePages.AddPage("default", defaultRecordValueInput, true, true)
+ recordValuePages.AddPage("multiple", multipleRecordValueInput, true, false)
+ recordValuePages.AddPage("namepref", nameprefRecordValueInput, true, false)
+ recordValuePages.AddPage("soa", soaRecordValueInput, true, false)
+ recordValuePages.AddPage("srv", srvRecordValueInput, true, false)
+
+ recordContentPanel := tview.NewFlex().SetDirection(tview.FlexRow).
+ AddItem(recordTypeInput, 2, 0, false).
+ AddItem(recordTTLInput, 2, 0, false).
+ AddItem(recordValuePages, 0, 1, false).
+ AddItem(addToPreview, 1, 0, false)
+
+ recordContentPanel.
+ SetTitle("Record Contents").
+ SetBorder(true)
+
+ leftPanel := tview.NewFlex().SetDirection(tview.FlexRow)
+
+ // If it's node creation,
+ // show an input to specify the node name.
+ // Otherwise just keep it hidden.
+ if !update {
+ nodeInfoPanel.AddFormItem(nodeNameInput)
+ }
+
+ actionNodePanel.AddItem(
+ tview.NewFlex().
+ AddItem(
+ leftPanel.
+ AddItem(nodeInfoPanel, 7, 0, false).
+ AddItem(recordContentPanel, 0, 1, false),
+ 0, 1, false).
+ AddItem(recordsPreview, 0, 1, false),
+ 0, 1, false).
+ AddItem(
+ tview.NewFlex().
+ AddItem(tview.NewBox(), 1, 0, false). // Spacing
+ AddItem(cancelBtn, 10, 0, false).
+ AddItem(tview.NewBox(), 0, 1, false). // Spacing
+ AddItem(updateBtn, 10, 0, false).
+ AddItem(tview.NewBox(), 1, 0, false), // Spacing
+ 1, 0, false)
+
+ app.SetRoot(actionNodePanel, true).SetFocus(actionNodePanel)
+}
+
+func openUpdateNodeForm(node *tview.TreeNode) {
+ openActionNodeForm(node, true)
+}
+
+func openCreateNodeForm(zone *tview.TreeNode) {
+ openActionNodeForm(zone, false)
+}
+
+func openDeleteRecordForm(record *tview.TreeNode) {
+ currentFocus := app.GetFocus()
+ recRef := record.GetReference().(recordRef)
+
+ nodeDN := recRef.nodeDN
+ recIdx := recRef.idx
+
+ node, ok := nodeCache[nodeDN]
+ if !ok {
+ return
+ }
+ records := node.Records
+
+ confirmText := fmt.Sprintf("Do you really want to delete this record?\nRecordIdx: %d\nNode: %s", recIdx, nodeDN)
+ promptModal := tview.NewModal().
+ SetText(confirmText).
+ AddButtons([]string{"No", "Yes"}).
+ SetDoneFunc(func(buttonIndex int, buttonLabel string) {
+ if buttonLabel == "Yes" {
+ // TODO: Add safety check for changes outside Godap
+ updateRecords := append(records[:recIdx], records[recIdx+1:]...)
+
+ err = lc.ReplaceADIDNSRecords(nodeDN, updateRecords)
+ if err != nil {
+ updateLog(fmt.Sprint(err), "red")
+ app.SetRoot(appPanel, true).SetFocus(currentFocus)
+ return
+ }
+
+ node := dnsTreePanel.GetCurrentNode()
+ reloadADIDNSNode(node)
+
+ updateLog("Record deleted successfully", "green")
+ app.SetRoot(appPanel, true).SetFocus(currentFocus)
+ } else {
+ app.SetRoot(appPanel, true).SetFocus(currentFocus)
+ }
+ })
+
+ app.SetRoot(promptModal, true).SetFocus(promptModal)
+}
+
+/*
+Records can also be added instead of replaced with:
+
+```
+ rec := recordFromInput(recordType, recordValue)
+
+ recordsToAdd := []adidns.DNSRecord{
+ adidns.MakeDNSRecord(
+ rec,
+ adidns.FindRecordType(recordType),
+ uint32(recordTTLInt)),
+ }
+
+ err = lc.AddADIDNSRecords(nodeDN, recordsToAdd)
+ if err != nil {
+ updateLog(fmt.Sprint(err), "red")
+ app.SetRoot(appPanel, true).SetFocus(currentFocus)
+ return
+ }
+
+ reloadADIDNSNode(node)
+````
+*/
diff --git a/tui/explorer.go b/tui/explorer.go
index 5811207..ef210e1 100644
--- a/tui/explorer.go
+++ b/tui/explorer.go
@@ -133,7 +133,7 @@ func collapseTreeNode(node *tview.TreeNode) {
}
func reloadParentNode(node *tview.TreeNode) *tview.TreeNode {
- parent := getParentNode(node)
+ parent := getParentNode(node, treePanel)
if parent != nil {
unloadChildren(parent)
@@ -151,28 +151,6 @@ func reloadExplorerAttrsPanel(node *tview.TreeNode, useCache bool) {
reloadAttributesPanel(node, explorerAttrsPanel, useCache, &explorerCache)
}
-func getParentNode(node *tview.TreeNode) *tview.TreeNode {
- pathToCurrent := treePanel.GetPath(node)
-
- if len(pathToCurrent) > 1 {
- return pathToCurrent[len(pathToCurrent)-2]
- }
-
- return nil
-}
-
-func findEntryInChildren(dn string, parent *tview.TreeNode) int {
- siblings := parent.GetChildren()
-
- for idx, loopNode := range siblings {
- if loopNode.GetReference().(string) == dn {
- return idx
- }
- }
-
- return -1
-}
-
func exportCacheToFile(currentNode *tview.TreeNode, cache *EntryCache, outputFilename string) {
exportMap := make(map[string]*ldap.Entry)
currentNode.Walk(func(node, parent *tview.TreeNode) bool {
@@ -430,7 +408,7 @@ func treePanelKeyHandler(event *tcell.EventKey) *tcell.EventKey {
return event
}
- parentNode := getParentNode(currentNode)
+ parentNode := getParentNode(currentNode, treePanel)
baseDN := currentNode.GetReference().(string)
switch event.Rune() {
diff --git a/tui/help.go b/tui/help.go
index 7f3bcc0..7ed6217 100644
--- a/tui/help.go
+++ b/tui/help.go
@@ -60,6 +60,10 @@ func initHelpPage() {
{"Ctrl + s", "GPO page", "Export the current GPOs and their links into a JSON file"},
{"Ctrl + s", "DNS zones panel", "Export the selected zones and their child DNS nodes into a JSON file"},
{"r", "DNS zones panel", "Reload the nodes of the selected zone / the records of the selected node"},
+ {"Ctrl + n", "DNS zones panel", "Create a new node under the selected zone or a new zone if the root is selected"},
+ {"Ctrl + e", "DNS zones panel", "Edit the records of the currently selected node"},
+ {"Delete", "DNS zones panel", "Delete the selected DNS zone or DNS node"},
+ {"Delete", "Records Preview (in ADIDNS Node Editor)", "Delete the selected record of the ADIDNS node"},
{"h", "Global", "Show/hide headers"},
{"q", "Global", "Exit the program"},
}
diff --git a/tui/interface.go b/tui/interface.go
index 9f7722e..4060d39 100644
--- a/tui/interface.go
+++ b/tui/interface.go
@@ -1,6 +1,8 @@
package tui
import (
+ "strconv"
+
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
@@ -14,3 +16,30 @@ func handleEscape(returnFocus tview.Primitive) func(*tcell.EventKey) *tcell.Even
return event
}
}
+
+func getParentNode(node *tview.TreeNode, tree *tview.TreeView) *tview.TreeNode {
+ pathToCurrent := tree.GetPath(node)
+
+ if len(pathToCurrent) > 1 {
+ return pathToCurrent[len(pathToCurrent)-2]
+ }
+
+ return nil
+}
+
+func findEntryInChildren(dn string, parent *tview.TreeNode) int {
+ siblings := parent.GetChildren()
+
+ for idx, loopNode := range siblings {
+ if loopNode.GetReference().(string) == dn {
+ return idx
+ }
+ }
+
+ return -1
+}
+
+func numericAcceptanceFunc(textToCheck string, lastChar rune) bool {
+ _, err := strconv.Atoi(textToCheck)
+ return err == nil
+}
diff --git a/tui/main.go b/tui/main.go
index 2f0fa35..cde5c88 100644
--- a/tui/main.go
+++ b/tui/main.go
@@ -18,7 +18,7 @@ import (
"software.sslmate.com/src/go-pkcs12"
)
-var GodapVer = "Godap v2.9.0"
+var GodapVer = "Godap v2.10.0"
var (
LdapServer string
diff --git a/tui/theme.go b/tui/theme.go
index 825ad6f..5567561 100644
--- a/tui/theme.go
+++ b/tui/theme.go
@@ -30,6 +30,7 @@ type GodapTheme struct {
DisabledNodeColor tcell.Color
}
+// Theme definitions - controls the colors of all Godap pages
var baseTheme tview.Theme = tview.Theme{
PrimitiveBackgroundColor: tcell.ColorBlack,
ContrastBackgroundColor: tcell.ColorBlue,
@@ -50,7 +51,7 @@ var DefaultTheme = GodapTheme{
// Input fields for main pages
FieldBackgroundColor: tcell.ColorBlack,
- PlaceholderStyle: tcell.Style{}.Foreground(tcell.ColorDefault).Background(tcell.ColorBlack),
+ PlaceholderStyle: tcell.Style{}.Foreground(tcell.ColorGray).Background(tcell.ColorBlack),
PlaceholderTextColor: tcell.ColorGray,
// Form buttons
@@ -79,6 +80,10 @@ func assignButtonTheme(btn *tview.Button) {
SetActivatedStyle(DefaultTheme.FormButtonActivatedStyle)
}
+func assignDropDownTheme(dropdown *tview.DropDown) {
+ dropdown.SetFieldBackgroundColor(DefaultTheme.FieldBackgroundColor)
+}
+
func assignFormTheme(form *tview.Form) {
form.
SetButtonBackgroundColor(DefaultTheme.FormButtonBackgroundColor).
@@ -121,6 +126,8 @@ func (f *XForm) AddInputField(label, value string, fieldWidth int, accept func(t
inputField := tview.NewInputField()
f.AddFormItem(inputField.
SetFieldStyle(tcell.StyleDefault.Background(tcell.ColorWhite).Foreground(tcell.ColorBlack)).
+ SetPlaceholderStyle(DefaultTheme.PlaceholderStyle).
+ SetPlaceholderTextColor(DefaultTheme.PlaceholderTextColor).
SetLabel(label).
SetText(value).
SetFieldWidth(fieldWidth).
@@ -130,6 +137,33 @@ func (f *XForm) AddInputField(label, value string, fieldWidth int, accept func(t
return f
}
+func (f *XForm) AddTextArea(label string, text string, fieldWidth, fieldHeight int, maxLength int, changed func(text string)) *XForm {
+ if fieldHeight == 0 {
+ fieldHeight = tview.DefaultFormFieldHeight
+ }
+
+ textArea := tview.NewTextArea()
+ textArea.
+ SetLabel(label).
+ SetSize(fieldHeight, fieldWidth).
+ SetMaxLength(maxLength).
+ SetPlaceholderStyle(DefaultTheme.PlaceholderStyle)
+
+ if text != "" {
+ textArea.SetText(text, true)
+ }
+
+ if changed != nil {
+ textArea.SetChangedFunc(func() {
+ changed(textArea.GetText())
+ })
+ }
+
+ f.AddFormItem(textArea)
+
+ return f
+}
+
func (f *XForm) AddPasswordField(label, value string, fieldWidth int, mask rune, changed func(text string)) *XForm {
if mask == 0 {
mask = '*'