mirror of
https://github.com/gravitl/netmaker.git
synced 2025-09-04 04:04:17 +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:
parent
6f11eb2bb0
commit
723375b334
10 changed files with 161 additions and 14 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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, " ")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
)
|
||||
|
||||
// GetUser - gets a user
|
||||
// TODO support "masteradmin"
|
||||
func GetUser(username string) (*models.User, error) {
|
||||
|
||||
var user models.User
|
||||
|
|
Loading…
Add table
Reference in a new issue