diff --git a/go.mod b/go.mod index eea9d0f..a882ad3 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/neticdk/tidydns-go go 1.23 -require github.com/stretchr/testify v1.8.4 +require github.com/stretchr/testify v1.10.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index d096685..7825265 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/tidydns/tidydns.go b/pkg/tidydns/tidydns.go index 4fe0861..78f52a6 100644 --- a/pkg/tidydns/tidydns.go +++ b/pkg/tidydns/tidydns.go @@ -9,6 +9,7 @@ import ( "net/url" "strconv" "strings" + "time" ) type TidyDNSClient interface { @@ -26,6 +27,10 @@ type TidyDNSClient interface { FindRecord(ctx context.Context, zoneID int, name string, rType RecordType) ([]*RecordInfo, error) ListRecords(ctx context.Context, zoneID int) ([]*RecordInfo, error) DeleteRecord(ctx context.Context, zoneID int, recordID int) error + CreateInternalUser(ctx context.Context, username string, password string, description string, changePasswordOnFirstLogin bool, authGroup AuthGroup, userAllow []UserAllowID) (UserID, error) + GetInternalUser(ctx context.Context, userID UserID) (*UserInfo, error) + UpdateInternalUser(ctx context.Context, userID UserID, password *string, description *string, authGroup *AuthGroup, userAllow []UserAllowID) error + DeleteInternalUser(ctx context.Context, userID UserID) error } type ZoneInfo struct { @@ -63,9 +68,32 @@ type RecordInfo struct { Location LocationID } +type UserInfo struct { + ModifiedBy string + Description string + ModifiedDate time.Time + Username string + AuthGroup AuthGroup + Name string + PasswdChangedDate time.Time + Id UserID + Groups []UserInfoGroup +} + +type UserInfoGroup struct { + GroupName string `json:"groupname"` + Name string `json:"name"` + Notes *string `json:"notes,omitempty"` + Id int `json:"id"` + Description *string `json:"description,omitempty"` +} + +type UserID int type LocationID int type RecordType int type RecordStatus int +type AuthGroup int +type UserAllowID int //goland:noinspection GoUnusedConst const ( @@ -84,8 +112,15 @@ const ( RecordTypeSSHFP RecordType = 8 RecordTypeTLSA RecordType = 9 RecordTypeCAA RecordType = 10 + + AuthGroupUser AuthGroup = 2 + AuthGroupSuperAdmin AuthGroup = 1 ) +const errorTidyDNS = "error from tidyDNS server: %s" +const headerContentType = "Content-Type" +const mimeForm = "application/x-www-form-urlencoded" + type tidyDNSClient struct { client *http.Client username string @@ -93,6 +128,222 @@ type tidyDNSClient struct { baseURL string } +func (c *tidyDNSClient) CreateInternalUser(ctx context.Context, username string, password string, description string, changePasswordOnFirstLogin bool, authGroup AuthGroup, userAllow []UserAllowID) (UserID, error) { + var userAllowFormatted []string + if len(userAllow) > 0 { + userAllowFormatted = make([]string, 0, len(userAllowFormatted)) + for _, id := range userAllow { + userAllowFormatted = append(userAllowFormatted, strconv.Itoa(int(id))) + } + } else { + userAllowFormatted = []string{""} + } + + var changePasswordOnFirstLoginFormatted string + if changePasswordOnFirstLogin { + changePasswordOnFirstLoginFormatted = "1" + } else { + changePasswordOnFirstLoginFormatted = "0" + } + + data := url.Values{ + "username": {username}, + "epassword": {password}, + "epassword_verify": {password}, + "change_password_on_first_login": {changePasswordOnFirstLoginFormatted}, + "description": {description}, + "auth_group": {strconv.Itoa(int(authGroup))}, + //"tmp_auth_group": {""}, + "user_allow": userAllowFormatted, + } + + newUserUrl := fmt.Sprintf("%s/=/user/new", c.baseURL) + req, err := http.NewRequestWithContext( + ctx, + "POST", + newUserUrl, + strings.NewReader(data.Encode()), + ) + if err != nil { + return 0, err + } + req.SetBasicAuth(c.username, c.password) + req.Header.Set(headerContentType, mimeForm) + + res, err := c.client.Do(req) + if err != nil { + return 0, err + } + defer closeResponse(res) + if res.StatusCode != http.StatusOK { + bodyBytes, err := io.ReadAll(res.Body) + bodyString := string(bodyBytes) + if err != nil { + return 0, err + } + if strings.Contains(bodyString, fmt.Sprintf("Key (username)=(%s) already exists", username)) { + return 0, fmt.Errorf("username already exists") + } + return 0, fmt.Errorf(errorTidyDNS, res.Status) + } + + var user userCreate + err = json.NewDecoder(res.Body).Decode(&user) + if err != nil { + return 0, err + } + + return UserID(user.Data.Id), nil +} + +func (c *tidyDNSClient) GetInternalUser(ctx context.Context, userID UserID) (*UserInfo, error) { + userLookupUrl := fmt.Sprintf("%s/=/user/%s", c.baseURL, strconv.Itoa(int(userID))) + req, err := http.NewRequestWithContext( + ctx, + "GET", + userLookupUrl, + nil, + ) + if err != nil { + return nil, err + } + + req.SetBasicAuth(c.username, c.password) + + res, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer closeResponse(res) + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf(errorTidyDNS, res.Status) + } + + var user userRead + err = json.NewDecoder(res.Body).Decode(&user) + if err != nil { + return nil, err + } + + modifiedDate, err := time.Parse(time.DateTime, user.ModifiedDate) + if err != nil { + return nil, err + } + + passwordChangedDate, err := time.Parse(time.DateTime, user.PasswdChangedDate) + if err != nil { + return nil, err + } + + var ag AuthGroup + switch user.AuthGroup { + case "User": + ag = AuthGroupUser + case "SuperAdmin": + ag = AuthGroupSuperAdmin + default: + return nil, fmt.Errorf("unknown auth group") + } + + return &UserInfo{ + ModifiedBy: user.ModifiedBy, + Description: user.Description, + ModifiedDate: modifiedDate, + Username: user.Username, + AuthGroup: ag, + Name: user.Name, + PasswdChangedDate: passwordChangedDate, + Id: UserID(user.Id), + Groups: user.Groups, + }, nil +} + +func (c *tidyDNSClient) UpdateInternalUser(ctx context.Context, userID UserID, password *string, description *string, authGroup *AuthGroup, userAllow []UserAllowID) error { + data := url.Values{} + + if password != nil { + data.Set("epassword", *password) + data.Set("epassword_verify", *password) + } + + if description != nil { + data.Set("description", *description) + } + + if authGroup != nil { + data.Set("auth_group", strconv.Itoa(int(*authGroup))) + } + + if userAllow != nil { + var userAllowFormatted []string + if len(userAllow) > 0 { + userAllowFormatted = make([]string, 0, len(userAllowFormatted)) + for _, id := range userAllow { + userAllowFormatted = append(userAllowFormatted, strconv.Itoa(int(id))) + } + } else { + userAllowFormatted = []string{""} + } + data["user_allow"] = userAllowFormatted + } + + userLookupUrl := fmt.Sprintf("%s/=/user/%s", c.baseURL, strconv.Itoa(int(userID))) + req, err := http.NewRequestWithContext( + ctx, + "POST", + userLookupUrl, + strings.NewReader(data.Encode()), + ) + if err != nil { + return err + } + req.SetBasicAuth(c.username, c.password) + req.Header.Set(headerContentType, mimeForm) + + res, err := c.client.Do(req) + if err != nil { + return err + } + defer closeResponse(res) + if res.StatusCode != http.StatusOK { + return fmt.Errorf(errorTidyDNS, res.Status) + } + + var user userCreate + err = json.NewDecoder(res.Body).Decode(&user) + if err != nil { + return err + } + + return nil +} + +func (c *tidyDNSClient) DeleteInternalUser(ctx context.Context, userID UserID) error { + userLookupUrl := fmt.Sprintf("%s/=/user/%s", c.baseURL, strconv.Itoa(int(userID))) + req, err := http.NewRequestWithContext( + ctx, + "DELETE", + userLookupUrl, + nil, + ) + if err != nil { + return err + } + + req.SetBasicAuth(c.username, c.password) + + res, err := c.client.Do(req) + if err != nil { + return err + } + defer closeResponse(res) + if res.StatusCode != http.StatusOK { + return fmt.Errorf(errorTidyDNS, res.Status) + } + + return nil +} + func New(baseURL, username, password string) TidyDNSClient { return &tidyDNSClient{ baseURL: baseURL, @@ -102,8 +353,20 @@ func New(baseURL, username, password string) TidyDNSClient { } } +func closeResponse(resp *http.Response) { + if resp != nil { + _ = resp.Body.Close() + } +} + func (c *tidyDNSClient) GetSubnetIDs(ctx context.Context, subnetCIDR string) (*SubnetIDs, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/=/dhcp_subnet?subnet=%s", c.baseURL, subnetCIDR), nil) + dhcpSubnetUrl := fmt.Sprintf("%s/=/dhcp_subnet?subnet=%s", c.baseURL, subnetCIDR) + req, err := http.NewRequestWithContext( + ctx, + "GET", + dhcpSubnetUrl, + nil, + ) if err != nil { return nil, err } @@ -114,11 +377,9 @@ func (c *tidyDNSClient) GetSubnetIDs(ctx context.Context, subnetCIDR string) (*S if err != nil { return nil, err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("error from tidyDNS server: %s", res.Status) + return nil, fmt.Errorf(errorTidyDNS, res.Status) } var subnets []dhcpSubnet @@ -143,7 +404,13 @@ func (c *tidyDNSClient) GetSubnetIDs(ctx context.Context, subnetCIDR string) (*S } func (c *tidyDNSClient) GetFreeIP(ctx context.Context, subnetID int) (string, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/=/dhcp_subnet_free_ip/%d", c.baseURL, subnetID), nil) + dhcpFreeIPUrl := fmt.Sprintf("%s/=/dhcp_subnet_free_ip/%d", c.baseURL, subnetID) + req, err := http.NewRequestWithContext( + ctx, + "GET", + dhcpFreeIPUrl, + nil, + ) if err != nil { return "", err } @@ -154,11 +421,9 @@ func (c *tidyDNSClient) GetFreeIP(ctx context.Context, subnetID int) (string, er if err != nil { return "", err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { - return "", fmt.Errorf("error from tidyDNS server: %s", res.Status) + return "", fmt.Errorf(errorTidyDNS, res.Status) } var freeIP dhcpFreeIP @@ -181,20 +446,24 @@ func (c *tidyDNSClient) CreateDHCPInterface(ctx context.Context, createInfo Crea var checkstring = fmt.Sprintf("Key (destination)=(%s) already exists", createInfo.InterfaceIP) - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/=/dhcp_interface//new", c.baseURL), strings.NewReader(data.Encode())) + dhcpInterfaceNewUrl := fmt.Sprintf("%s/=/dhcp_interface//new", c.baseURL) + req, err := http.NewRequestWithContext( + ctx, + "POST", + dhcpInterfaceNewUrl, + strings.NewReader(data.Encode()), + ) if err != nil { return 0, err } req.SetBasicAuth(c.username, c.password) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set(headerContentType, mimeForm) res, err := c.client.Do(req) if err != nil { return 0, err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { bodyBytes, err := io.ReadAll(res.Body) @@ -205,7 +474,7 @@ func (c *tidyDNSClient) CreateDHCPInterface(ctx context.Context, createInfo Crea if strings.Contains(bodyString, checkstring) { return 1, fmt.Errorf("try again") } - return 0, fmt.Errorf("error from tidyDNS server: %s", res.Status) + return 0, fmt.Errorf(errorTidyDNS, res.Status) } var createResp interfaceCreate @@ -218,7 +487,13 @@ func (c *tidyDNSClient) CreateDHCPInterface(ctx context.Context, createInfo Crea } func (c *tidyDNSClient) ReadDHCPInterface(ctx context.Context, interfaceID int) (*InterfaceInfo, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/=/dhcp_interface/?id=%d", c.baseURL, interfaceID), nil) + dhcpInterfaceReadUrl := fmt.Sprintf("%s/=/dhcp_interface/?id=%d", c.baseURL, interfaceID) + req, err := http.NewRequestWithContext( + ctx, + "GET", + dhcpInterfaceReadUrl, + nil, + ) if err != nil { return nil, err } @@ -229,11 +504,9 @@ func (c *tidyDNSClient) ReadDHCPInterface(ctx context.Context, interfaceID int) if err != nil { return nil, err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("error from tidyDNS server: %s", res.Status) + return nil, fmt.Errorf(errorTidyDNS, res.Status) } var interfaceRead interfaceRead @@ -253,22 +526,26 @@ func (c *tidyDNSClient) UpdateDHCPInterfaceName(ctx context.Context, interfaceID "name": {interfaceName}, } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/=/dhcp_interface//%d", c.baseURL, interfaceID), strings.NewReader(data.Encode())) + dhcpInterfaceLookupUrl := fmt.Sprintf("%s/=/dhcp_interface//%d", c.baseURL, interfaceID) + req, err := http.NewRequestWithContext( + ctx, + "POST", + dhcpInterfaceLookupUrl, + strings.NewReader(data.Encode()), + ) if err != nil { return 0, err } req.SetBasicAuth(c.username, c.password) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set(headerContentType, mimeForm) res, err := c.client.Do(req) if err != nil { return 0, err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { - return 0, fmt.Errorf("error from tidyDNS server: %s", res.Status) + return 0, fmt.Errorf(errorTidyDNS, res.Status) } var createResp interfaceCreate @@ -281,7 +558,13 @@ func (c *tidyDNSClient) UpdateDHCPInterfaceName(ctx context.Context, interfaceID } func (c *tidyDNSClient) DeleteDHCPInterface(ctx context.Context, interfaceID int) error { - req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/=/dhcp_interface/%d", c.baseURL, interfaceID), nil) + dhcpInterfaceLookupUrl := fmt.Sprintf("%s/=/dhcp_interface/%d", c.baseURL, interfaceID) + req, err := http.NewRequestWithContext( + ctx, + "DELETE", + dhcpInterfaceLookupUrl, + nil, + ) if err != nil { return err } @@ -292,11 +575,9 @@ func (c *tidyDNSClient) DeleteDHCPInterface(ctx context.Context, interfaceID int if err != nil { return err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { - return fmt.Errorf("error from tidyDNS server: %s", res.Status) + return fmt.Errorf(errorTidyDNS, res.Status) } return nil @@ -304,7 +585,12 @@ func (c *tidyDNSClient) DeleteDHCPInterface(ctx context.Context, interfaceID int func (c *tidyDNSClient) ListZones(ctx context.Context) ([]*ZoneInfo, error) { var zones []zoneInfo - err := c.getData(ctx, fmt.Sprintf("%s/=/zone?type=json", c.baseURL), &zones) + zoneListUrl := fmt.Sprintf("%s/=/zone?type=json", c.baseURL) + err := c.getData( + ctx, + zoneListUrl, + &zones, + ) if err != nil { return nil, err } @@ -321,7 +607,12 @@ func (c *tidyDNSClient) ListZones(ctx context.Context) ([]*ZoneInfo, error) { func (c *tidyDNSClient) FindZoneID(ctx context.Context, name string) (int, error) { var zones []zoneInfo - err := c.getData(ctx, fmt.Sprintf("%s/=/zone?type=json&name=%s", c.baseURL, name), &zones) + zoneLookupUrl := fmt.Sprintf("%s/=/zone?type=json&name=%s", c.baseURL, name) + err := c.getData( + ctx, + zoneLookupUrl, + &zones, + ) if err != nil { return 0, err } @@ -350,25 +641,35 @@ func (c *tidyDNSClient) CreateRecord(ctx context.Context, zoneID int, info Recor "location_id": {strconv.Itoa(int(info.Location))}, } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/=/record/new/%d", c.baseURL, zoneID), strings.NewReader(data.Encode())) + newRecordUrl := fmt.Sprintf("%s/=/record/new/%d", c.baseURL, zoneID) + req, err := http.NewRequestWithContext( + ctx, + "POST", + newRecordUrl, + strings.NewReader(data.Encode()), + ) if err != nil { return 0, err } req.SetBasicAuth(c.username, c.password) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set(headerContentType, mimeForm) res, err := c.client.Do(req) if err != nil || res == nil { return 0, err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { - return 0, fmt.Errorf("error from tidyDNS server: %s", res.Status) + return 0, fmt.Errorf(errorTidyDNS, res.Status) } - req, err = http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/=/record_merged?zone_id=%d", c.baseURL, zoneID), nil) + recordMergeUrl := fmt.Sprintf("%s/=/record_merged?zone_id=%d", c.baseURL, zoneID) + req, err = http.NewRequestWithContext( + ctx, + "GET", + recordMergeUrl, + nil, + ) if err != nil { return 0, err } @@ -379,11 +680,9 @@ func (c *tidyDNSClient) CreateRecord(ctx context.Context, zoneID int, info Recor if err != nil || res == nil { return 0, err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { - return 0, fmt.Errorf("error from tidyDNS server: %s", res.Status) + return 0, fmt.Errorf(errorTidyDNS, res.Status) } var records []recordList @@ -410,22 +709,26 @@ func (c *tidyDNSClient) UpdateRecord(ctx context.Context, zoneID int, recordID i "location_id": {strconv.Itoa(int(info.Location))}, } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/=/record/%d/%d", c.baseURL, recordID, zoneID), strings.NewReader(data.Encode())) + zoneLookupUrl := fmt.Sprintf("%s/=/record/%d/%d", c.baseURL, recordID, zoneID) + req, err := http.NewRequestWithContext( + ctx, + "POST", + zoneLookupUrl, + strings.NewReader(data.Encode()), + ) if err != nil { return err } req.SetBasicAuth(c.username, c.password) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set(headerContentType, mimeForm) res, err := c.client.Do(req) if err != nil || res == nil { return err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { - return fmt.Errorf("error from tidyDNS server: %s", res.Status) + return fmt.Errorf(errorTidyDNS, res.Status) } return nil @@ -433,7 +736,12 @@ func (c *tidyDNSClient) UpdateRecord(ctx context.Context, zoneID int, recordID i func (c *tidyDNSClient) FindRecord(ctx context.Context, zoneID int, name string, rType RecordType) ([]*RecordInfo, error) { var records []recordList - err := c.getData(ctx, fmt.Sprintf("%s/=/record?type=json&zone=%d&name=%s", c.baseURL, zoneID, name), &records) + recordLookupUrl := fmt.Sprintf("%s/=/record?type=json&zone=%d&name=%s", c.baseURL, zoneID, name) + err := c.getData( + ctx, + recordLookupUrl, + &records, + ) if err != nil { return nil, err } @@ -457,7 +765,12 @@ func (c *tidyDNSClient) FindRecord(ctx context.Context, zoneID int, name string, func (c *tidyDNSClient) ListRecords(ctx context.Context, zoneID int) ([]*RecordInfo, error) { var records []recordList - err := c.getData(ctx, fmt.Sprintf("%s/=/record_merged?type=json&zone_id=%d&showall=1", c.baseURL, zoneID), &records) + recordMergeUrl := fmt.Sprintf("%s/=/record_merged?type=json&zone_id=%d&showall=1", c.baseURL, zoneID) + err := c.getData( + ctx, + recordMergeUrl, + &records, + ) if err != nil { return nil, err } @@ -479,7 +792,12 @@ func (c *tidyDNSClient) ListRecords(ctx context.Context, zoneID int) ([]*RecordI func (c *tidyDNSClient) ReadRecord(ctx context.Context, zoneID int, recordID int) (*RecordInfo, error) { var record recordRead - err := c.getData(ctx, fmt.Sprintf("%s/=/record/%d/%d", c.baseURL, zoneID, recordID), &record) + recordLookupUrl := fmt.Sprintf("%s/=/record/%d/%d", c.baseURL, zoneID, recordID) + err := c.getData( + ctx, + recordLookupUrl, + &record, + ) if err != nil { return nil, err } @@ -497,7 +815,13 @@ func (c *tidyDNSClient) ReadRecord(ctx context.Context, zoneID int, recordID int } func (c *tidyDNSClient) DeleteRecord(ctx context.Context, zoneID int, recordID int) error { - req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/=/record/%d/%d", c.baseURL, recordID, zoneID), nil) + recordLookupUrl := fmt.Sprintf("%s/=/record/%d/%d", c.baseURL, recordID, zoneID) + req, err := http.NewRequestWithContext( + ctx, + "DELETE", + recordLookupUrl, + nil, + ) if err != nil { return err } @@ -508,11 +832,9 @@ func (c *tidyDNSClient) DeleteRecord(ctx context.Context, zoneID int, recordID i if err != nil || res == nil { return err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { - return fmt.Errorf("error from tidyDNS server: %s", res.Status) + return fmt.Errorf(errorTidyDNS, res.Status) } return nil @@ -530,11 +852,9 @@ func (c *tidyDNSClient) getData(ctx context.Context, url string, value interface if err != nil || res == nil { return err } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(res.Body) + defer closeResponse(res) if res.StatusCode != http.StatusOK { - return fmt.Errorf("error from tidyDNS server: %s", res.Status) + return fmt.Errorf(errorTidyDNS, res.Status) } err = json.NewDecoder(res.Body).Decode(value) diff --git a/pkg/tidydns/tidydns_test.go b/pkg/tidydns/tidydns_test.go index c6a89fa..7a50a56 100644 --- a/pkg/tidydns/tidydns_test.go +++ b/pkg/tidydns/tidydns_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -100,11 +101,16 @@ const readResponse = `{ "brother_destination": null }` +const userCreateResponse = `{"data":{"id":144},"status":"0"}` +const userReadResponse = `{"modified_by":"jra-api-test","description":"Awesome test user","modified_date":"2024-12-03 14:17:22","username":"jra-test-user","auth_group":"User","name":"jra-test-user","epassword":"*****","passwd_changed_date":"2024-12-03 14:17:22","id":148,"groups":[{"groupname":"user","name":"User","notes":null,"id":2,"description":null}]}` +const userUpdateResponse = `{"status":"0","data":{"id":146}}` +const userDeleteResponse = `{"status":"0"}` + func TestGetSubnetIDs(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Equal(t, "10.68.0.128/26", req.URL.Query().Get("subnet")) assert.Equal(t, "GET", req.Method) - rw.Write([]byte(subnetResponse)) + _, _ = rw.Write([]byte(subnetResponse)) })) defer server.Close() @@ -120,7 +126,7 @@ func TestGetFreeIP(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Contains(t, req.URL.Path, "1185") assert.Equal(t, "GET", req.Method) - rw.Write([]byte(freeIPResponse)) + _, _ = rw.Write([]byte(freeIPResponse)) })) defer server.Close() @@ -133,7 +139,7 @@ func TestGetFreeIP(t *testing.T) { func TestCreateDHCPInterfaceV1(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Equal(t, "POST", req.Method) - rw.Write([]byte(createResponseV1)) + _, _ = rw.Write([]byte(createResponseV1)) })) defer server.Close() @@ -152,7 +158,7 @@ func TestCreateDHCPInterfaceV1(t *testing.T) { func TestCreateDHCPInterfaceV2(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Equal(t, "POST", req.Method) - rw.Write([]byte(createResponseV2)) + _, _ = rw.Write([]byte(createResponseV2)) })) defer server.Close() @@ -172,7 +178,7 @@ func TestReadDHCPInterface(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Equal(t, "30641", req.URL.Query().Get("id")) assert.Equal(t, "GET", req.Method) - rw.Write([]byte(readResponse)) + _, _ = rw.Write([]byte(readResponse)) })) defer server.Close() @@ -187,7 +193,7 @@ func TestUpdateDHCPInterfaceName(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Contains(t, req.URL.Path, "30641") assert.Equal(t, "POST", req.Method) - rw.Write([]byte(createResponseV1)) + _, _ = rw.Write([]byte(createResponseV1)) })) defer server.Close() @@ -201,7 +207,7 @@ func TestDeleteDHCPInterface(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Contains(t, req.URL.Path, "30641") assert.Equal(t, "DELETE", req.Method) - rw.Write([]byte(createResponseV1)) + _, _ = rw.Write([]byte(createResponseV1)) })) defer server.Close() @@ -214,7 +220,7 @@ func TestFindZoneID(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Contains(t, req.URL.Query().Get("name"), "hackerdays.trifork.dev") assert.Equal(t, "GET", req.Method) - rw.Write([]byte(zoneSearchResponse)) + _, _ = rw.Write([]byte(zoneSearchResponse)) })) defer server.Close() @@ -226,7 +232,7 @@ func TestFindZoneID(t *testing.T) { func TestCreateRecord(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.Write([]byte(readRecordListResponse)) + _, _ = rw.Write([]byte(readRecordListResponse)) })) defer server.Close() @@ -247,7 +253,7 @@ func TestReadRecord(t *testing.T) { assert.Contains(t, req.URL.Path, "2861") assert.Contains(t, req.URL.Path, "64694") assert.Equal(t, "GET", req.Method) - rw.Write([]byte(readRecordResponse)) + _, _ = rw.Write([]byte(readRecordResponse)) })) defer server.Close() @@ -266,7 +272,7 @@ func TestDeleteRecord(t *testing.T) { assert.Contains(t, req.URL.Path, "2861") assert.Contains(t, req.URL.Path, "64694") assert.Equal(t, "DELETE", req.Method) - rw.Write([]byte(createResponse)) + _, _ = rw.Write([]byte(createResponse)) })) defer server.Close() @@ -278,7 +284,7 @@ func TestDeleteRecord(t *testing.T) { func TestUpdateRecord(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Equal(t, "POST", req.Method) - rw.Write([]byte(readRecordListResponse)) + _, _ = rw.Write([]byte(readRecordListResponse)) })) defer server.Close() @@ -296,7 +302,7 @@ func TestFindRecord(t *testing.T) { assert.Equal(t, "prod1-api.trifork.shared", req.URL.Query().Get("name")) assert.Equal(t, "2861", req.URL.Query().Get("zone")) assert.Equal(t, "GET", req.Method) - rw.Write([]byte(findRecordResponse)) + _, _ = rw.Write([]byte(findRecordResponse)) })) defer server.Close() @@ -310,7 +316,7 @@ func TestFindRecord(t *testing.T) { func TestListZones(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Equal(t, "GET", req.Method) - rw.Write([]byte(listZonesResponse)) + _, _ = rw.Write([]byte(listZonesResponse)) })) defer server.Close() @@ -324,7 +330,7 @@ func TestListRecords(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Equal(t, "2861", req.URL.Query().Get("zone_id")) assert.Equal(t, "GET", req.Method) - rw.Write([]byte(listRecordsResponse)) + _, _ = rw.Write([]byte(listRecordsResponse)) })) defer server.Close() @@ -334,6 +340,107 @@ func TestListRecords(t *testing.T) { assert.Equal(t, len(records), 22) } +func TestCreateInternalUser(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.NoError(t, req.ParseForm()) + assert.Equal(t, "/=/user/new", req.URL.Path) + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "test_user", req.PostForm.Get("username")) + assert.Equal(t, "test_password", req.PostForm.Get("epassword")) + assert.Equal(t, "test_password", req.PostForm.Get("epassword_verify")) + assert.Equal(t, "0", req.PostForm.Get("change_password_on_first_login")) + assert.Equal(t, "2", req.PostForm.Get("auth_group")) + assert.Equal(t, "", req.PostForm.Get("user_allow")) + _, _ = rw.Write([]byte(userCreateResponse)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + id, err := c.CreateInternalUser( + context.Background(), + "test_user", + "test_password", + "description", + false, + AuthGroupUser, + []UserAllowID{}, + ) + assert.NoError(t, err) + assert.Equal(t, id, UserID(144)) +} + +func TestGetInternalUser(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "/=/user/144", req.URL.Path) + assert.Equal(t, "GET", req.Method) + _, _ = rw.Write([]byte(userReadResponse)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + userInfo, err := c.GetInternalUser(context.Background(), 144) + assert.NoError(t, err) + assert.Equal(t, userInfo.AuthGroup, AuthGroupUser) + assert.Equal(t, userInfo.Description, "Awesome test user") + assert.Equal(t, len(userInfo.Groups), 1) + assert.Equal(t, userInfo.Groups[0].Id, 2) + assert.Equal(t, userInfo.Groups[0].Name, "User") + assert.Nil(t, userInfo.Groups[0].Notes) + assert.Equal(t, userInfo.Groups[0].GroupName, "user") + assert.Nil(t, userInfo.Groups[0].Description) + assert.Equal(t, userInfo.Id, UserID(148)) + assert.Equal(t, userInfo.ModifiedBy, "jra-api-test") + assert.Equal(t, userInfo.ModifiedDate, time.Date(2024, 12, 03, 14, 17, 22, 0, time.UTC)) + assert.Equal(t, userInfo.Name, "jra-test-user") + assert.Equal(t, userInfo.PasswdChangedDate, time.Date(2024, 12, 03, 14, 17, 22, 0, time.UTC)) + assert.Equal(t, userInfo.Username, "jra-test-user") +} + +func toPtr[T any](s T) *T { + return &s +} + +func TestUpdateInternalUser(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.NoError(t, req.ParseForm()) + assert.Equal(t, "/=/user/146", req.URL.Path) + assert.Equal(t, "POST", req.Method) + assert.False(t, req.PostForm.Has("username")) + assert.Equal(t, "test_password", req.PostForm.Get("epassword")) + assert.Equal(t, "test_password", req.PostForm.Get("epassword_verify")) + assert.False(t, req.PostForm.Has("change_password_on_first_login")) + assert.Equal(t, "2", req.PostForm.Get("auth_group")) + assert.Equal(t, "", req.PostForm.Get("user_allow")) + assert.Equal(t, "desc", req.PostForm.Get("description")) + _, _ = rw.Write([]byte(userUpdateResponse)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + err := c.UpdateInternalUser( + context.Background(), + UserID(146), + toPtr("test_password"), + toPtr("desc"), + toPtr(AuthGroupUser), + []UserAllowID{}, + ) + assert.NoError(t, err) +} + +func TestDeleteInternalUser(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "/=/user/144", req.URL.Path) + assert.Equal(t, "DELETE", req.Method) + _, _ = rw.Write([]byte(userDeleteResponse)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + err := c.DeleteInternalUser(context.Background(), UserID(144)) + assert.NoError(t, err) +} + const zoneSearchResponse = `[ { "soa_record": null, diff --git a/pkg/tidydns/types.go b/pkg/tidydns/types.go index a1ca23d..c4432c7 100644 --- a/pkg/tidydns/types.go +++ b/pkg/tidydns/types.go @@ -54,3 +54,23 @@ type recordList struct { Status interface{} `json:"status"` Location LocationID `json:"location_id"` } + +type userCreate struct { + Data struct { + Id int `json:"id"` + } `json:"data"` + Status string `json:"status"` +} + +type userRead struct { + ModifiedBy string `json:"modified_by"` + Description string `json:"description"` + ModifiedDate string `json:"modified_date"` + Username string `json:"username"` + AuthGroup string `json:"auth_group"` + Name string `json:"name"` + Epassword string `json:"epassword"` + PasswdChangedDate string `json:"passwd_changed_date"` + Id int `json:"id"` + Groups []UserInfoGroup `json:"groups"` +}