Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/database/queries/zones.sql
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ WHERE id = ?;
SELECT id
FROM zones
WHERE name = ?;

-- name: UpdateZoneConfig :exec
UPDATE zones
SET refresh = ?,
retry = ?,
expire = ?,
ttl = ?
WHERE id = ?;
28 changes: 28 additions & 0 deletions internal/database/zones.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions internal/routes/records.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import (
"github.com/gobuffalo/nulls"
)

const ttlMaxOneWeek = 60 * 60 * 24 * 7

type recordQueries interface {
GetZoneRecords(ctx context.Context, zoneId int64) ([]database.GetZoneRecordsRow, error)
GetZoneRecord(ctx context.Context, row database.GetZoneRecordParams) (database.GetZoneRecordRow, error)
Expand Down
83 changes: 83 additions & 0 deletions internal/routes/zones.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,27 @@ import (
"github.com/miekg/dns"
)

const oneDaySeconds = 60 * 60 * 24
const oneWeekSeconds = oneDaySeconds * 7
const tenMinutesSeconds = 60 * 10

const refreshMaxOneWeek = oneWeekSeconds
const retryMaxOneWeek = oneWeekSeconds
const expireMax90Days = oneDaySeconds * 90
const ttlMaxOneWeek = oneWeekSeconds

type zoneUpdates struct {
Refresh int32 `json:"refresh"`
Retry int32 `json:"retry"`
Expire int32 `json:"expire"`
Ttl int32 `json:"ttl"`
}

type zoneQueries interface {
GetOwnedZones(ctx context.Context, userID string) ([]database.GetOwnedZonesRow, error)
GetZone(ctx context.Context, id int64) (database.Zone, error)
LookupZone(ctx context.Context, name string) (int64, error)
UpdateZoneConfig(ctx context.Context, updateZoneConfigParams database.UpdateZoneConfigParams) error
}

func ZoneToRestZone(zone database.Zone, nameservers []string) rest.Zone {
Expand Down Expand Up @@ -87,6 +104,72 @@ func AddZoneRoutes(r chi.Router, db zoneQueries, keystore *mjwt.KeyStore, namese
json.NewEncoder(rw).Encode(ZoneToRestZone(zone, nameservers.GetNameserversForZone(zone)))
}))

// Update individual zone
r.Put("/zones/{zone_id:[0-9]+}", validateAuthToken(keystore, func(rw http.ResponseWriter, req *http.Request, b mjwt.BaseTypeClaims[auth.AccessTokenClaims]) {
zoneId, err := getZoneId(req)
if err != nil {
http.Error(rw, "Invalid zone ID", http.StatusBadRequest)
return
}

var updates zoneUpdates
dec := json.NewDecoder(req.Body)
dec.DisallowUnknownFields()
err = dec.Decode(&updates)
if err != nil {
logger.Logger.Error("Failed to decode zone updates", "err", err)
http.Error(rw, "Invalid zone update configuration", http.StatusBadRequest)
return
}

if updates.Refresh > refreshMaxOneWeek {
http.Error(rw, "Invalid refresh value, expected less than one week", http.StatusBadRequest)
return
}
if updates.Retry > retryMaxOneWeek {
http.Error(rw, "Invalid retry value, expected less than one week", http.StatusBadRequest)
return
}
if updates.Expire > expireMax90Days {
http.Error(rw, "Invalid expire value, expected less than 90 days", http.StatusBadRequest)
return
}
if updates.Ttl > ttlMaxOneWeek {
http.Error(rw, "Invalid time-to-live value, expected less than one week", http.StatusBadRequest)
return
}

zone, err := db.GetZone(req.Context(), zoneId)
switch {
case errors.Is(err, sql.ErrNoRows):
http.NotFound(rw, req)
return
case err != nil:
logger.Logger.Error("Failed to get zone", "err", err)
http.Error(rw, "Database error occurred", http.StatusInternalServerError)
return
}

if !b.Claims.Perms.Has("domain:owns=" + zone.Name) {
http.NotFound(rw, req)
return
}

err = db.UpdateZoneConfig(req.Context(), database.UpdateZoneConfigParams{
Refresh: updates.Refresh,
Retry: updates.Retry,
Expire: updates.Expire,
Ttl: updates.Ttl,
ID: zoneId,
})
if err != nil {
logger.Logger.Error("Failed to update zone config", "err", err)
http.Error(rw, "Database error occurred", http.StatusInternalServerError)
return
}
http.Error(rw, "OK", http.StatusOK)
Comment thread
mrmelon54 marked this conversation as resolved.
}))

r.Get("/zones/lookup/{zone_name:[a-z0-9-.]+}", validateAuthToken(keystore, func(rw http.ResponseWriter, req *http.Request, b mjwt.BaseTypeClaims[auth.AccessTokenClaims]) {
zoneName := chi.URLParam(req, "zone_name")

Expand Down
97 changes: 97 additions & 0 deletions internal/routes/zones_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package routes

import (
"bytes"
"context"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -20,6 +22,15 @@ import (
type zoneTestQueries struct {
}

func (z *zoneTestQueries) UpdateZoneConfig(ctx context.Context, updateZoneConfigParams database.UpdateZoneConfigParams) error {
if updateZoneConfigParams.ID != 3456 {
return sql.ErrNoRows
}

// Fake update zone config
return nil
}

func (z *zoneTestQueries) GetOwnedZones(ctx context.Context, userID string) ([]database.GetOwnedZonesRow, error) {
if userID != "1234" {
return []database.GetOwnedZonesRow{}, nil
Expand Down Expand Up @@ -142,6 +153,92 @@ func TestAddZoneRoutes(t *testing.T) {
assert.Equal(t, "{\"id\":3456,\"name\":\"example.com\",\"serial\":2025062801,\"admin\":\"admin.example.com\",\"refresh\":10,\"retry\":11,\"expire\":12,\"ttl\":13,\"active\":true,\"nameservers\":[\"ns1.example.com\",\"ns2.example.com\"]}\n", rec.Body.String())
})

t.Run("PUT /zones/{id}", func(t *testing.T) {
zoneUpdatesValid := zoneUpdates{
Refresh: 100,
Retry: 200,
Expire: 300,
Ttl: 400,
}
zoneUpdatesInvalid := []zoneUpdates{
{
Refresh: refreshMaxOneWeek + 1,
Retry: 1,
Expire: 1,
Ttl: 1,
},
{
Refresh: 1,
Retry: retryMaxOneWeek + 1,
Expire: 1,
Ttl: 1,
},
{
Refresh: 1,
Retry: 1,
Expire: expireMax90Days + 1,
Ttl: 1,
},
{
Refresh: 1,
Retry: 1,
Expire: 1,
Ttl: ttlMaxOneWeek + 1,
},
}

zoneUpdatesValidJson, err := json.Marshal(zoneUpdatesValid)
if err != nil {
t.Fatal(err)
}

_, _ = json.Marshal(zoneUpdatesInvalid)

for invalid := range zoneUpdatesInvalid {
invalidJson, err := json.Marshal(invalid)
if err != nil {
t.Fatal(err)
}

rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, "/zones/3456", bytes.NewReader(invalidJson))
ps := auth.NewPermStorage()
ps.Set("domain:owns=example.org")
token, err := issuer.GenerateJwt("1234", "", jwt.ClaimStrings{}, time.Hour, auth.AccessTokenClaims{Perms: ps})
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+token)
r.ServeHTTP(rec, req)
assert.Equal(t, http.StatusBadRequest, rec.Code)
}

rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, "/zones/3456", bytes.NewReader(zoneUpdatesValidJson))
ps := auth.NewPermStorage()
ps.Set("domain:owns=example.org")
token, err := issuer.GenerateJwt("1234", "", jwt.ClaimStrings{}, time.Hour, auth.AccessTokenClaims{Perms: ps})
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+token)
r.ServeHTTP(rec, req)
assert.Equal(t, http.StatusNotFound, rec.Code)

rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPut, "/zones/3456", bytes.NewReader(zoneUpdatesValidJson))
ps = auth.NewPermStorage()
ps.Set("domain:owns=example.com")
token, err = issuer.GenerateJwt("1234", "", jwt.ClaimStrings{}, time.Hour, auth.AccessTokenClaims{Perms: ps})
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+token)
r.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "OK\n", rec.Body.String())
})

t.Run("/zones/lookup/{name}", func(t *testing.T) {
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/zones/lookup/example.com", nil)
Expand Down