Browse Source

NET-152 enrollment keys for non admins (#2346)

* return 401 instead of 403

* fixed http.StatusForbidden

* Tagged build version (temp)

* Unauthorized_Err when applicable

* untagged version

* fixed PUT /api/users/networks/user1

* - expired token redirs to login
- added `/api/enrollment_keys` for non-admins
- unit test for enrollment keys for non-admins

* handle user perms in `/hosts`

* removed debug

* misc

* - support masteradmin
- return hosts with partial access

* added `ismaster` to middleware
Tobias Cudnik 2 years ago
parent
commit
723375b334

+ 1 - 0
cli/functions/http_client.go

@@ -148,6 +148,7 @@ retry:
 	if res.StatusCode == http.StatusUnauthorized && !retried && ctx.MasterKey == "" {
 		req.Header.Set("Authorization", "Bearer "+getAuthToken(ctx, true))
 		retried = true
+		// TODO add a retry limit, drop goto
 		goto retry
 	}
 	resBodyBytes, err := io.ReadAll(res.Body)

+ 19 - 6
controllers/enrollmentkeys.go

@@ -17,7 +17,7 @@ import (
 
 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", logic.SecurityCheck(false, 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)
 }
@@ -34,24 +34,37 @@ func enrollmentKeyHandlers(r *mux.Router) {
 //			Responses:
 //				200: getEnrollmentKeysSlice
 func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
-	currentKeys, err := logic.GetAllEnrollmentKeys()
+	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
 	}
-	for i := range currentKeys {
-		currentKey := currentKeys[i]
-		if err = logic.Tokenize(currentKey, servercfg.GetAPIHost()); err != nil {
+	isMasterAdmin := r.Header.Get("ismaster") == "yes"
+	// regular user flow
+	user, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil && !isMasterAdmin {
+		logger.Log(0, r.Header.Get("user"), "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	// TODO drop double pointer
+	ret := []*models.EnrollmentKey{}
+	for _, key := range keys {
+		if !isMasterAdmin && !logic.UserHasNetworksAccess(key.Networks, user) {
+			continue
+		}
+		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(currentKeys)
+	json.NewEncoder(w).Encode(ret)
 }
 
 // swagger:route DELETE /api/v1/enrollment-keys/{keyID} enrollmentKeys deleteEnrollmentKey

+ 32 - 3
controllers/hosts.go

@@ -19,7 +19,7 @@ import (
 )
 
 func hostHandlers(r *mux.Router) {
-	r.HandleFunc("/api/hosts", logic.SecurityCheck(true, http.HandlerFunc(getHosts))).Methods(http.MethodGet)
+	r.HandleFunc("/api/hosts", logic.SecurityCheck(false, http.HandlerFunc(getHosts))).Methods(http.MethodGet)
 	r.HandleFunc("/api/hosts/keys", logic.SecurityCheck(true, http.HandlerFunc(updateAllKeys))).Methods(http.MethodPut)
 	r.HandleFunc("/api/hosts/{hostid}/keys", logic.SecurityCheck(true, http.HandlerFunc(updateKeys))).Methods(http.MethodPut)
 	r.HandleFunc("/api/hosts/{hostid}", logic.SecurityCheck(true, http.HandlerFunc(updateHost))).Methods(http.MethodPut)
@@ -52,12 +52,41 @@ func getHosts(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	isMasterAdmin := r.Header.Get("ismaster") == "yes"
+	user, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil && !isMasterAdmin {
+		logger.Log(0, r.Header.Get("user"), "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
 	// return JSON/API formatted hosts
+	ret := []models.ApiHost{}
 	apiHosts := logic.GetAllHostsAPI(currentHosts[:])
 	logger.Log(2, r.Header.Get("user"), "fetched all hosts")
-	logic.SortApiHosts(apiHosts[:])
+	for _, host := range apiHosts {
+		nodes := host.Nodes
+		// work on the copy
+		host.Nodes = []string{}
+		for _, nid := range nodes {
+			node, err := logic.GetNodeByID(nid)
+			if err != nil {
+				logger.Log(0, r.Header.Get("user"), "failed to fetch node: ", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				return
+			}
+			if !isMasterAdmin && !logic.UserHasNetworksAccess([]string{node.Network}, user) {
+				continue
+			}
+			host.Nodes = append(host.Nodes, nid)
+		}
+		// add to the response only if has perms to some nodes / networks
+		if len(host.Nodes) > 0 {
+			ret = append(ret, host)
+		}
+	}
+	logic.SortApiHosts(ret[:])
 	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(apiHosts)
+	json.NewEncoder(w).Encode(ret)
 }
 
 // swagger:route GET /api/v1/host pull pullHost

+ 1 - 4
controllers/network.go

@@ -40,10 +40,7 @@ func networkHandlers(r *mux.Router) {
 //			Responses:
 //				200: getNetworksSliceResponse
 func getNetworks(w http.ResponseWriter, r *http.Request) {
-
-	headerNetworks := r.Header.Get("networks")
-	networksSlice := []string{}
-	marshalErr := json.Unmarshal([]byte(headerNetworks), &networksSlice)
+	networksSlice, marshalErr := getHeaderNetworks(r)
 	if marshalErr != nil {
 		logger.Log(0, r.Header.Get("user"), "error unmarshalling networks: ",
 			marshalErr.Error())

+ 1 - 1
controllers/server.go

@@ -56,7 +56,7 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
 func allowUsers(next http.Handler) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var errorResponse = models.ErrorResponse{
-			Code: http.StatusInternalServerError, Message: logic.Forbidden_Msg,
+			Code: http.StatusUnauthorized, Message: logic.Unauthorized_Msg,
 		}
 		bearerToken := r.Header.Get("Authorization")
 		var tokenSplit = strings.Split(bearerToken, " ")

+ 11 - 0
controllers/user.go

@@ -491,3 +491,14 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
 	// Start handling the session
 	go auth.SessionHandler(conn)
 }
+
+// getHeaderNetworks returns a slice of networks parsed form the request header.
+func getHeaderNetworks(r *http.Request) ([]string, error) {
+	headerNetworks := r.Header.Get("networks")
+	networksSlice := []string{}
+	err := json.Unmarshal([]byte(headerNetworks), &networksSlice)
+	if err != nil {
+		return nil, err
+	}
+	return networksSlice, nil
+}

+ 15 - 0
logic/enrollmentkey.go

@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"golang.org/x/exp/slices"
 	"time"
 
 	"github.com/gravitl/netmaker/database"
@@ -68,6 +69,7 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string
 }
 
 // GetAllEnrollmentKeys - fetches all enrollment keys from DB
+// TODO drop double pointer
 func GetAllEnrollmentKeys() ([]*models.EnrollmentKey, error) {
 	currentKeys, err := getEnrollmentKeysMap()
 	if err != nil {
@@ -222,3 +224,16 @@ func getEnrollmentKeysMap() (map[string]*models.EnrollmentKey, error) {
 	}
 	return currentKeys, nil
 }
+
+// UserHasNetworksAccess - checks if a user `u` has access to all `networks`
+func UserHasNetworksAccess(networks []string, u *models.User) bool {
+	if u.IsAdmin {
+		return true
+	}
+	for _, n := range networks {
+		if !slices.Contains(u.Networks, n) {
+			return false
+		}
+	}
+	return true
+}

+ 74 - 0
logic/enrollmentkey_test.go

@@ -204,3 +204,77 @@ func TestDeTokenize_EnrollmentKeys(t *testing.T) {
 
 	removeAllEnrollments()
 }
+
+func TestHasNetworksAccess(t *testing.T) {
+	type Case struct {
+		// network names
+		n []string
+		u models.User
+	}
+	pass := []Case{
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{"n1", "n2"},
+				IsAdmin:  false,
+			},
+		},
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{},
+				IsAdmin:  true,
+			},
+		},
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{"n1", "n2", "n3"},
+				IsAdmin:  false,
+			},
+		},
+		{
+			n: []string{"n2"},
+			u: models.User{
+				Networks: []string{"n2"},
+				IsAdmin:  false,
+			},
+		},
+	}
+	deny := []Case{
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{"n2"},
+				IsAdmin:  false,
+			},
+		},
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{},
+				IsAdmin:  false,
+			},
+		},
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{"n3"},
+				IsAdmin:  false,
+			},
+		},
+		{
+			n: []string{"n2"},
+			u: models.User{
+				Networks: []string{"n1"},
+				IsAdmin:  false,
+			},
+		},
+	}
+	for _, tc := range pass {
+		assert.True(t, UserHasNetworksAccess(tc.n, &tc.u))
+	}
+	for _, tc := range deny {
+		assert.False(t, UserHasNetworksAccess(tc.n, &tc.u))
+	}
+}

+ 6 - 0
logic/security.go

@@ -31,6 +31,7 @@ func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
 		var errorResponse = models.ErrorResponse{
 			Code: http.StatusForbidden, Message: Forbidden_Msg,
 		}
+		r.Header.Set("ismaster", "no")
 
 		var params = mux.Vars(r)
 		bearerToken := r.Header.Get("Authorization")
@@ -53,6 +54,10 @@ func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
 			ReturnErrorResponse(w, r, errorResponse)
 			return
 		}
+		// detect masteradmin
+		if len(networks) > 0 && networks[0] == ALL_NETWORK_ACCESS {
+			r.Header.Set("ismaster", "yes")
+		}
 		networksJson, err := json.Marshal(&networks)
 		if err != nil {
 			ReturnErrorResponse(w, r, errorResponse)
@@ -147,6 +152,7 @@ func UserPermissions(reqAdmin bool, netname string, token string) ([]string, str
 	}
 	//all endpoints here require master so not as complicated
 	if authenticateMaster(authToken) {
+		// TODO log in as an actual admin user
 		return []string{ALL_NETWORK_ACCESS}, master_uname, nil
 	}
 	username, networks, isadmin, err := VerifyUserToken(authToken)

+ 1 - 0
logic/users.go

@@ -12,6 +12,7 @@ import (
 )
 
 // GetUser - gets a user
+// TODO support "masteradmin"
 func GetUser(username string) (*models.User, error) {
 
 	var user models.User