mirror of
				https://github.com/gravitl/netmaker.git
				synced 2025-10-31 16:43:07 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			362 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			362 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package controller
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/go-playground/validator/v10"
 | |
| 	"github.com/google/uuid"
 | |
| 	"github.com/gorilla/mux"
 | |
| 
 | |
| 	"github.com/gravitl/netmaker/auth"
 | |
| 	"github.com/gravitl/netmaker/logger"
 | |
| 	"github.com/gravitl/netmaker/logic"
 | |
| 	"github.com/gravitl/netmaker/models"
 | |
| 	"github.com/gravitl/netmaker/mq"
 | |
| 	"github.com/gravitl/netmaker/servercfg"
 | |
| 	"golang.org/x/exp/slog"
 | |
| )
 | |
| 
 | |
| func enrollmentKeyHandlers(r *mux.Router) {
 | |
| 	r.HandleFunc("/api/v1/enrollment-keys", logic.SecurityCheck(true, http.HandlerFunc(createEnrollmentKey))).
 | |
| 		Methods(http.MethodPost)
 | |
| 	r.HandleFunc("/api/v1/enrollment-keys", logic.SecurityCheck(true, http.HandlerFunc(getEnrollmentKeys))).
 | |
| 		Methods(http.MethodGet)
 | |
| 	r.HandleFunc("/api/v1/enrollment-keys/{keyID}", logic.SecurityCheck(true, http.HandlerFunc(deleteEnrollmentKey))).
 | |
| 		Methods(http.MethodDelete)
 | |
| 	r.HandleFunc("/api/v1/host/register/{token}", http.HandlerFunc(handleHostRegister)).
 | |
| 		Methods(http.MethodPost)
 | |
| 	r.HandleFunc("/api/v1/enrollment-keys/{keyID}", logic.SecurityCheck(true, http.HandlerFunc(updateEnrollmentKey))).
 | |
| 		Methods(http.MethodPut)
 | |
| }
 | |
| 
 | |
| // @Summary     Lists all EnrollmentKeys for admins
 | |
| // @Router      /api/v1/enrollment-keys [get]
 | |
| // @Tags        EnrollmentKeys
 | |
| // @Security    oauth
 | |
| // @Success     200 {array} models.EnrollmentKey
 | |
| // @Failure     500 {object} models.ErrorResponse
 | |
| func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
 | |
| 	keys, err := logic.GetAllEnrollmentKeys()
 | |
| 	if err != nil {
 | |
| 		logger.Log(0, r.Header.Get("user"), "failed to fetch enrollment keys: ", err.Error())
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	ret := []*models.EnrollmentKey{}
 | |
| 	for _, key := range keys {
 | |
| 		key := key
 | |
| 		if err = logic.Tokenize(&key, servercfg.GetAPIHost()); err != nil {
 | |
| 			logger.Log(0, r.Header.Get("user"), "failed to get token values for keys:", err.Error())
 | |
| 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 			return
 | |
| 		}
 | |
| 		ret = append(ret, &key)
 | |
| 	}
 | |
| 	// return JSON/API formatted keys
 | |
| 	logger.Log(2, r.Header.Get("user"), "fetched enrollment keys")
 | |
| 	w.WriteHeader(http.StatusOK)
 | |
| 	json.NewEncoder(w).Encode(ret)
 | |
| }
 | |
| 
 | |
| // @Summary     Deletes an EnrollmentKey from Netmaker server
 | |
| // @Router      /api/v1/enrollment-keys/{keyid} [delete]
 | |
| // @Tags        EnrollmentKeys
 | |
| // @Security    oauth
 | |
| // @Param       keyid path string true "Enrollment Key ID"
 | |
| // @Success     200
 | |
| // @Failure     500 {object} models.ErrorResponse
 | |
| func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 | |
| 	params := mux.Vars(r)
 | |
| 	keyID := params["keyID"]
 | |
| 	err := logic.DeleteEnrollmentKey(keyID, false)
 | |
| 	if err != nil {
 | |
| 		logger.Log(0, r.Header.Get("user"), "failed to remove enrollment key: ", err.Error())
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 		return
 | |
| 	}
 | |
| 	logger.Log(2, r.Header.Get("user"), "deleted enrollment key", keyID)
 | |
| 	w.WriteHeader(http.StatusOK)
 | |
| }
 | |
| 
 | |
| // @Summary     Creates an EnrollmentKey for hosts to register with server and join networks
 | |
| // @Router      /api/v1/enrollment-keys [post]
 | |
| // @Tags        EnrollmentKeys
 | |
| // @Security    oauth
 | |
| // @Param       body body models.APIEnrollmentKey true "Enrollment Key parameters"
 | |
| // @Success     200 {object} models.EnrollmentKey
 | |
| // @Failure     400 {object} models.ErrorResponse
 | |
| // @Failure     500 {object} models.ErrorResponse
 | |
| func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 | |
| 	var enrollmentKeyBody models.APIEnrollmentKey
 | |
| 
 | |
| 	err := json.NewDecoder(r.Body).Decode(&enrollmentKeyBody)
 | |
| 	if err != nil {
 | |
| 		logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
 | |
| 			err.Error())
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 | |
| 		return
 | |
| 	}
 | |
| 	var newTime time.Time
 | |
| 	if enrollmentKeyBody.Expiration > 0 {
 | |
| 		newTime = time.Unix(enrollmentKeyBody.Expiration, 0)
 | |
| 	}
 | |
| 	v := validator.New()
 | |
| 	err = v.Struct(enrollmentKeyBody)
 | |
| 	if err != nil {
 | |
| 		logger.Log(0, r.Header.Get("user"), "error validating request body: ",
 | |
| 			err.Error())
 | |
| 		logic.ReturnErrorResponse(
 | |
| 			w,
 | |
| 			r,
 | |
| 			logic.FormatError(
 | |
| 				fmt.Errorf("validation error: name length must be between 3 and 32: %w", err),
 | |
| 				"badrequest",
 | |
| 			),
 | |
| 		)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if existingKeys, err := logic.GetAllEnrollmentKeys(); err != nil {
 | |
| 		logger.Log(0, r.Header.Get("user"), "error validating request body: ",
 | |
| 			err.Error())
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 		return
 | |
| 	} else {
 | |
| 		// check if any tags are duplicate
 | |
| 		existingTags := make(map[string]struct{})
 | |
| 		for _, existingKey := range existingKeys {
 | |
| 			for _, t := range existingKey.Tags {
 | |
| 				existingTags[t] = struct{}{}
 | |
| 			}
 | |
| 		}
 | |
| 		for _, t := range enrollmentKeyBody.Tags {
 | |
| 			if _, ok := existingTags[t]; ok {
 | |
| 				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("key names must be unique"), "badrequest"))
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	relayId := uuid.Nil
 | |
| 	if enrollmentKeyBody.Relay != "" {
 | |
| 		relayId, err = uuid.Parse(enrollmentKeyBody.Relay)
 | |
| 		if err != nil {
 | |
| 			logger.Log(0, r.Header.Get("user"), "error parsing relay id: ", err.Error())
 | |
| 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	newEnrollmentKey, err := logic.CreateEnrollmentKey(
 | |
| 		enrollmentKeyBody.UsesRemaining,
 | |
| 		newTime,
 | |
| 		enrollmentKeyBody.Networks,
 | |
| 		enrollmentKeyBody.Tags,
 | |
| 		enrollmentKeyBody.Groups,
 | |
| 		enrollmentKeyBody.Unlimited,
 | |
| 		relayId,
 | |
| 		false,
 | |
| 	)
 | |
| 	if err != nil {
 | |
| 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err = logic.Tokenize(newEnrollmentKey, servercfg.GetAPIHost()); err != nil {
 | |
| 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 		return
 | |
| 	}
 | |
| 	logger.Log(2, r.Header.Get("user"), "created enrollment key")
 | |
| 	w.WriteHeader(http.StatusOK)
 | |
| 	json.NewEncoder(w).Encode(newEnrollmentKey)
 | |
| }
 | |
| 
 | |
| // @Summary     Updates an EnrollmentKey. Updates are only limited to the relay to use
 | |
| // @Router      /api/v1/enrollment-keys/{keyid} [put]
 | |
| // @Tags        EnrollmentKeys
 | |
| // @Security    oauth
 | |
| // @Param       keyid path string true "Enrollment Key ID"
 | |
| // @Param       body body models.APIEnrollmentKey true "Enrollment Key parameters"
 | |
| // @Success     200 {object} models.EnrollmentKey
 | |
| // @Failure     400 {object} models.ErrorResponse
 | |
| // @Failure     500 {object} models.ErrorResponse
 | |
| func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 | |
| 	var enrollmentKeyBody models.APIEnrollmentKey
 | |
| 	params := mux.Vars(r)
 | |
| 	keyId := params["keyID"]
 | |
| 
 | |
| 	err := json.NewDecoder(r.Body).Decode(&enrollmentKeyBody)
 | |
| 	if err != nil {
 | |
| 		slog.Error("error decoding request body", "error", err)
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	relayId := uuid.Nil
 | |
| 	if enrollmentKeyBody.Relay != "" {
 | |
| 		relayId, err = uuid.Parse(enrollmentKeyBody.Relay)
 | |
| 		if err != nil {
 | |
| 			slog.Error("error parsing relay id", "error", err)
 | |
| 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups)
 | |
| 	if err != nil {
 | |
| 		slog.Error("failed to update enrollment key", "error", err)
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err = logic.Tokenize(newEnrollmentKey, servercfg.GetAPIHost()); err != nil {
 | |
| 		slog.Error("failed to update enrollment key", "error", err)
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	slog.Info("updated enrollment key", "id", keyId)
 | |
| 	w.WriteHeader(http.StatusOK)
 | |
| 	json.NewEncoder(w).Encode(newEnrollmentKey)
 | |
| }
 | |
| 
 | |
| // @Summary     Handles a Netclient registration with server and add nodes accordingly
 | |
| // @Router      /api/v1/host/register/{token} [post]
 | |
| // @Tags        EnrollmentKeys
 | |
| // @Security    oauth
 | |
| // @Param       token path string true "Enrollment Key Token"
 | |
| // @Param       body body models.Host true "Host registration parameters"
 | |
| // @Success     200 {object} models.RegisterResponse
 | |
| // @Failure     400 {object} models.ErrorResponse
 | |
| // @Failure     500 {object} models.ErrorResponse
 | |
| func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 | |
| 	params := mux.Vars(r)
 | |
| 	token := params["token"]
 | |
| 	logger.Log(0, "received registration attempt with token", token)
 | |
| 	// check if token exists
 | |
| 	enrollmentKey, err := logic.DeTokenize(token)
 | |
| 	if err != nil {
 | |
| 		logger.Log(0, "invalid enrollment key used", token, err.Error())
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 | |
| 		return
 | |
| 	}
 | |
| 	// get the host
 | |
| 	var newHost models.Host
 | |
| 	if err = json.NewDecoder(r.Body).Decode(&newHost); err != nil {
 | |
| 		logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
 | |
| 			err.Error())
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 		return
 | |
| 	}
 | |
| 	// check if host already exists
 | |
| 	hostExists := false
 | |
| 	if hostExists = logic.HostExists(&newHost); hostExists && len(enrollmentKey.Networks) == 0 {
 | |
| 		logger.Log(
 | |
| 			0,
 | |
| 			"host",
 | |
| 			newHost.ID.String(),
 | |
| 			newHost.Name,
 | |
| 			"attempted to re-register with no networks",
 | |
| 		)
 | |
| 		logic.ReturnErrorResponse(
 | |
| 			w,
 | |
| 			r,
 | |
| 			logic.FormatError(fmt.Errorf("host already exists"), "badrequest"),
 | |
| 		)
 | |
| 		return
 | |
| 	}
 | |
| 	// version check
 | |
| 	if !logic.IsVersionCompatible(newHost.Version) {
 | |
| 		err := fmt.Errorf("bad client version on register: %s", newHost.Version)
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 | |
| 		return
 | |
| 	}
 | |
| 	if newHost.TrafficKeyPublic == nil && newHost.OS != models.OS_Types.IoT {
 | |
| 		err := fmt.Errorf("missing traffic key")
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 | |
| 		return
 | |
| 	}
 | |
| 	key, keyErr := logic.RetrievePublicTrafficKey()
 | |
| 	if keyErr != nil {
 | |
| 		logger.Log(0, "error retrieving key:", keyErr.Error())
 | |
| 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 		return
 | |
| 	}
 | |
| 	// use the token
 | |
| 	if ok := logic.TryToUseEnrollmentKey(enrollmentKey); !ok {
 | |
| 		logger.Log(0, "host", newHost.ID.String(), newHost.Name, "failed registration")
 | |
| 		logic.ReturnErrorResponse(
 | |
| 			w,
 | |
| 			r,
 | |
| 			logic.FormatError(fmt.Errorf("invalid enrollment key"), "badrequest"),
 | |
| 		)
 | |
| 		return
 | |
| 	}
 | |
| 	if !hostExists {
 | |
| 		newHost.PersistentKeepalive = models.DefaultPersistentKeepAlive
 | |
| 		// register host
 | |
| 		//logic.CheckHostPorts(&newHost)
 | |
| 		// create EMQX credentials and ACLs for host
 | |
| 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 | |
| 			if err := mq.GetEmqxHandler().CreateEmqxUser(newHost.ID.String(), newHost.HostPass); err != nil {
 | |
| 				logger.Log(0, "failed to create host credentials for EMQX: ", err.Error())
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if err = logic.CreateHost(&newHost); err != nil {
 | |
| 			logger.Log(
 | |
| 				0,
 | |
| 				"host",
 | |
| 				newHost.ID.String(),
 | |
| 				newHost.Name,
 | |
| 				"failed registration -",
 | |
| 				err.Error(),
 | |
| 			)
 | |
| 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 			return
 | |
| 		}
 | |
| 	} else {
 | |
| 		// need to revise the list of networks from key
 | |
| 		// based on the ones host currently has
 | |
| 		networksToAdd := []string{}
 | |
| 		currentNets := logic.GetHostNetworks(newHost.ID.String())
 | |
| 		for _, newNet := range enrollmentKey.Networks {
 | |
| 			if !logic.StringSliceContains(currentNets, newNet) {
 | |
| 				networksToAdd = append(networksToAdd, newNet)
 | |
| 			}
 | |
| 		}
 | |
| 		enrollmentKey.Networks = networksToAdd
 | |
| 		currHost, err := logic.GetHost(newHost.ID.String())
 | |
| 		if err != nil {
 | |
| 			slog.Error("failed registration", "hostID", newHost.ID.String(), "hostName", newHost.Name, "error", err.Error())
 | |
| 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 			return
 | |
| 		}
 | |
| 		logic.UpdateHostFromClient(&newHost, currHost)
 | |
| 		err = logic.UpsertHost(currHost)
 | |
| 		if err != nil {
 | |
| 			slog.Error("failed to update host", "id", currHost.ID, "error", err)
 | |
| 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 	// ready the response
 | |
| 	server := servercfg.GetServerInfo()
 | |
| 	server.TrafficKey = key
 | |
| 	response := models.RegisterResponse{
 | |
| 		ServerConf:    server,
 | |
| 		RequestedHost: newHost,
 | |
| 	}
 | |
| 	logger.Log(0, newHost.Name, newHost.ID.String(), "registered with Netmaker")
 | |
| 	w.WriteHeader(http.StatusOK)
 | |
| 	json.NewEncoder(w).Encode(&response)
 | |
| 	// notify host of changes, peer and node updates
 | |
| 	go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost, enrollmentKey.Relay, enrollmentKey.Groups)
 | |
| }
 |