package apikey import ( "crypto/sha256" "encoding/base64" "fmt" "strings" "testing" "time" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/datastore" "github.com/stretchr/testify/assert" "github.com/rs/zerolog/log" ) func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) { is := assert.New(t) is.Implements((*APIKeyService)(nil), NewAPIKeyService(nil, nil)) } func Test_GenerateApiKey(t *testing.T) { is := assert.New(t) _, store := datastore.MustNewTestStore(t, true, true) service := NewAPIKeyService(store.APIKeyRepository(), store.User()) t.Run("Successfully generates API key", func(t *testing.T) { desc := "test-1" rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, desc) is.NoError(err) is.NotEmpty(rawKey) is.NotEmpty(apiKey) is.Equal(desc, apiKey.Description) }) t.Run("Api key prefix is 7 chars", func(t *testing.T) { rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-2") is.NoError(err) is.Equal(rawKey[:7], apiKey.Prefix) is.Len(apiKey.Prefix, 7) }) t.Run("Api key has 'ptr_' as prefix", func(t *testing.T) { rawKey, _, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-x") is.NoError(err) is.Equal(portainerAPIKeyPrefix, "ptr_") is.True(strings.HasPrefix(rawKey, "ptr_")) }) t.Run("Successfully caches API key", func(t *testing.T) { user := portainer.User{ID: 1} _, apiKey, err := service.GenerateApiKey(user, "test-3") is.NoError(err) userFromCache, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest) is.True(ok) is.Equal(user, userFromCache) is.Equal(apiKey, &apiKeyFromCache) }) t.Run("Decoded raw api-key digest matches generated digest", func(t *testing.T) { rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-4") is.NoError(err) generatedDigest := sha256.Sum256([]byte(rawKey)) is.Equal(apiKey.Digest, base64.StdEncoding.EncodeToString(generatedDigest[:])) }) } func Test_GetAPIKey(t *testing.T) { is := assert.New(t) _, store := datastore.MustNewTestStore(t, true, true) service := NewAPIKeyService(store.APIKeyRepository(), store.User()) t.Run("Successfully returns all API keys", func(t *testing.T) { user := portainer.User{ID: 1} _, apiKey, err := service.GenerateApiKey(user, "test-1") is.NoError(err) apiKeyGot, err := service.GetAPIKey(apiKey.ID) is.NoError(err) is.Equal(apiKey, apiKeyGot) }) } func Test_GetAPIKeys(t *testing.T) { is := assert.New(t) _, store := datastore.MustNewTestStore(t, true, true) service := NewAPIKeyService(store.APIKeyRepository(), store.User()) t.Run("Successfully returns all API keys", func(t *testing.T) { user := portainer.User{ID: 1} _, _, err := service.GenerateApiKey(user, "test-1") is.NoError(err) _, _, err = service.GenerateApiKey(user, "test-2") is.NoError(err) keys, err := service.GetAPIKeys(user.ID) is.NoError(err) is.Len(keys, 2) }) } func Test_GetDigestUserAndKey(t *testing.T) { is := assert.New(t) _, store := datastore.MustNewTestStore(t, true, true) service := NewAPIKeyService(store.APIKeyRepository(), store.User()) t.Run("Successfully returns user and api key associated to digest", func(t *testing.T) { user := portainer.User{ID: 1} _, apiKey, err := service.GenerateApiKey(user, "test-1") is.NoError(err) userGot, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest) is.NoError(err) is.Equal(user, userGot) is.Equal(*apiKey, apiKeyGot) }) t.Run("Successfully caches user and api key associated to digest", func(t *testing.T) { user := portainer.User{ID: 1} _, apiKey, err := service.GenerateApiKey(user, "test-1") is.NoError(err) userGot, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest) is.NoError(err) is.Equal(user, userGot) is.Equal(*apiKey, apiKeyGot) userFromCache, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest) is.True(ok) is.Equal(userGot, userFromCache) is.Equal(apiKeyGot, apiKeyFromCache) }) } func Test_UpdateAPIKey(t *testing.T) { is := assert.New(t) _, store := datastore.MustNewTestStore(t, true, true) service := NewAPIKeyService(store.APIKeyRepository(), store.User()) t.Run("Successfully updates the api-key LastUsed time", func(t *testing.T) { user := portainer.User{ID: 1} store.User().Create(&user) _, apiKey, err := service.GenerateApiKey(user, "test-x") is.NoError(err) apiKey.LastUsed = time.Now().UTC().Unix() err = service.UpdateAPIKey(apiKey) is.NoError(err) _, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest) is.NoError(err) log.Debug().Str("wanted", fmt.Sprintf("%+v", apiKey)).Str("got", fmt.Sprintf("%+v", apiKeyGot)).Msg("") is.Equal(apiKey.LastUsed, apiKeyGot.LastUsed) }) t.Run("Successfully updates api-key in cache upon api-key update", func(t *testing.T) { _, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-x2") is.NoError(err) _, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest) is.True(ok) is.Equal(*apiKey, apiKeyFromCache) apiKey.LastUsed = time.Now().UTC().Unix() is.NotEqual(*apiKey, apiKeyFromCache) err = service.UpdateAPIKey(apiKey) is.NoError(err) _, updatedAPIKeyFromCache, ok := service.cache.Get(apiKey.Digest) is.True(ok) is.Equal(*apiKey, updatedAPIKeyFromCache) }) } func Test_DeleteAPIKey(t *testing.T) { is := assert.New(t) _, store := datastore.MustNewTestStore(t, true, true) service := NewAPIKeyService(store.APIKeyRepository(), store.User()) t.Run("Successfully updates the api-key", func(t *testing.T) { user := portainer.User{ID: 1} _, apiKey, err := service.GenerateApiKey(user, "test-1") is.NoError(err) _, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest) is.NoError(err) is.Equal(*apiKey, apiKeyGot) err = service.DeleteAPIKey(apiKey.ID) is.NoError(err) _, _, err = service.GetDigestUserAndKey(apiKey.Digest) is.Error(err) }) t.Run("Successfully removes api-key from cache upon deletion", func(t *testing.T) { user := portainer.User{ID: 1} _, apiKey, err := service.GenerateApiKey(user, "test-1") is.NoError(err) _, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest) is.True(ok) is.Equal(*apiKey, apiKeyFromCache) err = service.DeleteAPIKey(apiKey.ID) is.NoError(err) _, _, ok = service.cache.Get(apiKey.Digest) is.False(ok) }) } func Test_InvalidateUserKeyCache(t *testing.T) { is := assert.New(t) _, store := datastore.MustNewTestStore(t, true, true) service := NewAPIKeyService(store.APIKeyRepository(), store.User()) t.Run("Successfully updates evicts keys from cache", func(t *testing.T) { // generate api keys user := portainer.User{ID: 1} _, apiKey1, err := service.GenerateApiKey(user, "test-1") is.NoError(err) _, apiKey2, err := service.GenerateApiKey(user, "test-2") is.NoError(err) // verify api keys are present in cache _, apiKeyFromCache, ok := service.cache.Get(apiKey1.Digest) is.True(ok) is.Equal(*apiKey1, apiKeyFromCache) _, apiKeyFromCache, ok = service.cache.Get(apiKey2.Digest) is.True(ok) is.Equal(*apiKey2, apiKeyFromCache) // evict cache ok = service.InvalidateUserKeyCache(user.ID) is.True(ok) // verify users keys have been flushed from cache _, _, ok = service.cache.Get(apiKey1.Digest) is.False(ok) _, _, ok = service.cache.Get(apiKey2.Digest) is.False(ok) }) t.Run("User key eviction does not affect other users keys", func(t *testing.T) { // generate keys for 2 users user1 := portainer.User{ID: 1} _, apiKey1, err := service.GenerateApiKey(user1, "test-1") is.NoError(err) user2 := portainer.User{ID: 2} _, apiKey2, err := service.GenerateApiKey(user2, "test-2") is.NoError(err) // verify keys in cache _, apiKeyFromCache, ok := service.cache.Get(apiKey1.Digest) is.True(ok) is.Equal(*apiKey1, apiKeyFromCache) _, apiKeyFromCache, ok = service.cache.Get(apiKey2.Digest) is.True(ok) is.Equal(*apiKey2, apiKeyFromCache) // evict key of single user from cache ok = service.cache.InvalidateUserKeyCache(user1.ID) is.True(ok) // verify user1 key has been flushed from cache _, _, ok = service.cache.Get(apiKey1.Digest) is.False(ok) // verify user2 key is still in cache _, _, ok = service.cache.Get(apiKey2.Digest) is.True(ok) }) }