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  }