Add SetupKey usage limit (#605)

Add a usage_limit parameter to the API.
This limits the number of times a setup key
can be used. 
usage_limit == 0 indicates the the usage is inlimited.
This commit is contained in:
Misha Bragin 2022-12-05 13:09:59 +01:00 committed by GitHub
parent d2d5d4b4b9
commit d1b7c23b19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 65 additions and 31 deletions

View File

@ -38,13 +38,8 @@ func cacheEntryExpiration() time.Duration {
type AccountManager interface { type AccountManager interface {
GetOrCreateAccountByUser(userId, domain string) (*Account, error) GetOrCreateAccountByUser(userId, domain string) (*Account, error)
CreateSetupKey( CreateSetupKey(accountID string, keyName string, keyType SetupKeyType, expiresIn time.Duration,
accountId string, autoGroups []string, usageLimit int) (*SetupKey, error)
keyName string,
keyType SetupKeyType,
expiresIn time.Duration,
autoGroups []string,
) (*SetupKey, error)
SaveSetupKey(accountID string, key *SetupKey) (*SetupKey, error) SaveSetupKey(accountID string, key *SetupKey) (*SetupKey, error)
CreateUser(accountID string, key *UserInfo) (*UserInfo, error) CreateUser(accountID string, key *UserInfo) (*UserInfo, error)
ListSetupKeys(accountID, userID string) ([]*SetupKey, error) ListSetupKeys(accountID, userID string) ([]*SetupKey, error)
@ -945,7 +940,8 @@ func newAccountWithId(accountId, userId, domain string) *Account {
setupKeys := make(map[string]*SetupKey) setupKeys := make(map[string]*SetupKey)
defaultKey := GenerateDefaultSetupKey() defaultKey := GenerateDefaultSetupKey()
oneOffKey := GenerateSetupKey("One-off key", SetupKeyOneOff, DefaultSetupKeyDuration, []string{}) oneOffKey := GenerateSetupKey("One-off key", SetupKeyOneOff, DefaultSetupKeyDuration, []string{},
SetupKeyUnlimitedUsage)
setupKeys[defaultKey.Key] = defaultKey setupKeys[defaultKey.Key] = defaultKey
setupKeys[oneOffKey.Key] = oneOffKey setupKeys[oneOffKey.Key] = oneOffKey
network := NewNetwork() network := NewNetwork()

View File

@ -193,6 +193,9 @@ components:
description: Setup key last update date description: Setup key last update date
type: string type: string
format: date-time format: date-time
usage_limit:
description: A number of times this key can be used. The value of 0 indicates the unlimited usage.
type: integer
required: required:
- id - id
- key - key
@ -206,6 +209,7 @@ components:
- state - state
- auto_groups - auto_groups
- updated_at - updated_at
- usage_limit
SetupKeyRequest: SetupKeyRequest:
type: object type: object
properties: properties:
@ -226,12 +230,16 @@ components:
type: array type: array
items: items:
type: string type: string
usage_limit:
description: A number of times this key can be used. The value of 0 indicates the unlimited usage.
type: integer
required: required:
- name - name
- type - type
- expires_in - expires_in
- revoked - revoked
- auto_groups - auto_groups
- usage_limit
GroupMinimum: GroupMinimum:
type: object type: object
properties: properties:

View File

@ -449,6 +449,9 @@ type SetupKey struct {
// UpdatedAt Setup key last update date // UpdatedAt Setup key last update date
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
// UsageLimit A number of times this key can be used. The value of 0 indicates the unlimited usage.
UsageLimit int `json:"usage_limit"`
// UsedTimes Usage count of setup key // UsedTimes Usage count of setup key
UsedTimes int `json:"used_times"` UsedTimes int `json:"used_times"`
@ -472,6 +475,9 @@ type SetupKeyRequest struct {
// Type Setup key type, one-off for single time usage and reusable // Type Setup key type, one-off for single time usage and reusable
Type string `json:"type"` Type string `json:"type"`
// UsageLimit A number of times this key can be used. The value of 0 indicates the unlimited usage.
UsageLimit int `json:"usage_limit"`
} }
// User defines model for User. // User defines model for User.

View File

@ -50,7 +50,7 @@ func (h *SetupKeys) CreateSetupKeyHandler(w http.ResponseWriter, r *http.Request
if !(server.SetupKeyType(req.Type) == server.SetupKeyReusable || if !(server.SetupKeyType(req.Type) == server.SetupKeyReusable ||
server.SetupKeyType(req.Type) == server.SetupKeyOneOff) { server.SetupKeyType(req.Type) == server.SetupKeyOneOff) {
util.WriteError(status.Errorf(status.InvalidArgument, "unknown setup key type %s", string(req.Type)), w) util.WriteError(status.Errorf(status.InvalidArgument, "unknown setup key type %s", req.Type), w)
return return
} }
@ -61,7 +61,7 @@ func (h *SetupKeys) CreateSetupKeyHandler(w http.ResponseWriter, r *http.Request
} }
setupKey, err := h.accountManager.CreateSetupKey(account.Id, req.Name, server.SetupKeyType(req.Type), expiresIn, setupKey, err := h.accountManager.CreateSetupKey(account.Id, req.Name, server.SetupKeyType(req.Type), expiresIn,
req.AutoGroups) req.AutoGroups, req.UsageLimit)
if err != nil { if err != nil {
util.WriteError(err, w) util.WriteError(err, w)
return return
@ -201,5 +201,6 @@ func toResponseBody(key *server.SetupKey) *api.SetupKey {
State: state, State: state,
AutoGroups: key.AutoGroups, AutoGroups: key.AutoGroups,
UpdatedAt: key.UpdatedAt, UpdatedAt: key.UpdatedAt,
UsageLimit: key.UsageLimit,
} }
} }

View File

@ -46,7 +46,8 @@ func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.Setup
"id-all": {ID: "id-all", Name: "All"}}, "id-all": {ID: "id-all", Name: "All"}},
}, user, nil }, user, nil
}, },
CreateSetupKeyFunc: func(_ string, keyName string, typ server.SetupKeyType, _ time.Duration, _ []string) (*server.SetupKey, error) { CreateSetupKeyFunc: func(_ string, keyName string, typ server.SetupKeyType, _ time.Duration, _ []string,
_ int) (*server.SetupKey, error) {
if keyName == newKey.Name || typ != newKey.Type { if keyName == newKey.Name || typ != newKey.Type {
return newKey, nil return newKey, nil
} }
@ -93,7 +94,8 @@ func TestSetupKeysHandlers(t *testing.T) {
adminUser := server.NewAdminUser("test_user") adminUser := server.NewAdminUser("test_user")
newSetupKey := server.GenerateSetupKey(newSetupKeyName, server.SetupKeyReusable, 0, []string{"group-1"}) newSetupKey := server.GenerateSetupKey(newSetupKeyName, server.SetupKeyReusable, 0, []string{"group-1"},
server.SetupKeyUnlimitedUsage)
updatedDefaultSetupKey := defaultSetupKey.Copy() updatedDefaultSetupKey := defaultSetupKey.Copy()
updatedDefaultSetupKey.AutoGroups = []string{"group-1"} updatedDefaultSetupKey.AutoGroups = []string{"group-1"}
updatedDefaultSetupKey.Name = updatedSetupKeyName updatedDefaultSetupKey.Name = updatedSetupKeyName

View File

@ -13,7 +13,7 @@ import (
type MockAccountManager struct { type MockAccountManager struct {
GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error) GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error)
GetAccountByUserFunc func(userId string) (*server.Account, error) GetAccountByUserFunc func(userId string) (*server.Account, error)
CreateSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn time.Duration, autoGroups []string) (*server.SetupKey, error) CreateSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn time.Duration, autoGroups []string, usageLimit int) (*server.SetupKey, error)
GetSetupKeyFunc func(accountID, userID, keyID string) (*server.SetupKey, error) GetSetupKeyFunc func(accountID, userID, keyID string) (*server.SetupKey, error)
GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error) GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error)
IsUserAdminFunc func(claims jwtclaims.AuthorizationClaims) (bool, error) IsUserAdminFunc func(claims jwtclaims.AuthorizationClaims) (bool, error)
@ -102,14 +102,15 @@ func (am *MockAccountManager) GetAccountByUser(userId string) (*server.Account,
// CreateSetupKey mock implementation of CreateSetupKey from server.AccountManager interface // CreateSetupKey mock implementation of CreateSetupKey from server.AccountManager interface
func (am *MockAccountManager) CreateSetupKey( func (am *MockAccountManager) CreateSetupKey(
accountId string, accountID string,
keyName string, keyName string,
keyType server.SetupKeyType, keyType server.SetupKeyType,
expiresIn time.Duration, expiresIn time.Duration,
autoGroups []string, autoGroups []string,
usageLimit int,
) (*server.SetupKey, error) { ) (*server.SetupKey, error) {
if am.CreateSetupKeyFunc != nil { if am.CreateSetupKeyFunc != nil {
return am.CreateSetupKeyFunc(accountId, keyName, keyType, expiresIn, autoGroups) return am.CreateSetupKeyFunc(accountID, keyName, keyType, expiresIn, autoGroups, usageLimit)
} }
return nil, status.Errorf(codes.Unimplemented, "method CreateSetupKey is not implemented") return nil, status.Errorf(codes.Unimplemented, "method CreateSetupKey is not implemented")
} }

View File

@ -20,7 +20,11 @@ const (
DefaultSetupKeyDuration = 24 * 30 * time.Hour DefaultSetupKeyDuration = 24 * 30 * time.Hour
// DefaultSetupKeyName is a default name of the default setup key // DefaultSetupKeyName is a default name of the default setup key
DefaultSetupKeyName = "Default key" DefaultSetupKeyName = "Default key"
// SetupKeyUnlimitedUsage indicates an unlimited usage of a setup key
SetupKeyUnlimitedUsage = 0
)
const (
// UpdateSetupKeyName indicates a setup key name update operation // UpdateSetupKeyName indicates a setup key name update operation
UpdateSetupKeyName SetupKeyUpdateOperationType = iota UpdateSetupKeyName SetupKeyUpdateOperationType = iota
// UpdateSetupKeyRevoked indicates a setup key revoked filed update operation // UpdateSetupKeyRevoked indicates a setup key revoked filed update operation
@ -75,6 +79,9 @@ type SetupKey struct {
LastUsed time.Time 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 is a list of Group IDs that are auto assigned to a Peer when it uses this key to register
AutoGroups []string AutoGroups []string
// UsageLimit indicates the number of times this key can be used to enroll a machine.
// The value of 0 indicates the unlimited usage.
UsageLimit int
} }
// Copy copies SetupKey to a new object // Copy copies SetupKey to a new object
@ -96,6 +103,7 @@ func (key *SetupKey) Copy() *SetupKey {
UsedTimes: key.UsedTimes, UsedTimes: key.UsedTimes,
LastUsed: key.LastUsed, LastUsed: key.LastUsed,
AutoGroups: autoGroups, AutoGroups: autoGroups,
UsageLimit: key.UsageLimit,
} }
} }
@ -131,14 +139,23 @@ func (key *SetupKey) IsExpired() bool {
return time.Now().After(key.ExpiresAt) return time.Now().After(key.ExpiresAt)
} }
// IsOverUsed if key was used too many times // IsOverUsed if the key was used too many times. SetupKey.UsageLimit == 0 indicates the unlimited usage.
func (key *SetupKey) IsOverUsed() bool { func (key *SetupKey) IsOverUsed() bool {
return key.Type == SetupKeyOneOff && key.UsedTimes >= 1 limit := key.UsageLimit
if key.Type == SetupKeyOneOff {
limit = 1
}
return limit > 0 && key.UsedTimes >= limit
} }
// GenerateSetupKey generates a new setup key // GenerateSetupKey generates a new setup key
func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoGroups []string) *SetupKey { func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoGroups []string,
usageLimit int) *SetupKey {
key := strings.ToUpper(uuid.New().String()) key := strings.ToUpper(uuid.New().String())
limit := usageLimit
if t == SetupKeyOneOff {
limit = 1
}
return &SetupKey{ return &SetupKey{
Id: strconv.Itoa(int(Hash(key))), Id: strconv.Itoa(int(Hash(key))),
Key: key, Key: key,
@ -150,12 +167,14 @@ func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoG
Revoked: false, Revoked: false,
UsedTimes: 0, UsedTimes: 0,
AutoGroups: autoGroups, AutoGroups: autoGroups,
UsageLimit: limit,
} }
} }
// GenerateDefaultSetupKey generates a default setup key // GenerateDefaultSetupKey generates a default reusable setup key with an unlimited usage and 30 days expiration
func GenerateDefaultSetupKey() *SetupKey { func GenerateDefaultSetupKey() *SetupKey {
return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{}) return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{},
SetupKeyUnlimitedUsage)
} }
func Hash(s string) uint32 { func Hash(s string) uint32 {
@ -170,7 +189,7 @@ func Hash(s string) uint32 {
// CreateSetupKey generates a new setup key with a given name, type, list of groups IDs to auto-assign to peers registered with this key, // 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. // 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, func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string, keyType SetupKeyType,
expiresIn time.Duration, autoGroups []string) (*SetupKey, error) { expiresIn time.Duration, autoGroups []string, usageLimit int) (*SetupKey, error) {
unlock := am.Store.AcquireAccountLock(accountID) unlock := am.Store.AcquireAccountLock(accountID)
defer unlock() defer unlock()
@ -190,7 +209,7 @@ func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string
} }
} }
setupKey := GenerateSetupKey(keyName, keyType, keyDuration, autoGroups) setupKey := GenerateSetupKey(keyName, keyType, keyDuration, autoGroups, usageLimit)
account.SetupKeys[setupKey.Key] = setupKey account.SetupKeys[setupKey.Key] = setupKey
err = am.Store.SaveAccount(account) err = am.Store.SaveAccount(account)

View File

@ -32,7 +32,8 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) {
expiresIn := time.Hour expiresIn := time.Hour
keyName := "my-test-key" keyName := "my-test-key"
key, err := manager.CreateSetupKey(account.Id, keyName, SetupKeyReusable, expiresIn, []string{}) key, err := manager.CreateSetupKey(account.Id, keyName, SetupKeyReusable, expiresIn, []string{},
SetupKeyUnlimitedUsage)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -120,7 +121,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
for _, tCase := range []testCase{testCase1, testCase2} { for _, tCase := range []testCase{testCase1, testCase2} {
t.Run(tCase.name, func(t *testing.T) { t.Run(tCase.name, func(t *testing.T) {
key, err := manager.CreateSetupKey(account.Id, tCase.expectedKeyName, SetupKeyReusable, expiresIn, key, err := manager.CreateSetupKey(account.Id, tCase.expectedKeyName, SetupKeyReusable, expiresIn,
tCase.expectedGroups) tCase.expectedGroups, SetupKeyUnlimitedUsage)
if tCase.expectedFailure { if tCase.expectedFailure {
if err == nil { if err == nil {
@ -168,7 +169,7 @@ func TestGenerateSetupKey(t *testing.T) {
expectedUpdatedAt := time.Now() expectedUpdatedAt := time.Now()
var expectedAutoGroups []string var expectedAutoGroups []string
key := GenerateSetupKey(expectedName, SetupKeyOneOff, time.Hour, []string{}) key := GenerateSetupKey(expectedName, SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage)
assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt,
expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))), expectedUpdatedAt, expectedAutoGroups) expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))), expectedUpdatedAt, expectedAutoGroups)
@ -176,33 +177,33 @@ func TestGenerateSetupKey(t *testing.T) {
} }
func TestSetupKey_IsValid(t *testing.T) { func TestSetupKey_IsValid(t *testing.T) {
validKey := GenerateSetupKey("valid key", SetupKeyOneOff, time.Hour, []string{}) validKey := GenerateSetupKey("valid key", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage)
if !validKey.IsValid() { if !validKey.IsValid() {
t.Errorf("expected key to be valid, got invalid %v", validKey) t.Errorf("expected key to be valid, got invalid %v", validKey)
} }
// expired // expired
expiredKey := GenerateSetupKey("invalid key", SetupKeyOneOff, -time.Hour, []string{}) expiredKey := GenerateSetupKey("invalid key", SetupKeyOneOff, -time.Hour, []string{}, SetupKeyUnlimitedUsage)
if expiredKey.IsValid() { if expiredKey.IsValid() {
t.Errorf("expected key to be invalid due to expiration, got valid %v", expiredKey) t.Errorf("expected key to be invalid due to expiration, got valid %v", expiredKey)
} }
// revoked // revoked
revokedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{}) revokedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage)
revokedKey.Revoked = true revokedKey.Revoked = true
if revokedKey.IsValid() { if revokedKey.IsValid() {
t.Errorf("expected revoked key to be invalid, got valid %v", revokedKey) t.Errorf("expected revoked key to be invalid, got valid %v", revokedKey)
} }
// overused // overused
overUsedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{}) overUsedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage)
overUsedKey.UsedTimes = 1 overUsedKey.UsedTimes = 1
if overUsedKey.IsValid() { if overUsedKey.IsValid() {
t.Errorf("expected overused key to be invalid, got valid %v", overUsedKey) t.Errorf("expected overused key to be invalid, got valid %v", overUsedKey)
} }
// overused // overused
reusableKey := GenerateSetupKey("valid key", SetupKeyReusable, time.Hour, []string{}) reusableKey := GenerateSetupKey("valid key", SetupKeyReusable, time.Hour, []string{}, SetupKeyUnlimitedUsage)
reusableKey.UsedTimes = 99 reusableKey.UsedTimes = 99
if !reusableKey.IsValid() { if !reusableKey.IsValid() {
t.Errorf("expected reusable key to be valid when used many times, got valid %v", reusableKey) t.Errorf("expected reusable key to be valid when used many times, got valid %v", reusableKey)
@ -257,7 +258,7 @@ func assertKey(t *testing.T, key *SetupKey, expectedName string, expectedRevoke
func TestSetupKey_Copy(t *testing.T) { func TestSetupKey_Copy(t *testing.T) {
key := GenerateSetupKey("key name", SetupKeyOneOff, time.Hour, []string{}) key := GenerateSetupKey("key name", SetupKeyOneOff, time.Hour, []string{}, SetupKeyUnlimitedUsage)
keyCopy := key.Copy() 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,