mirror of
https://github.com/gravitl/netmaker.git
synced 2025-10-18 22:55:15 +08:00
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
This commit is contained in:
@@ -148,6 +148,7 @@ retry:
|
|||||||
if res.StatusCode == http.StatusUnauthorized && !retried && ctx.MasterKey == "" {
|
if res.StatusCode == http.StatusUnauthorized && !retried && ctx.MasterKey == "" {
|
||||||
req.Header.Set("Authorization", "Bearer "+getAuthToken(ctx, true))
|
req.Header.Set("Authorization", "Bearer "+getAuthToken(ctx, true))
|
||||||
retried = true
|
retried = true
|
||||||
|
// TODO add a retry limit, drop goto
|
||||||
goto retry
|
goto retry
|
||||||
}
|
}
|
||||||
resBodyBytes, err := io.ReadAll(res.Body)
|
resBodyBytes, err := io.ReadAll(res.Body)
|
||||||
|
@@ -17,7 +17,7 @@ import (
|
|||||||
|
|
||||||
func enrollmentKeyHandlers(r *mux.Router) {
|
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(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/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/host/register/{token}", http.HandlerFunc(handleHostRegister)).Methods(http.MethodPost)
|
||||||
}
|
}
|
||||||
@@ -34,24 +34,37 @@ func enrollmentKeyHandlers(r *mux.Router) {
|
|||||||
// Responses:
|
// Responses:
|
||||||
// 200: getEnrollmentKeysSlice
|
// 200: getEnrollmentKeysSlice
|
||||||
func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
|
func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
currentKeys, err := logic.GetAllEnrollmentKeys()
|
keys, err := logic.GetAllEnrollmentKeys()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Log(0, r.Header.Get("user"), "failed to fetch enrollment keys: ", err.Error())
|
logger.Log(0, r.Header.Get("user"), "failed to fetch enrollment keys: ", err.Error())
|
||||||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i := range currentKeys {
|
isMasterAdmin := r.Header.Get("ismaster") == "yes"
|
||||||
currentKey := currentKeys[i]
|
// regular user flow
|
||||||
if err = logic.Tokenize(currentKey, servercfg.GetAPIHost()); err != nil {
|
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())
|
logger.Log(0, r.Header.Get("user"), "failed to get token values for keys:", err.Error())
|
||||||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ret = append(ret, key)
|
||||||
}
|
}
|
||||||
// return JSON/API formatted keys
|
// return JSON/API formatted keys
|
||||||
logger.Log(2, r.Header.Get("user"), "fetched enrollment keys")
|
logger.Log(2, r.Header.Get("user"), "fetched enrollment keys")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(currentKeys)
|
json.NewEncoder(w).Encode(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
// swagger:route DELETE /api/v1/enrollment-keys/{keyID} enrollmentKeys deleteEnrollmentKey
|
// swagger:route DELETE /api/v1/enrollment-keys/{keyID} enrollmentKeys deleteEnrollmentKey
|
||||||
|
@@ -19,7 +19,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func hostHandlers(r *mux.Router) {
|
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/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}/keys", logic.SecurityCheck(true, http.HandlerFunc(updateKeys))).Methods(http.MethodPut)
|
||||||
r.HandleFunc("/api/hosts/{hostid}", logic.SecurityCheck(true, http.HandlerFunc(updateHost))).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"))
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||||||
return
|
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
|
// return JSON/API formatted hosts
|
||||||
|
ret := []models.ApiHost{}
|
||||||
apiHosts := logic.GetAllHostsAPI(currentHosts[:])
|
apiHosts := logic.GetAllHostsAPI(currentHosts[:])
|
||||||
logger.Log(2, r.Header.Get("user"), "fetched all hosts")
|
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)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(apiHosts)
|
json.NewEncoder(w).Encode(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
// swagger:route GET /api/v1/host pull pullHost
|
// swagger:route GET /api/v1/host pull pullHost
|
||||||
|
@@ -40,10 +40,7 @@ func networkHandlers(r *mux.Router) {
|
|||||||
// Responses:
|
// Responses:
|
||||||
// 200: getNetworksSliceResponse
|
// 200: getNetworksSliceResponse
|
||||||
func getNetworks(w http.ResponseWriter, r *http.Request) {
|
func getNetworks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
networksSlice, marshalErr := getHeaderNetworks(r)
|
||||||
headerNetworks := r.Header.Get("networks")
|
|
||||||
networksSlice := []string{}
|
|
||||||
marshalErr := json.Unmarshal([]byte(headerNetworks), &networksSlice)
|
|
||||||
if marshalErr != nil {
|
if marshalErr != nil {
|
||||||
logger.Log(0, r.Header.Get("user"), "error unmarshalling networks: ",
|
logger.Log(0, r.Header.Get("user"), "error unmarshalling networks: ",
|
||||||
marshalErr.Error())
|
marshalErr.Error())
|
||||||
|
@@ -56,7 +56,7 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
func allowUsers(next http.Handler) http.HandlerFunc {
|
func allowUsers(next http.Handler) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var errorResponse = models.ErrorResponse{
|
var errorResponse = models.ErrorResponse{
|
||||||
Code: http.StatusInternalServerError, Message: logic.Forbidden_Msg,
|
Code: http.StatusUnauthorized, Message: logic.Unauthorized_Msg,
|
||||||
}
|
}
|
||||||
bearerToken := r.Header.Get("Authorization")
|
bearerToken := r.Header.Get("Authorization")
|
||||||
var tokenSplit = strings.Split(bearerToken, " ")
|
var tokenSplit = strings.Split(bearerToken, " ")
|
||||||
|
@@ -491,3 +491,14 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Start handling the session
|
// Start handling the session
|
||||||
go auth.SessionHandler(conn)
|
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
|
||||||
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gravitl/netmaker/database"
|
"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
|
// GetAllEnrollmentKeys - fetches all enrollment keys from DB
|
||||||
|
// TODO drop double pointer
|
||||||
func GetAllEnrollmentKeys() ([]*models.EnrollmentKey, error) {
|
func GetAllEnrollmentKeys() ([]*models.EnrollmentKey, error) {
|
||||||
currentKeys, err := getEnrollmentKeysMap()
|
currentKeys, err := getEnrollmentKeysMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -222,3 +224,16 @@ func getEnrollmentKeysMap() (map[string]*models.EnrollmentKey, error) {
|
|||||||
}
|
}
|
||||||
return currentKeys, nil
|
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
|
||||||
|
}
|
||||||
|
@@ -204,3 +204,77 @@ func TestDeTokenize_EnrollmentKeys(t *testing.T) {
|
|||||||
|
|
||||||
removeAllEnrollments()
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -31,6 +31,7 @@ func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
|
|||||||
var errorResponse = models.ErrorResponse{
|
var errorResponse = models.ErrorResponse{
|
||||||
Code: http.StatusForbidden, Message: Forbidden_Msg,
|
Code: http.StatusForbidden, Message: Forbidden_Msg,
|
||||||
}
|
}
|
||||||
|
r.Header.Set("ismaster", "no")
|
||||||
|
|
||||||
var params = mux.Vars(r)
|
var params = mux.Vars(r)
|
||||||
bearerToken := r.Header.Get("Authorization")
|
bearerToken := r.Header.Get("Authorization")
|
||||||
@@ -53,6 +54,10 @@ func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
|
|||||||
ReturnErrorResponse(w, r, errorResponse)
|
ReturnErrorResponse(w, r, errorResponse)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// detect masteradmin
|
||||||
|
if len(networks) > 0 && networks[0] == ALL_NETWORK_ACCESS {
|
||||||
|
r.Header.Set("ismaster", "yes")
|
||||||
|
}
|
||||||
networksJson, err := json.Marshal(&networks)
|
networksJson, err := json.Marshal(&networks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ReturnErrorResponse(w, r, errorResponse)
|
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
|
//all endpoints here require master so not as complicated
|
||||||
if authenticateMaster(authToken) {
|
if authenticateMaster(authToken) {
|
||||||
|
// TODO log in as an actual admin user
|
||||||
return []string{ALL_NETWORK_ACCESS}, master_uname, nil
|
return []string{ALL_NETWORK_ACCESS}, master_uname, nil
|
||||||
}
|
}
|
||||||
username, networks, isadmin, err := VerifyUserToken(authToken)
|
username, networks, isadmin, err := VerifyUserToken(authToken)
|
||||||
|
@@ -12,6 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetUser - gets a user
|
// GetUser - gets a user
|
||||||
|
// TODO support "masteradmin"
|
||||||
func GetUser(username string) (*models.User, error) {
|
func GetUser(username string) (*models.User, error) {
|
||||||
|
|
||||||
var user models.User
|
var user models.User
|
||||||
|
Reference in New Issue
Block a user