From be7d829858566c9d2a251c2c9b7209c486b32903 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Sun, 11 Sep 2022 23:16:40 +0200 Subject: [PATCH] Add SetupKey auto-groups property (#460) --- management/server/account.go | 107 +-------- management/server/http/api/openapi.yml | 17 ++ management/server/http/api/types.gen.go | 9 + management/server/http/handler.go | 9 +- management/server/http/routes.go | 5 + management/server/http/routes_test.go | 9 +- management/server/http/setupkeys.go | 195 ++++++++------- management/server/http/setupkeys_test.go | 222 ++++++++++++++++++ management/server/mock_server/account_mock.go | 68 +++--- management/server/setupkey.go | 213 +++++++++++++++-- management/server/setupkey_test.go | 172 +++++++++++++- 11 files changed, 767 insertions(+), 259 deletions(-) create mode 100644 management/server/http/setupkeys_test.go diff --git a/management/server/account.go b/management/server/account.go index 5ad3b3729..2c3fb4233 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -31,14 +31,15 @@ const ( type AccountManager interface { GetOrCreateAccountByUser(userId, domain string) (*Account, error) GetAccountByUser(userId string) (*Account, error) - AddSetupKey( + CreateSetupKey( accountId string, keyName string, keyType SetupKeyType, expiresIn time.Duration, + autoGroups []string, ) (*SetupKey, error) - RevokeSetupKey(accountId string, keyId string) (*SetupKey, error) - RenameSetupKey(accountId string, keyId string, newName string) (*SetupKey, error) + SaveSetupKey(accountID string, key *SetupKey) (*SetupKey, error) + GetSetupKey(accountID, keyID string) (*SetupKey, error) GetAccountById(accountId string) (*Account, error) GetAccountByUserOrAccountId(userId, accountId, domain string) (*Account, error) GetAccountWithAuthorizationClaims(claims jwtclaims.AuthorizationClaims) (*Account, error) @@ -75,6 +76,7 @@ type AccountManager interface { UpdateRoute(accountID string, routeID string, operations []RouteUpdateOperation) (*route.Route, error) DeleteRoute(accountID, routeID string) error ListRoutes(accountID string) ([]*route.Route, error) + ListSetupKeys(accountID string) ([]*SetupKey, error) } type DefaultAccountManager struct { @@ -244,93 +246,6 @@ func (am *DefaultAccountManager) warmupIDPCache() error { return nil } -// AddSetupKey generates a new setup key with a given name and type, and adds it to the specified account -func (am *DefaultAccountManager) AddSetupKey( - accountId string, - keyName string, - keyType SetupKeyType, - expiresIn time.Duration, -) (*SetupKey, error) { - am.mux.Lock() - defer am.mux.Unlock() - - keyDuration := DefaultSetupKeyDuration - if expiresIn != 0 { - keyDuration = expiresIn - } - - account, err := am.Store.GetAccount(accountId) - if err != nil { - return nil, status.Errorf(codes.NotFound, "account not found") - } - - setupKey := GenerateSetupKey(keyName, keyType, keyDuration) - account.SetupKeys[setupKey.Key] = setupKey - - err = am.Store.SaveAccount(account) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed adding account key") - } - - return setupKey, nil -} - -// RevokeSetupKey marks SetupKey as revoked - becomes not valid anymore -func (am *DefaultAccountManager) RevokeSetupKey(accountId string, keyId string) (*SetupKey, error) { - am.mux.Lock() - defer am.mux.Unlock() - - account, err := am.Store.GetAccount(accountId) - if err != nil { - return nil, status.Errorf(codes.NotFound, "account not found") - } - - setupKey := getAccountSetupKeyById(account, keyId) - if setupKey == nil { - return nil, status.Errorf(codes.NotFound, "unknown setupKey %s", keyId) - } - - keyCopy := setupKey.Copy() - keyCopy.Revoked = true - account.SetupKeys[keyCopy.Key] = keyCopy - err = am.Store.SaveAccount(account) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed adding account key") - } - - return keyCopy, nil -} - -// RenameSetupKey renames existing setup key of the specified account. -func (am *DefaultAccountManager) RenameSetupKey( - accountId string, - keyId string, - newName string, -) (*SetupKey, error) { - am.mux.Lock() - defer am.mux.Unlock() - - account, err := am.Store.GetAccount(accountId) - if err != nil { - return nil, status.Errorf(codes.NotFound, "account not found") - } - - setupKey := getAccountSetupKeyById(account, keyId) - if setupKey == nil { - return nil, status.Errorf(codes.NotFound, "unknown setupKey %s", keyId) - } - - keyCopy := setupKey.Copy() - keyCopy.Name = newName - account.SetupKeys[keyCopy.Key] = keyCopy - err = am.Store.SaveAccount(account) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed adding account key") - } - - return keyCopy, nil -} - // GetAccountById returns an existing account using its ID or error (NotFound) if doesn't exist func (am *DefaultAccountManager) GetAccountById(accountId string) (*Account, error) { am.mux.Lock() @@ -504,7 +419,6 @@ func (am *DefaultAccountManager) updateAccountDomainAttributes( // handleExistingUserAccount handles existing User accounts and update its domain attributes. // -// // If there is no primary domain account yet, we set the account as primary for the domain. Otherwise, // we compare the account's ID with the domain account ID, and if they don't match, we set the account as // non-primary account for the domain. We don't merge accounts at this stage, because of cases when a domain @@ -688,7 +602,7 @@ func newAccountWithId(accountId, userId, domain string) *Account { setupKeys := make(map[string]*SetupKey) defaultKey := GenerateDefaultSetupKey() - oneOffKey := GenerateSetupKey("One-off key", SetupKeyOneOff, DefaultSetupKeyDuration) + oneOffKey := GenerateSetupKey("One-off key", SetupKeyOneOff, DefaultSetupKeyDuration, []string{}) setupKeys[defaultKey.Key] = defaultKey setupKeys[oneOffKey.Key] = oneOffKey network := NewNetwork() @@ -713,15 +627,6 @@ func newAccountWithId(accountId, userId, domain string) *Account { return acc } -func getAccountSetupKeyById(acc *Account, keyId string) *SetupKey { - for _, k := range acc.SetupKeys { - if keyId == k.Id { - return k - } - } - return nil -} - func getAccountSetupKeyByKey(acc *Account, key string) *SetupKey { for _, k := range acc.SetupKeys { if key == k.Key { diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 51367c0a8..c76bc9c9b 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -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: diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 2817a6d21..8811ae348 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -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"` diff --git a/management/server/http/handler.go b/management/server/http/handler.go index 9ed3fbb19..2d536cd3c 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -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") diff --git a/management/server/http/routes.go b/management/server/http/routes.go index eb817c01e..7a26ae2e7 100644 --- a/management/server/http/routes.go +++ b/management/server/http/routes.go @@ -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 diff --git a/management/server/http/routes_test.go b/management/server/http/routes_test.go index bfca838e5..459e1cc83 100644 --- a/management/server/http/routes_test.go +++ b/management/server/http/routes_test.go @@ -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, }, { diff --git a/management/server/http/setupkeys.go b/management/server/http/setupkeys.go index eb154b226..632b7afdb 100644 --- a/management/server/http/setupkeys.go +++ b/management/server/http/setupkeys.go @@ -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, } } diff --git a/management/server/http/setupkeys_test.go b/management/server/http/setupkeys_test.go new file mode 100644 index 000000000..c37702e47 --- /dev/null +++ b/management/server/http/setupkeys_test.go @@ -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) +} diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index f2698ed1f..44e1519ae 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -12,9 +12,8 @@ import ( type MockAccountManager struct { GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error) GetAccountByUserFunc func(userId string) (*server.Account, error) - AddSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn time.Duration) (*server.SetupKey, error) - RevokeSetupKeyFunc func(accountId string, keyId string) (*server.SetupKey, error) - RenameSetupKeyFunc func(accountId string, keyId string, newName string) (*server.SetupKey, error) + CreateSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn time.Duration, autoGroups []string) (*server.SetupKey, error) + GetSetupKeyFunc func(accountID string, keyID string) (*server.SetupKey, error) GetAccountByIdFunc func(accountId string) (*server.Account, error) GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error) GetAccountWithAuthorizationClaimsFunc func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) @@ -51,6 +50,8 @@ type MockAccountManager struct { UpdateRouteFunc func(accountID string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error) DeleteRouteFunc func(accountID, routeID string) error ListRoutesFunc func(accountID string) ([]*route.Route, error) + SaveSetupKeyFunc func(accountID string, key *server.SetupKey) (*server.SetupKey, error) + ListSetupKeysFunc func(accountID string) ([]*server.SetupKey, error) } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface @@ -82,40 +83,18 @@ func (am *MockAccountManager) GetAccountByUser(userId string) (*server.Account, return nil, status.Errorf(codes.Unimplemented, "method GetAccountByUser is not implemented") } -// AddSetupKey mock implementation of AddSetupKey from server.AccountManager interface -func (am *MockAccountManager) AddSetupKey( +// CreateSetupKey mock implementation of CreateSetupKey from server.AccountManager interface +func (am *MockAccountManager) CreateSetupKey( accountId string, keyName string, keyType server.SetupKeyType, expiresIn time.Duration, + autoGroups []string, ) (*server.SetupKey, error) { - if am.AddSetupKeyFunc != nil { - return am.AddSetupKeyFunc(accountId, keyName, keyType, expiresIn) + if am.CreateSetupKeyFunc != nil { + return am.CreateSetupKeyFunc(accountId, keyName, keyType, expiresIn, autoGroups) } - return nil, status.Errorf(codes.Unimplemented, "method AddSetupKey is not implemented") -} - -// RevokeSetupKey mock implementation of RevokeSetupKey from server.AccountManager interface -func (am *MockAccountManager) RevokeSetupKey( - accountId string, - keyId string, -) (*server.SetupKey, error) { - if am.RevokeSetupKeyFunc != nil { - return am.RevokeSetupKeyFunc(accountId, keyId) - } - return nil, status.Errorf(codes.Unimplemented, "method RevokeSetupKey is not implemented") -} - -// RenameSetupKey mock implementation of RenameSetupKey from server.AccountManager interface -func (am *MockAccountManager) RenameSetupKey( - accountId string, - keyId string, - newName string, -) (*server.SetupKey, error) { - if am.RenameSetupKeyFunc != nil { - return am.RenameSetupKeyFunc(accountId, keyId, newName) - } - return nil, status.Errorf(codes.Unimplemented, "method RenameSetupKey is not implemented") + return nil, status.Errorf(codes.Unimplemented, "method CreateSetupKey is not implemented") } // GetAccountById mock implementation of GetAccountById from server.AccountManager interface @@ -415,3 +394,30 @@ func (am *MockAccountManager) ListRoutes(accountID string) ([]*route.Route, erro } return nil, status.Errorf(codes.Unimplemented, "method ListRoutes is not implemented") } + +// SaveSetupKey mocks SaveSetupKey of the AccountManager interface +func (am *MockAccountManager) SaveSetupKey(accountID string, key *server.SetupKey) (*server.SetupKey, error) { + if am.SaveSetupKeyFunc != nil { + return am.SaveSetupKeyFunc(accountID, key) + } + + return nil, status.Errorf(codes.Unimplemented, "method SaveSetupKey is not implemented") +} + +// GetSetupKey mocks GetSetupKey of the AccountManager interface +func (am *MockAccountManager) GetSetupKey(accountID, keyID string) (*server.SetupKey, error) { + if am.GetSetupKeyFunc != nil { + return am.GetSetupKeyFunc(accountID, keyID) + } + + return nil, status.Errorf(codes.Unimplemented, "method GetSetupKey is not implemented") +} + +// ListSetupKeys mocks ListSetupKeys of the AccountManager interface +func (am *MockAccountManager) ListSetupKeys(accountID string) ([]*server.SetupKey, error) { + if am.ListSetupKeysFunc != nil { + return am.ListSetupKeysFunc(accountID) + } + + return nil, status.Errorf(codes.Unimplemented, "method ListSetupKeys is not implemented") +} diff --git a/management/server/setupkey.go b/management/server/setupkey.go index 1dad9fe43..4e1de2915 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -1,7 +1,10 @@ package server import ( + "fmt" "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "hash/fnv" "strconv" "strings" @@ -18,8 +21,41 @@ const ( DefaultSetupKeyDuration = 24 * 30 * time.Hour // DefaultSetupKeyName is a default name of the default setup key DefaultSetupKeyName = "Default key" + + // UpdateSetupKeyName indicates a setup key name update operation + UpdateSetupKeyName SetupKeyUpdateOperationType = iota + // UpdateSetupKeyRevoked indicates a setup key revoked filed update operation + UpdateSetupKeyRevoked + // UpdateSetupKeyAutoGroups indicates a setup key auto-assign groups update operation + UpdateSetupKeyAutoGroups + // UpdateSetupKeyExpiresAt indicates a setup key expiration time update operation + UpdateSetupKeyExpiresAt ) +// SetupKeyUpdateOperationType operation type +type SetupKeyUpdateOperationType int + +func (t SetupKeyUpdateOperationType) String() string { + switch t { + case UpdateSetupKeyName: + return "UpdateSetupKeyName" + case UpdateSetupKeyRevoked: + return "UpdateSetupKeyRevoked" + case UpdateSetupKeyAutoGroups: + return "UpdateSetupKeyAutoGroups" + case UpdateSetupKeyExpiresAt: + return "UpdateSetupKeyExpiresAt" + default: + return "InvalidOperation" + } +} + +// SetupKeyUpdateOperation operation object with type and values to be applied +type SetupKeyUpdateOperation struct { + Type SetupKeyUpdateOperationType + Values []string +} + // SetupKeyType is the type of setup key type SetupKeyType string @@ -31,30 +67,38 @@ type SetupKey struct { Type SetupKeyType CreatedAt time.Time ExpiresAt time.Time + UpdatedAt time.Time // Revoked indicates whether the key was revoked or not (we don't remove them for tracking purposes) Revoked bool // UsedTimes indicates how many times the key was used UsedTimes int // LastUsed last time the key was used for peer registration LastUsed time.Time + // AutoGroups is a list of Group IDs that are auto assigned to a Peer when it uses this key to register + AutoGroups []string } -//Copy copies SetupKey to a new object +// Copy copies SetupKey to a new object func (key *SetupKey) Copy() *SetupKey { + if key.UpdatedAt.IsZero() { + key.UpdatedAt = key.CreatedAt + } return &SetupKey{ - Id: key.Id, - Key: key.Key, - Name: key.Name, - Type: key.Type, - CreatedAt: key.CreatedAt, - ExpiresAt: key.ExpiresAt, - Revoked: key.Revoked, - UsedTimes: key.UsedTimes, - LastUsed: key.LastUsed, + Id: key.Id, + Key: key.Key, + Name: key.Name, + Type: key.Type, + CreatedAt: key.CreatedAt, + ExpiresAt: key.ExpiresAt, + UpdatedAt: key.UpdatedAt, + Revoked: key.Revoked, + UsedTimes: key.UsedTimes, + LastUsed: key.LastUsed, + AutoGroups: key.AutoGroups, } } -//IncrementUsage makes a copy of a key, increments the UsedTimes by 1 and sets LastUsed to now +// IncrementUsage makes a copy of a key, increments the UsedTimes by 1 and sets LastUsed to now func (key *SetupKey) IncrementUsage() *SetupKey { c := key.Copy() c.UsedTimes = c.UsedTimes + 1 @@ -83,24 +127,25 @@ func (key *SetupKey) IsOverUsed() bool { } // GenerateSetupKey generates a new setup key -func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration) *SetupKey { +func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoGroups []string) *SetupKey { key := strings.ToUpper(uuid.New().String()) - createdAt := time.Now() return &SetupKey{ - Id: strconv.Itoa(int(Hash(key))), - Key: key, - Name: name, - Type: t, - CreatedAt: createdAt, - ExpiresAt: createdAt.Add(validFor), - Revoked: false, - UsedTimes: 0, + Id: strconv.Itoa(int(Hash(key))), + Key: key, + Name: name, + Type: t, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(validFor), + UpdatedAt: time.Now(), + Revoked: false, + UsedTimes: 0, + AutoGroups: autoGroups, } } // GenerateDefaultSetupKey generates a default setup key func GenerateDefaultSetupKey() *SetupKey { - return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration) + return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{}) } func Hash(s string) uint32 { @@ -111,3 +156,127 @@ func Hash(s string) uint32 { } return h.Sum32() } + +// CreateSetupKey generates a new setup key with a given name, type, list of groups IDs to auto-assign to peers registered with this key, +// and adds it to the specified account. A list of autoGroups IDs can be empty. +func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string, keyType SetupKeyType, + expiresIn time.Duration, autoGroups []string) (*SetupKey, error) { + am.mux.Lock() + defer am.mux.Unlock() + + keyDuration := DefaultSetupKeyDuration + if expiresIn != 0 { + keyDuration = expiresIn + } + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + for _, group := range autoGroups { + if _, ok := account.Groups[group]; !ok { + return nil, fmt.Errorf("group %s doesn't exist", group) + } + } + + setupKey := GenerateSetupKey(keyName, keyType, keyDuration, autoGroups) + account.SetupKeys[setupKey.Key] = setupKey + + err = am.Store.SaveAccount(account) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed adding account key") + } + + return setupKey, nil +} + +// SaveSetupKey saves the provided SetupKey to the database overriding the existing one. +// Due to the unique nature of a SetupKey certain properties must not be overwritten +// (e.g. the key itself, creation date, ID, etc). +// These properties are overwritten: Name, AutoGroups, Revoked. The rest is copied from the existing key. +func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *SetupKey) (*SetupKey, error) { + am.mux.Lock() + defer am.mux.Unlock() + + if keyToSave == nil { + return nil, status.Errorf(codes.InvalidArgument, "provided setup key to update is nil") + } + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + var oldKey *SetupKey + for _, key := range account.SetupKeys { + if key.Id == keyToSave.Id { + oldKey = key.Copy() + break + } + } + if oldKey == nil { + return nil, status.Errorf(codes.NotFound, "setup key not found") + } + + // only auto groups, revoked status, and name can be updated for now + newKey := oldKey.Copy() + newKey.Name = keyToSave.Name + newKey.AutoGroups = keyToSave.AutoGroups + newKey.Revoked = keyToSave.Revoked + newKey.UpdatedAt = time.Now() + + account.SetupKeys[newKey.Key] = newKey + + if err = am.Store.SaveAccount(account); err != nil { + return nil, err + } + + return newKey, am.updateAccountPeers(account) +} + +// ListSetupKeys returns a list of all setup keys of the account +func (am *DefaultAccountManager) ListSetupKeys(accountID string) ([]*SetupKey, error) { + am.mux.Lock() + defer am.mux.Unlock() + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + keys := make([]*SetupKey, 0, len(account.SetupKeys)) + for _, key := range account.SetupKeys { + keys = append(keys, key.Copy()) + } + + return keys, nil +} + +// GetSetupKey looks up a SetupKey by KeyID, returns NotFound error if not found. +func (am *DefaultAccountManager) GetSetupKey(accountID, keyID string) (*SetupKey, error) { + am.mux.Lock() + defer am.mux.Unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + var foundKey *SetupKey + for _, key := range account.SetupKeys { + if key.Id == keyID { + foundKey = key.Copy() + break + } + } + if foundKey == nil { + return nil, status.Errorf(codes.NotFound, "setup key not found") + } + + // the UpdatedAt field was introduced later, so there might be that some keys have a Zero value (e.g, null in the store file) + if foundKey.UpdatedAt.IsZero() { + foundKey.UpdatedAt = foundKey.CreatedAt + } + + return foundKey, nil +} diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index e4cad8fd7..08b6d8ad0 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -2,23 +2,159 @@ package server import ( "github.com/google/uuid" + "github.com/stretchr/testify/assert" "strconv" "testing" "time" ) +func TestDefaultAccountManager_SaveSetupKey(t *testing.T) { + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + } + + userID := "test_user" + account, err := manager.GetOrCreateAccountByUser(userID, "") + if err != nil { + t.Fatal(err) + } + + err = manager.SaveGroup(account.Id, &Group{ + ID: "group_1", + Name: "group_name_1", + Peers: []string{}, + }) + if err != nil { + t.Fatal(err) + } + + expiresIn := time.Hour + keyName := "my-test-key" + + key, err := manager.CreateSetupKey(account.Id, keyName, SetupKeyReusable, expiresIn, []string{}) + if err != nil { + t.Fatal(err) + } + + autoGroups := []string{"group_1", "group_2"} + newKeyName := "my-new-test-key" + revoked := true + newKey, err := manager.SaveSetupKey(account.Id, &SetupKey{ + Id: key.Id, + Name: newKeyName, + Revoked: revoked, + AutoGroups: autoGroups, + }) + if err != nil { + t.Fatal(err) + } + + assertKey(t, newKey, newKeyName, revoked, "reusable", 0, key.CreatedAt, key.ExpiresAt, + key.Id, time.Now(), autoGroups) +} + +func TestDefaultAccountManager_CreateSetupKey(t *testing.T) { + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + } + + userID := "test_user" + account, err := manager.GetOrCreateAccountByUser(userID, "") + if err != nil { + t.Fatal(err) + } + + err = manager.SaveGroup(account.Id, &Group{ + ID: "group_1", + Name: "group_name_1", + Peers: []string{}, + }) + if err != nil { + t.Fatal(err) + } + + err = manager.SaveGroup(account.Id, &Group{ + ID: "group_2", + Name: "group_name_2", + Peers: []string{}, + }) + if err != nil { + t.Fatal(err) + } + + type testCase struct { + name string + + expectedKeyName string + expectedUsedTimes int + expectedType string + expectedGroups []string + expectedCreatedAt time.Time + expectedUpdatedAt time.Time + expectedExpiresAt time.Time + expectedFailure bool //indicates whether key creation should fail + } + + now := time.Now() + expiresIn := time.Hour + testCase1 := testCase{ + name: "Should Create Setup Key successfully", + expectedKeyName: "my-test-key", + expectedUsedTimes: 0, + expectedType: "reusable", + expectedGroups: []string{"group_1", "group_2"}, + expectedCreatedAt: now, + expectedUpdatedAt: now, + expectedExpiresAt: now.Add(expiresIn), + expectedFailure: false, + } + testCase2 := testCase{ + name: "Create Setup Key should fail because of unexistent group", + expectedKeyName: "my-test-key", + expectedGroups: []string{"FAKE"}, + expectedFailure: true, + } + + for _, tCase := range []testCase{testCase1, testCase2} { + t.Run(tCase.name, func(t *testing.T) { + key, err := manager.CreateSetupKey(account.Id, tCase.expectedKeyName, SetupKeyReusable, expiresIn, + tCase.expectedGroups) + + if tCase.expectedFailure { + if err == nil { + t.Fatal("expected to fail") + } + return + } + + if err != nil { + t.Fatal(err) + } + + assertKey(t, key, tCase.expectedKeyName, false, tCase.expectedType, tCase.expectedUsedTimes, + tCase.expectedCreatedAt, tCase.expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))), + tCase.expectedUpdatedAt, tCase.expectedGroups) + }) + } + +} + func TestGenerateDefaultSetupKey(t *testing.T) { expectedName := "Default key" expectedRevoke := false expectedType := "reusable" expectedUsedTimes := 0 expectedCreatedAt := time.Now() + expectedUpdatedAt := time.Now() expectedExpiresAt := time.Now().Add(24 * 30 * time.Hour) + var expectedAutoGroups []string key := GenerateDefaultSetupKey() assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, - expectedExpiresAt, strconv.Itoa(int(Hash(key.Key)))) + expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))), expectedUpdatedAt, expectedAutoGroups) } @@ -29,41 +165,44 @@ func TestGenerateSetupKey(t *testing.T) { expectedUsedTimes := 0 expectedCreatedAt := time.Now() expectedExpiresAt := time.Now().Add(time.Hour) + expectedUpdatedAt := time.Now() + var expectedAutoGroups []string - key := GenerateSetupKey(expectedName, SetupKeyOneOff, time.Hour) + key := GenerateSetupKey(expectedName, SetupKeyOneOff, time.Hour, []string{}) - assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, expectedExpiresAt, strconv.Itoa(int(Hash(key.Key)))) + assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, + expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))), expectedUpdatedAt, expectedAutoGroups) } func TestSetupKey_IsValid(t *testing.T) { - validKey := GenerateSetupKey("valid key", SetupKeyOneOff, time.Hour) + validKey := GenerateSetupKey("valid key", SetupKeyOneOff, time.Hour, []string{}) if !validKey.IsValid() { t.Errorf("expected key to be valid, got invalid %v", validKey) } // expired - expiredKey := GenerateSetupKey("invalid key", SetupKeyOneOff, -time.Hour) + expiredKey := GenerateSetupKey("invalid key", SetupKeyOneOff, -time.Hour, []string{}) if expiredKey.IsValid() { t.Errorf("expected key to be invalid due to expiration, got valid %v", expiredKey) } // revoked - revokedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour) + revokedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{}) revokedKey.Revoked = true if revokedKey.IsValid() { t.Errorf("expected revoked key to be invalid, got valid %v", revokedKey) } // overused - overUsedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour) + overUsedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{}) overUsedKey.UsedTimes = 1 if overUsedKey.IsValid() { t.Errorf("expected overused key to be invalid, got valid %v", overUsedKey) } // overused - reusableKey := GenerateSetupKey("valid key", SetupKeyReusable, time.Hour) + reusableKey := GenerateSetupKey("valid key", SetupKeyReusable, time.Hour, []string{}) reusableKey.UsedTimes = 99 if !reusableKey.IsValid() { t.Errorf("expected reusable key to be valid when used many times, got valid %v", reusableKey) @@ -71,7 +210,8 @@ func TestSetupKey_IsValid(t *testing.T) { } func assertKey(t *testing.T, key *SetupKey, expectedName string, expectedRevoke bool, expectedType string, - expectedUsedTimes int, expectedCreatedAt time.Time, expectedExpiresAt time.Time, expectedID string) { + expectedUsedTimes int, expectedCreatedAt time.Time, expectedExpiresAt time.Time, expectedID string, + expectedUpdatedAt time.Time, expectedAutoGroups []string) { if key.Name != expectedName { t.Errorf("expected setup key to have Name %v, got %v", expectedName, key.Name) } @@ -92,6 +232,10 @@ func assertKey(t *testing.T, key *SetupKey, expectedName string, expectedRevoke t.Errorf("expected setup key to have ExpiresAt ~ %v, got %v", expectedExpiresAt, key.ExpiresAt) } + if key.UpdatedAt.Sub(expectedUpdatedAt).Round(time.Hour) != 0 { + t.Errorf("expected setup key to have UpdatedAt ~ %v, got %v", expectedUpdatedAt, key.UpdatedAt) + } + if key.CreatedAt.Sub(expectedCreatedAt).Round(time.Hour) != 0 { t.Errorf("expected setup key to have CreatedAt ~ %v, got %v", expectedCreatedAt, key.CreatedAt) } @@ -104,13 +248,19 @@ func assertKey(t *testing.T, key *SetupKey, expectedName string, expectedRevoke if key.Id != strconv.Itoa(int(Hash(key.Key))) { t.Errorf("expected key Id t= %v, got %v", expectedID, key.Id) } + + if len(key.AutoGroups) != len(expectedAutoGroups) { + t.Errorf("expected key AutoGroups size=%d, got %d", len(expectedAutoGroups), len(key.AutoGroups)) + } + assert.ElementsMatch(t, key.AutoGroups, expectedAutoGroups, "expected key AutoGroups to be equal") } func TestSetupKey_Copy(t *testing.T) { - key := GenerateSetupKey("key name", SetupKeyOneOff, time.Hour) + key := GenerateSetupKey("key name", SetupKeyOneOff, time.Hour, []string{}) keyCopy := key.Copy() - assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.ExpiresAt, key.Id) + assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.ExpiresAt, key.Id, + key.UpdatedAt, key.AutoGroups) }