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:
Tobias Cudnik 2023-05-31 09:41:54 +02:00 committed by GitHub
parent 6f11eb2bb0
commit 723375b334
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 161 additions and 14 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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())

View file

@ -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, " ")

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
}
}

View file

@ -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)

View file

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