Add API endpoint to delete a registered user (resolves joohoi/acme-dns#177)

This commit is contained in:
Pascal Uhlmann 2020-02-17 17:54:18 +01:00
parent 19069f50ec
commit 7910db24da
7 changed files with 205 additions and 4 deletions

View File

@ -75,6 +75,26 @@ With the credentials, you can update the TXT response in the service to match th
}
```
### Unregister endpoint
This method deletes a registered user and the associated TXT records from the database. The subdomain has to be sent as JSON input. The user's credentials must be provided like with an update update request.
#### Example input
```json
{
"subdomain": "0b3736b4-32dd-43c1-8b4f-db117767b3ca"
}
```
#### Response
```Status: 200 OK```
```json
{
"unregister": "c2d7e8fd-ff4c-41c6-8f16-1d76628cbfde"
}
```
### Update endpoint
The method allows you to update the TXT answer contents of your unique subdomain. Usually carried automatically by automated ACME client.

33
api.go
View File

@ -6,6 +6,7 @@ import (
"io/ioutil"
"net/http"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
)
@ -19,6 +20,12 @@ type RegResponse struct {
Allowfrom []string `json:"allowfrom"`
}
// UnregRequest is a struct providing the data to unregister
type UnregRequest struct {
Username uuid.UUID `json:"username"`
Subdomain string `json:"subdomain"`
}
func webRegisterPost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var regStatus int
var reg []byte
@ -71,6 +78,32 @@ func webRegisterPost(w http.ResponseWriter, r *http.Request, _ httprouter.Params
w.Write(reg)
}
func webUnregisterPost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var unregStatus int
var err error
var upd []byte
// Get user data
unregData, ok := r.Context().Value(ACMETxtKey).(UnregRequest)
if !ok {
log.WithFields(log.Fields{"error": "context"}).Error("Context error")
}
// Delete user
err = DB.Unregister(unregData.Username)
if err != nil {
unregStatus = http.StatusInternalServerError
upd = jsonError(fmt.Sprintf("%s (%v)", "delete_error", err))
} else {
log.WithFields(log.Fields{"user": unregData.Username.String()}).Debug("Deleted user")
upd = []byte("{\"unregister\": \"" + unregData.Username.String() + "\"}")
unregStatus = http.StatusOK
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(unregStatus)
w.Write(upd)
}
func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var updStatus int
var upd []byte

View File

@ -73,9 +73,11 @@ func setupRouter(debug bool, noauth bool) http.Handler {
api.POST("/register", webRegisterPost)
api.GET("/health", healthCheck)
if noauth {
api.POST("/unregister", noAuth(webUnregisterPost))
api.POST("/update", noAuth(webUpdatePost))
} else {
api.POST("/update", Auth(webUpdatePost))
api.POST("/unregister", AuthUnregister(webUnregisterPost))
api.POST("/update", AuthUpdate(webUpdatePost))
}
return c.Handler(api)
}
@ -191,6 +193,43 @@ func TestApiRegisterWithMockDB(t *testing.T) {
DB.SetBackend(oldDb)
}
func TestApiUnregisterWithoutCredentials(t *testing.T) {
router := setupRouter(false, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
e.POST("/unregister").Expect().
Status(http.StatusUnauthorized).
JSON().Object().
ContainsKey("error")
}
func TestApiUnregisterInvalidSubdomain(t *testing.T) {
unregisterJSON := map[string]interface{}{
"subdomain": ""}
// Invalid username
unregisterJSON["subdomain"] = "example.com"
router := setupRouter(false, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
newUser, err := DB.Register(cidrslice{})
if err != nil {
t.Errorf("Could not create new user, got error [%v]", err)
}
e.POST("/unregister").
WithJSON(unregisterJSON).
WithHeader("X-Api-User", newUser.Username.String()).
WithHeader("X-Api-Key", newUser.Password).
Expect().
Status(http.StatusUnauthorized).
JSON().Object().
ContainsKey("error")
}
func TestApiUpdateWithInvalidSubdomain(t *testing.T) {
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

38
auth.go
View File

@ -16,8 +16,8 @@ type key int
// ACMETxtKey is a context key for ACMETxt struct
const ACMETxtKey key = 0
// Auth middleware for update request
func Auth(update httprouter.Handle) httprouter.Handle {
// AuthUpdate middleware for update request
func AuthUpdate(update httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
postData := ACMETxt{}
userOK := false
@ -55,6 +55,40 @@ func Auth(update httprouter.Handle) httprouter.Handle {
}
}
// AuthUnregister middleware for unregister request
func AuthUnregister(unregister httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
postData := UnregRequest{}
userOK := false
user, err := getUserFromRequest(r)
if err == nil {
dec := json.NewDecoder(r.Body)
err = dec.Decode(&postData)
if err != nil {
log.WithFields(log.Fields{"error": "json_error", "string": err.Error()}).Error("Decode error")
}
if user.Subdomain == postData.Subdomain {
userOK = true
} else {
log.WithFields(log.Fields{"error": "subdomain_mismatch", "name": postData.Subdomain, "expected": user.Subdomain}).Error("Subdomain mismatch")
}
} else {
log.WithFields(log.Fields{"error": err.Error()}).Error("Error while trying to get user")
}
if userOK {
// Set username info to the decoded UnregRequest object
postData.Username = user.Username
// Set the UnregRequest struct to context to pull in from unregister function
ctx := context.WithValue(r.Context(), ACMETxtKey, postData)
unregister(w, r.WithContext(ctx), p)
} else {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write(jsonError("forbidden"))
}
}
}
func getUserFromRequest(r *http.Request) (ACMETxt, error) {
uname := r.Header.Get("X-Api-User")
passwd := r.Header.Get("X-Api-Key")

73
db.go
View File

@ -209,6 +209,58 @@ func (d *acmedb) Register(afrom cidrslice) (ACMETxt, error) {
return a, err
}
func (d *acmedb) Unregister(username uuid.UUID) error {
var err error
// Check if user exists
user, err := DB.GetByUsername(username)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Error("User doesn't exist")
return errors.New("User doesn't exist")
}
tx, err := d.DB.Begin()
// Delete user's TXT records
err = d.DeleteTXTForDomain(user.Subdomain)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Error("Could not delete TXT records")
return err
}
// Rollback if errored, commit if not
defer func() {
if err != nil {
tx.Rollback()
return
}
tx.Commit()
}()
// Delete user record
d.Lock()
defer d.Unlock()
unregSQL := `
DELETE FROM records
WHERE Username = $1`
if Config.Database.Engine == "sqlite3" {
unregSQL = getSQLiteStmt(unregSQL)
}
sm, err := tx.Prepare(unregSQL)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Error("Database error in prepare")
return errors.New("SQL error")
}
defer sm.Close()
_, err = sm.Exec(username.String())
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Error("Database error in execute")
return errors.New("SQL error")
}
return nil
}
func (d *acmedb) GetByUsername(u uuid.UUID) (ACMETxt, error) {
d.Lock()
defer d.Unlock()
@ -309,6 +361,27 @@ func (d *acmedb) Update(a ACMETxtPost) error {
return nil
}
func (d *acmedb) DeleteTXTForDomain(domain string) error {
d.Lock()
defer d.Unlock()
domain = sanitizeString(domain)
getSQL := `DELETE FROM txt WHERE Subdomain=$1`
if Config.Database.Engine == "sqlite3" {
getSQL = getSQLiteStmt(getSQL)
}
sm, err := d.DB.Prepare(getSQL)
if err != nil {
return err
}
defer sm.Close()
_, err = sm.Exec(domain)
if err != nil {
return err
}
return nil
}
func getModelFromRow(r *sql.Rows) (ACMETxt, error) {
txt := ACMETxt{}
afrom := ""

View File

@ -125,8 +125,9 @@ func startHTTPAPI(errChan chan error, config DNSConfig, dnsservers []*DNSServer)
}
if !Config.API.DisableRegistration {
api.POST("/register", webRegisterPost)
api.POST("/unregister", AuthUnregister(webUnregisterPost))
}
api.POST("/update", Auth(webUpdatePost))
api.POST("/update", AuthUpdate(webUpdatePost))
api.GET("/health", healthCheck)
host := Config.API.IP + ":" + Config.API.Port

View File

@ -72,6 +72,7 @@ type acmedb struct {
type database interface {
Init(string, string) error
Register(cidrslice) (ACMETxt, error)
Unregister(uuid.UUID) error
GetByUsername(uuid.UUID) (ACMETxt, error)
GetTXTForDomain(string) ([]string, error)
Update(ACMETxtPost) error