github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/activedir/domains.go (about) 1 package activedir 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "os" 8 "strings" 9 "time" 10 11 "github.com/TomOnTime/utfutil" 12 "github.com/miekg/dns/dnsutil" 13 14 "github.com/StackExchange/dnscontrol/models" 15 "github.com/StackExchange/dnscontrol/providers/diff" 16 ) 17 18 const zoneDumpFilenamePrefix = "adzonedump" 19 20 type RecordConfigJson struct { 21 Name string `json:"hostname"` 22 Type string `json:"recordtype"` 23 Data string `json:"recorddata"` 24 TTL uint32 `json:"timetolive"` 25 } 26 27 func (c *adProvider) GetNameservers(string) ([]*models.Nameserver, error) { 28 //TODO: If using AD for publicly hosted zones, probably pull these from config. 29 return nil, nil 30 } 31 32 // GetDomainCorrections gets existing records, diffs them against existing, and returns corrections. 33 func (c *adProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 34 35 dc.Filter(func(r *models.RecordConfig) bool { 36 if r.Type != "A" && r.Type != "CNAME" { 37 log.Printf("WARNING: Active Directory only manages A and CNAME records. Won't consider %s %s", r.Type, r.NameFQDN) 38 return false 39 } 40 return true 41 }) 42 43 // Read foundRecords: 44 foundRecords, err := c.getExistingRecords(dc.Name) 45 if err != nil { 46 return nil, fmt.Errorf("c.getExistingRecords(%v) failed: %v", dc.Name, err) 47 } 48 49 differ := diff.New(dc) 50 _, creates, dels, modifications := differ.IncrementalDiff(foundRecords) 51 // NOTE(tlim): This provider does not delete records. If 52 // you need to delete a record, either delete it manually 53 // or see providers/activedir/doc.md for implementation tips. 54 55 // Generate changes. 56 corrections := []*models.Correction{} 57 for _, del := range dels { 58 corrections = append(corrections, c.deleteRec(dc.Name, del.Existing)) 59 } 60 for _, cre := range creates { 61 corrections = append(corrections, c.createRec(dc.Name, cre.Desired)...) 62 } 63 for _, m := range modifications { 64 corrections = append(corrections, c.modifyRec(dc.Name, m)) 65 } 66 return corrections, nil 67 68 } 69 70 // zoneDumpFilename returns the filename to use to write or read 71 // an activedirectory zone dump for a particular domain. 72 func zoneDumpFilename(domainname string) string { 73 return zoneDumpFilenamePrefix + "." + domainname + ".json" 74 } 75 76 // readZoneDump reads a pre-existing zone dump from adzonedump.*.json. 77 func (c *adProvider) readZoneDump(domainname string) ([]byte, error) { 78 // File not found is considered an error. 79 dat, err := utfutil.ReadFile(zoneDumpFilename(domainname), utfutil.WINDOWS) 80 if err != nil { 81 fmt.Println("Powershell to generate zone dump:") 82 fmt.Println(c.generatePowerShellZoneDump(domainname)) 83 } 84 return dat, err 85 } 86 87 // powerShellLogCommand logs to flagPsLog that a PowerShell command is going to be run. 88 func (c *adProvider) logCommand(command string) error { 89 return c.logHelper(fmt.Sprintf("# %s\r\n%s\r\n", time.Now().UTC(), strings.TrimSpace(command))) 90 } 91 92 // powerShellLogOutput logs to flagPsLog that a PowerShell command is going to be run. 93 func (c *adProvider) logOutput(s string) error { 94 return c.logHelper(fmt.Sprintf("OUTPUT: START\r\n%s\r\nOUTPUT: END\r\n", s)) 95 } 96 97 // powerShellLogErr logs that a PowerShell command had an error. 98 func (c *adProvider) logErr(e error) error { 99 err := c.logHelper(fmt.Sprintf("ERROR: %v\r\r", e)) //Log error to powershell.log 100 if err != nil { 101 return err //Bubble up error created in logHelper 102 } 103 return e //Bubble up original error 104 } 105 106 func (c *adProvider) logHelper(s string) error { 107 logfile, err := os.OpenFile(c.psLog, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0660) 108 if err != nil { 109 return fmt.Errorf("error: Can not create/append to %#v: %v", c.psLog, err) 110 } 111 _, err = fmt.Fprintln(logfile, s) 112 if err != nil { 113 return fmt.Errorf("Append to %#v failed: %v", c.psLog, err) 114 } 115 if logfile.Close() != nil { 116 return fmt.Errorf("Closing %#v failed: %v", c.psLog, err) 117 } 118 return nil 119 } 120 121 // powerShellRecord records that a PowerShell command should be executed later. 122 func (c *adProvider) powerShellRecord(command string) error { 123 recordfile, err := os.OpenFile(c.psOut, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0660) 124 if err != nil { 125 return fmt.Errorf("Can not create/append to %#v: %v\n", c.psOut, err) 126 } 127 _, err = recordfile.WriteString(command) 128 if err != nil { 129 return fmt.Errorf("Append to %#v failed: %v\n", c.psOut, err) 130 } 131 return recordfile.Close() 132 } 133 134 func (c *adProvider) getExistingRecords(domainname string) ([]*models.RecordConfig, error) { 135 //log.Printf("getExistingRecords(%s)\n", domainname) 136 137 // Get the JSON either from adzonedump or by running a PowerShell script. 138 data, err := c.getRecords(domainname) 139 if err != nil { 140 return nil, fmt.Errorf("getRecords failed on %#v: %v", domainname, err) 141 } 142 143 var recs []*RecordConfigJson 144 err = json.Unmarshal(data, &recs) 145 if err != nil { 146 return nil, fmt.Errorf("json.Unmarshal failed on %#v: %v", domainname, err) 147 } 148 149 result := make([]*models.RecordConfig, 0, len(recs)) 150 for i := range recs { 151 t := recs[i].unpackRecord(domainname) 152 if t != nil { 153 result = append(result, t) 154 } 155 } 156 157 return result, nil 158 } 159 160 func (r *RecordConfigJson) unpackRecord(origin string) *models.RecordConfig { 161 rc := models.RecordConfig{} 162 163 rc.Name = strings.ToLower(r.Name) 164 rc.NameFQDN = dnsutil.AddOrigin(rc.Name, origin) 165 rc.Type = r.Type 166 rc.TTL = r.TTL 167 168 switch rc.Type { // #rtype_variations 169 case "A": 170 rc.Target = r.Data 171 case "CNAME": 172 rc.Target = strings.ToLower(r.Data) 173 case "NS", "SOA": 174 return nil 175 default: 176 log.Printf("Warning: Record of type %s found in AD zone. Will be ignored.", rc.Type) 177 return nil 178 } 179 return &rc 180 } 181 182 // powerShellDump runs a PowerShell command to get a dump of all records in a DNS zone. 183 func (c *adProvider) generatePowerShellZoneDump(domainname string) string { 184 cmdTxt := `@("REPLACE_WITH_ZONE") | %{ 185 Get-DnsServerResourceRecord -ComputerName REPLACE_WITH_COMPUTER_NAME -ZoneName $_ | select hostname,recordtype,@{n="timestamp";e={$_.timestamp.tostring()}},@{n="timetolive";e={$_.timetolive.totalseconds}},@{n="recorddata";e={($_.recorddata.ipv4address,$_.recorddata.ipv6address,$_.recorddata.HostNameAlias,"other_record" -ne $null)[0]-as [string]}} | ConvertTo-Json > REPLACE_WITH_FILENAMEPREFIX.REPLACE_WITH_ZONE.json 186 }` 187 cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_ZONE", domainname, -1) 188 cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_COMPUTER_NAME", c.adServer, -1) 189 cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_FILENAMEPREFIX", zoneDumpFilenamePrefix, -1) 190 191 return cmdTxt 192 } 193 194 // generatePowerShellCreate generates PowerShell commands to ADD a record. 195 func (c *adProvider) generatePowerShellCreate(domainname string, rec *models.RecordConfig) string { 196 content := rec.Target 197 text := "\r\n" // Skip a line. 198 text += fmt.Sprintf("Add-DnsServerResourceRecord%s", rec.Type) 199 text += fmt.Sprintf(` -ComputerName "%s"`, c.adServer) 200 text += fmt.Sprintf(` -ZoneName "%s"`, domainname) 201 text += fmt.Sprintf(` -Name "%s"`, rec.Name) 202 text += fmt.Sprintf(` -TimeToLive $(New-TimeSpan -Seconds %d)`, rec.TTL) 203 switch rec.Type { // #rtype_variations 204 case "CNAME": 205 text += fmt.Sprintf(` -HostNameAlias "%s"`, content) 206 case "A": 207 text += fmt.Sprintf(` -IPv4Address "%s"`, content) 208 case "NS": 209 text = fmt.Sprintf("\r\n"+`echo "Skipping NS update (%v %v)"`+"\r\n", rec.Name, rec.Target) 210 default: 211 panic(fmt.Errorf("ERROR: generatePowerShellCreate() does not yet handle recType=%s recName=%#v content=%#v)\n", rec.Type, rec.Name, content)) 212 // We panic so that we quickly find any switch statements 213 // that have not been updated for a new RR type. 214 } 215 text += "\r\n" 216 217 return text 218 } 219 220 // generatePowerShellModify generates PowerShell commands to MODIFY a record. 221 func (c *adProvider) generatePowerShellModify(domainname, recName, recType, oldContent, newContent string, oldTTL, newTTL uint32) string { 222 223 var queryField, queryContent string 224 225 switch recType { // #rtype_variations 226 case "A": 227 queryField = "IPv4address" 228 queryContent = `"` + oldContent + `"` 229 case "CNAME": 230 queryField = "HostNameAlias" 231 queryContent = `"` + oldContent + `"` 232 default: 233 panic(fmt.Errorf("ERROR: generatePowerShellModify() does not yet handle recType=%s recName=%#v content=(%#v, %#v)\n", recType, recName, oldContent, newContent)) 234 // We panic so that we quickly find any switch statements 235 // that have not been updated for a new RR type. 236 } 237 238 text := "\r\n" // Skip a line. 239 text += fmt.Sprintf(`echo "MODIFY %s %s %s old=%s new=%s"`, recName, domainname, recType, oldContent, newContent) 240 text += "\r\n" 241 242 text += "$OldObj = Get-DnsServerResourceRecord" 243 text += fmt.Sprintf(` -ComputerName "%s"`, c.adServer) 244 text += fmt.Sprintf(` -ZoneName "%s"`, domainname) 245 text += fmt.Sprintf(` -Name "%s"`, recName) 246 text += fmt.Sprintf(` -RRType "%s"`, recType) 247 text += fmt.Sprintf(" | Where-Object {$_.RecordData.%s -eq %s -and $_.HostName -eq \"%s\"}", queryField, queryContent, recName) 248 text += "\r\n" 249 text += `if($OldObj.Length -ne $null){ throw "Error, multiple results for Get-DnsServerResourceRecord" }` 250 text += "\r\n" 251 252 text += "$NewObj = $OldObj.Clone()" 253 text += "\r\n" 254 255 if oldContent != newContent { 256 text += fmt.Sprintf(`$NewObj.RecordData.%s = "%s"`, queryField, newContent) 257 text += "\r\n" 258 } 259 260 if oldTTL != newTTL { 261 text += fmt.Sprintf(`$NewObj.TimeToLive = New-TimeSpan -Seconds %d`, newTTL) 262 text += "\r\n" 263 } 264 265 text += "Set-DnsServerResourceRecord" 266 text += fmt.Sprintf(` -ComputerName "%s"`, c.adServer) 267 text += fmt.Sprintf(` -ZoneName "%s"`, domainname) 268 text += fmt.Sprintf(` -NewInputObject $NewObj -OldInputObject $OldObj`) 269 text += "\r\n" 270 271 return text 272 } 273 274 func (c *adProvider) generatePowerShellDelete(domainname, recName, recType, content string) string { 275 text := fmt.Sprintf(`echo "DELETE %s %s %s"`, recType, recName, content) 276 text += "\r\n" 277 text += `Remove-DnsServerResourceRecord -Force -ComputerName "%s" -ZoneName "%s" -Name "%s" -RRType "%s" -RecordData "%s"` 278 text += "\r\n" 279 return fmt.Sprintf(text, c.adServer, domainname, recName, recType, content) 280 } 281 282 func (c *adProvider) createRec(domainname string, rec *models.RecordConfig) []*models.Correction { 283 arr := []*models.Correction{ 284 { 285 Msg: fmt.Sprintf("CREATE record: %s %s ttl(%d) %s", rec.Name, rec.Type, rec.TTL, rec.Target), 286 F: func() error { 287 return c.powerShellDoCommand(c.generatePowerShellCreate(domainname, rec), true) 288 }}, 289 } 290 return arr 291 } 292 293 func (c *adProvider) modifyRec(domainname string, m diff.Correlation) *models.Correction { 294 old, rec := m.Existing, m.Desired 295 return &models.Correction{ 296 Msg: m.String(), 297 F: func() error { 298 return c.powerShellDoCommand(c.generatePowerShellModify(domainname, rec.Name, rec.Type, old.Target, rec.Target, old.TTL, rec.TTL), true) 299 }, 300 } 301 } 302 303 func (c *adProvider) deleteRec(domainname string, rec *models.RecordConfig) *models.Correction { 304 return &models.Correction{ 305 Msg: fmt.Sprintf("DELETE record: %s %s ttl(%d) %s", rec.Name, rec.Type, rec.TTL, rec.Target), 306 F: func() error { 307 return c.powerShellDoCommand(c.generatePowerShellDelete(domainname, rec.Name, rec.Type, rec.Target), true) 308 }, 309 } 310 }