Add SetupKey auto-groups property (#460)

This commit is contained in:
Misha Bragin
2022-09-11 23:16:40 +02:00
committed by GitHub
parent ed1872560f
commit be7d829858
11 changed files with 767 additions and 259 deletions

View File

@@ -134,6 +134,15 @@ components:
state:
description: Setup key status, "valid", "overused","expired" or "revoked"
type: string
auto_groups:
description: Setup key groups to auto-assign to peers registered with this key
type: array
items:
type: string
updated_at:
description: Setup key last update date
type: string
format: date-time
required:
- id
- key
@@ -145,6 +154,8 @@ components:
- used_times
- last_used
- state
- auto_groups
- updated_at
SetupKeyRequest:
type: object
properties:
@@ -160,11 +171,17 @@ components:
revoked:
description: Setup key revocation status
type: boolean
auto_groups:
description: Setup key groups to auto-assign to peers registered with this key
type: array
items:
type: string
required:
- name
- type
- expires_in
- revoked
- auto_groups
GroupMinimum:
type: object
properties:

View File

@@ -299,6 +299,9 @@ type RulePatchOperationPath string
// SetupKey defines model for SetupKey.
type SetupKey struct {
// Setup key groups to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"`
// Setup Key expiration date
Expires time.Time `json:"expires"`
@@ -323,6 +326,9 @@ type SetupKey struct {
// Setup key type, one-off for single time usage and reusable
Type string `json:"type"`
// Setup key last update date
UpdatedAt time.Time `json:"updated_at"`
// Usage count of setup key
UsedTimes int `json:"used_times"`
@@ -332,6 +338,9 @@ type SetupKey struct {
// SetupKeyRequest defines model for SetupKeyRequest.
type SetupKeyRequest struct {
// Setup key groups to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"`
// Expiration time in seconds
ExpiresIn int `json:"expires_in"`

View File

@@ -39,12 +39,11 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
apiHandler.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer).
Methods("GET", "PUT", "DELETE", "OPTIONS")
apiHandler.HandleFunc("/api/users", userHandler.GetUsers).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("GET", "POST", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).Methods("GET", "PUT", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).
Methods("GET", "PUT", "DELETE", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys", keysHandler.GetAllSetupKeysHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys", keysHandler.CreateSetupKeyHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.GetSetupKeyHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.UpdateSetupKeyHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/api/rules", rulesHandler.GetAllRulesHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/rules", rulesHandler.CreateRuleHandler).Methods("POST", "OPTIONS")

View File

@@ -348,6 +348,11 @@ func (h *Routes) DeleteRouteHandler(w http.ResponseWriter, r *http.Request) {
err = h.accountManager.DeleteRoute(account.Id, routeID)
if err != nil {
errStatus, ok := status.FromError(err)
if ok && errStatus.Code() == codes.NotFound {
http.Error(w, fmt.Sprintf("route %s not found under account %s", routeID, account.Id), http.StatusNotFound)
return
}
log.Errorf("failed delete route %s under account %s %v", routeID, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return

View File

@@ -78,7 +78,10 @@ func initRoutesTestData() *Routes {
SaveRouteFunc: func(_ string, _ *route.Route) error {
return nil
},
DeleteRouteFunc: func(_ string, _ string) error {
DeleteRouteFunc: func(_ string, peerIP string) error {
if peerIP != existingRouteID {
return status.Errorf(codes.NotFound, "Peer with ID %s not found", peerIP)
}
return nil
},
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
@@ -155,7 +158,7 @@ func TestRoutesHandlers(t *testing.T) {
{
name: "Get Not Existing Route",
requestType: http.MethodGet,
requestPath: "/api/rules/" + notFoundRouteID,
requestPath: "/api/routes/" + notFoundRouteID,
expectedStatus: http.StatusNotFound,
},
{
@@ -168,7 +171,7 @@ func TestRoutesHandlers(t *testing.T) {
{
name: "Delete Not Existing Route",
requestType: http.MethodDelete,
requestPath: "/api/rules/" + notFoundRouteID,
requestPath: "/api/routes/" + notFoundRouteID,
expectedStatus: http.StatusNotFound,
},
{

View File

@@ -2,6 +2,7 @@ package http
import (
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/http/api"
@@ -28,54 +29,17 @@ func NewSetupKeysHandler(accountManager server.AccountManager, authAudience stri
}
}
func (h *SetupKeys) updateKey(accountId string, keyId string, w http.ResponseWriter, r *http.Request) {
req := &api.PutApiSetupKeysIdJSONRequestBody{}
err := json.NewDecoder(r.Body).Decode(&req)
// CreateSetupKeyHandler is a POST requests that creates a new SetupKey
func (h *SetupKeys) CreateSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
var key *server.SetupKey
if req.Revoked {
//handle only if being revoked, don't allow to enable key again for now
key, err = h.accountManager.RevokeSetupKey(accountId, keyId)
if err != nil {
http.Error(w, "failed revoking key", http.StatusInternalServerError)
return
}
}
if len(req.Name) != 0 {
key, err = h.accountManager.RenameSetupKey(accountId, keyId, req.Name)
if err != nil {
http.Error(w, "failed renaming key", http.StatusInternalServerError)
return
}
}
if key != nil {
writeSuccess(w, key)
}
}
func (h *SetupKeys) getKey(accountId string, keyId string, w http.ResponseWriter, r *http.Request) {
account, err := h.accountManager.GetAccountById(accountId)
if err != nil {
http.Error(w, "account doesn't exist", http.StatusInternalServerError)
return
}
for _, key := range account.SetupKeys {
if key.Id == keyId {
writeSuccess(w, key)
return
}
}
http.Error(w, "setup key not found", http.StatusNotFound)
}
func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.Request) {
req := &api.PostApiSetupKeysJSONRequestBody{}
err := json.NewDecoder(r.Body).Decode(&req)
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -95,7 +59,13 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R
expiresIn := time.Duration(req.ExpiresIn) * time.Second
setupKey, err := h.accountManager.AddSetupKey(accountId, req.Name, server.SetupKeyType(req.Type), expiresIn)
if req.AutoGroups == nil {
req.AutoGroups = []string{}
}
// newExpiresIn := time.Duration(req.ExpiresIn) * time.Second
// newKey.ExpiresAt = time.Now().Add(newExpiresIn)
setupKey, err := h.accountManager.CreateSetupKey(account.Id, req.Name, server.SetupKeyType(req.Type), expiresIn,
req.AutoGroups)
if err != nil {
errStatus, ok := status.FromError(err)
if ok && errStatus.Code() == codes.NotFound {
@@ -109,7 +79,8 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R
writeSuccess(w, setupKey)
}
func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
// GetSetupKeyHandler is a GET request to get a SetupKey by ID
func (h *SetupKeys) GetSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
@@ -118,25 +89,84 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
}
vars := mux.Vars(r)
keyId := vars["id"]
if len(keyId) == 0 {
keyID := vars["id"]
if len(keyID) == 0 {
http.Error(w, "invalid key Id", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodPut:
h.updateKey(account.Id, keyId, w, r)
key, err := h.accountManager.GetSetupKey(account.Id, keyID)
if err != nil {
errStatus, ok := status.FromError(err)
if ok && errStatus.Code() == codes.NotFound {
http.Error(w, fmt.Sprintf("setup key %s not found under account %s", keyID, account.Id), http.StatusNotFound)
return
}
log.Errorf("failed getting setup key %s under account %s %v", keyID, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
case http.MethodGet:
h.getKey(account.Id, keyId, w, r)
return
default:
http.Error(w, "", http.StatusNotFound)
}
writeSuccess(w, key)
}
func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) {
// UpdateSetupKeyHandler is a PUT request to update server.SetupKey
func (h *SetupKeys) UpdateSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
vars := mux.Vars(r)
keyID := vars["id"]
if len(keyID) == 0 {
http.Error(w, "invalid key Id", http.StatusBadRequest)
return
}
req := &api.PutApiSetupKeysIdJSONRequestBody{}
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Name == "" {
http.Error(w, fmt.Sprintf("setup key name field is invalid: %s", req.Name), http.StatusBadRequest)
return
}
if req.AutoGroups == nil {
http.Error(w, fmt.Sprintf("setup key AutoGroups field is invalid: %s", req.AutoGroups), http.StatusBadRequest)
return
}
newKey := &server.SetupKey{}
newKey.AutoGroups = req.AutoGroups
newKey.Revoked = req.Revoked
newKey.Name = req.Name
newKey.Id = keyID
newKey, err = h.accountManager.SaveSetupKey(account.Id, newKey)
if err != nil {
if e, ok := status.FromError(err); ok {
switch e.Code() {
case codes.NotFound:
http.Error(w, fmt.Sprintf("couldn't find setup key for ID %s", keyID), http.StatusNotFound)
default:
http.Error(w, "failed updating setup key", http.StatusInternalServerError)
}
}
return
}
writeSuccess(w, newKey)
}
// GetAllSetupKeysHandler is a GET request that returns a list of SetupKey
func (h *SetupKeys) GetAllSetupKeysHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
@@ -145,28 +175,18 @@ func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) {
return
}
switch r.Method {
case http.MethodPost:
h.createKey(account.Id, w, r)
setupKeys, err := h.accountManager.ListSetupKeys(account.Id)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
case http.MethodGet:
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
respBody := []*api.SetupKey{}
for _, key := range account.SetupKeys {
respBody = append(respBody, toResponseBody(key))
}
err = json.NewEncoder(w).Encode(respBody)
if err != nil {
log.Errorf("failed encoding account peers %s: %v", account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
default:
http.Error(w, "", http.StatusNotFound)
}
apiSetupKeys := make([]*api.SetupKey, 0)
for _, key := range setupKeys {
apiSetupKeys = append(apiSetupKeys, toResponseBody(key))
}
writeJSONObject(w, apiSetupKeys)
}
func writeSuccess(w http.ResponseWriter, key *server.SetupKey) {
@@ -190,16 +210,19 @@ func toResponseBody(key *server.SetupKey) *api.SetupKey {
} else {
state = "valid"
}
return &api.SetupKey{
Id: key.Id,
Key: key.Key,
Name: key.Name,
Expires: key.ExpiresAt,
Type: string(key.Type),
Valid: key.IsValid(),
Revoked: key.Revoked,
UsedTimes: key.UsedTimes,
LastUsed: key.LastUsed,
State: state,
Id: key.Id,
Key: key.Key,
Name: key.Name,
Expires: key.ExpiresAt,
Type: string(key.Type),
Valid: key.IsValid(),
Revoked: key.Revoked,
UsedTimes: key.UsedTimes,
LastUsed: key.LastUsed,
State: state,
AutoGroups: key.AutoGroups,
UpdatedAt: key.UpdatedAt,
}
}

View File

@@ -0,0 +1,222 @@
package http
import (
"bytes"
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/mock_server"
)
const (
existingSetupKeyID = "existingSetupKeyID"
newSetupKeyName = "New Setup Key"
updatedSetupKeyName = "KKKey"
notFoundSetupKeyID = "notFoundSetupKeyID"
)
func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.SetupKey, updatedSetupKey *server.SetupKey) *SetupKeys {
return &SetupKeys{
accountManager: &mock_server.MockAccountManager{
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
return &server.Account{
Id: testAccountID,
Domain: "hotmail.com",
SetupKeys: map[string]*server.SetupKey{
defaultKey.Key: defaultKey,
},
Groups: map[string]*server.Group{
"group-1": {ID: "group-1", Peers: []string{"A", "B"}},
"id-all": {ID: "id-all", Name: "All"}},
}, nil
},
CreateSetupKeyFunc: func(_ string, keyName string, typ server.SetupKeyType, _ time.Duration, _ []string) (*server.SetupKey, error) {
if keyName == newKey.Name || typ != newKey.Type {
return newKey, nil
}
return nil, fmt.Errorf("failed creating setup key")
},
GetSetupKeyFunc: func(accountID string, keyID string) (*server.SetupKey, error) {
switch keyID {
case defaultKey.Id:
return defaultKey, nil
case newKey.Id:
return newKey, nil
default:
return nil, status.Errorf(codes.NotFound, "key %s not found", keyID)
}
},
SaveSetupKeyFunc: func(accountID string, key *server.SetupKey) (*server.SetupKey, error) {
if key.Id == updatedSetupKey.Id {
return updatedSetupKey, nil
}
return nil, status.Errorf(codes.NotFound, "key %s not found", key.Id)
},
ListSetupKeysFunc: func(accountID string) ([]*server.SetupKey, error) {
return []*server.SetupKey{defaultKey}, nil
},
},
authAudience: "",
jwtExtractor: jwtclaims.ClaimsExtractor{
ExtractClaimsFromRequestContext: func(r *http.Request, authAudience string) jwtclaims.AuthorizationClaims {
return jwtclaims.AuthorizationClaims{
UserId: "test_user",
Domain: "hotmail.com",
AccountId: testAccountID,
}
},
},
}
}
func TestSetupKeysHandlers(t *testing.T) {
defaultSetupKey := server.GenerateDefaultSetupKey()
defaultSetupKey.Id = existingSetupKeyID
newSetupKey := server.GenerateSetupKey(newSetupKeyName, server.SetupKeyReusable, 0, []string{"group-1"})
updatedDefaultSetupKey := defaultSetupKey.Copy()
updatedDefaultSetupKey.AutoGroups = []string{"group-1"}
updatedDefaultSetupKey.Name = updatedSetupKeyName
updatedDefaultSetupKey.Revoked = true
tt := []struct {
name string
requestType string
requestPath string
requestBody io.Reader
expectedStatus int
expectedBody bool
expectedSetupKey *api.SetupKey
expectedSetupKeys []*api.SetupKey
}{
{
name: "Get Setup Keys",
requestType: http.MethodGet,
requestPath: "/api/setup-keys",
expectedStatus: http.StatusOK,
expectedBody: true,
expectedSetupKeys: []*api.SetupKey{toResponseBody(defaultSetupKey)},
},
{
name: "Get Existing Setup Key",
requestType: http.MethodGet,
requestPath: "/api/setup-keys/" + existingSetupKeyID,
expectedStatus: http.StatusOK,
expectedBody: true,
expectedSetupKey: toResponseBody(defaultSetupKey),
},
{
name: "Get Not Existing Setup Key",
requestType: http.MethodGet,
requestPath: "/api/setup-keys/" + notFoundSetupKeyID,
expectedStatus: http.StatusNotFound,
expectedBody: false,
},
{
name: "Create Setup Key",
requestType: http.MethodPost,
requestPath: "/api/setup-keys",
requestBody: bytes.NewBuffer(
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"%s\"}", newSetupKey.Name, newSetupKey.Type))),
expectedStatus: http.StatusOK,
expectedBody: true,
expectedSetupKey: toResponseBody(newSetupKey),
},
{
name: "Update Setup Key",
requestType: http.MethodPut,
requestPath: "/api/setup-keys/" + defaultSetupKey.Id,
requestBody: bytes.NewBuffer(
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"auto_groups\":[\"%s\"], \"revoked\":%v}",
updatedDefaultSetupKey.Type,
updatedDefaultSetupKey.AutoGroups[0],
updatedDefaultSetupKey.Revoked,
))),
expectedStatus: http.StatusOK,
expectedBody: true,
expectedSetupKey: toResponseBody(updatedDefaultSetupKey),
},
}
handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey)
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
router := mux.NewRouter()
router.HandleFunc("/api/setup-keys", handler.GetAllSetupKeysHandler).Methods("GET", "OPTIONS")
router.HandleFunc("/api/setup-keys", handler.CreateSetupKeyHandler).Methods("POST", "OPTIONS")
router.HandleFunc("/api/setup-keys/{id}", handler.GetSetupKeyHandler).Methods("GET", "OPTIONS")
router.HandleFunc("/api/setup-keys/{id}", handler.UpdateSetupKeyHandler).Methods("PUT", "OPTIONS")
router.ServeHTTP(recorder, req)
res := recorder.Result()
defer res.Body.Close()
content, err := io.ReadAll(res.Body)
if err != nil {
t.Fatalf("I don't know what I expected; %v", err)
}
if status := recorder.Code; status != tc.expectedStatus {
t.Errorf("handler returned wrong status code: got %v want %v, content: %s",
status, tc.expectedStatus, string(content))
return
}
if !tc.expectedBody {
return
}
if tc.expectedSetupKey != nil {
got := &api.SetupKey{}
if err = json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assertKeys(t, got, tc.expectedSetupKey)
return
}
if len(tc.expectedSetupKeys) > 0 {
var got []*api.SetupKey
if err = json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assertKeys(t, got[0], tc.expectedSetupKeys[0])
return
}
})
}
}
func assertKeys(t *testing.T, got *api.SetupKey, expected *api.SetupKey) {
// this comparison is done manually because when converting to JSON dates formatted differently
// assert.Equal(t, got.UpdatedAt, tc.expectedSetupKey.UpdatedAt) //doesn't work
assert.WithinDurationf(t, got.UpdatedAt, expected.UpdatedAt, 0, "")
assert.WithinDurationf(t, got.Expires, expected.Expires, 0, "")
assert.Equal(t, got.Name, expected.Name)
assert.Equal(t, got.Id, expected.Id)
assert.Equal(t, got.Key, expected.Key)
assert.Equal(t, got.Type, expected.Type)
assert.Equal(t, got.UsedTimes, expected.UsedTimes)
assert.Equal(t, got.Revoked, expected.Revoked)
assert.ElementsMatch(t, got.AutoGroups, expected.AutoGroups)
}