feat(api-key/backend): introducing support for api-key based auth EE-978 (#6079)

* feat(access-token): Multi-auth middleware support EE-1891 (#5936)

* AnyAuth middleware initial implementation with tests

* using mux.MiddlewareFunc instead of custom definition

* removed redundant comments

* - ExtractBearerToken bouncer func made private
- changed helm token handling functionality to use jwt service to convert token to jwt string
- updated tests
- fixed helm list broken test due to missing token in request context

* rename mwCheckAuthentication -> mwCheckJWTAuthentication

* - introduce initial api-key auth support using X-API-KEY header
- added tests to validate x-api-key request header presence

* updated core mwAuthenticatedUser middleware to support multiple auth paradigms

* - simplified anyAuth middleware
- enforcing authmiddleware to implement verificationFunc interface
- created tests for middleware

* simplify bouncer

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>

* feat(api-key): user-access-token generation endpoint EE-1889 EE-1888 EE-1895 (#6012)

* user-access-token generation endpoint

* fix comment

* - introduction of apikey service
- seperation of repository from service logic - called in handler

* fixed tests

* - fixed api key prefix
- added tests

* added another test for digest matching

* updated swagger spec for access token creation

* api key response returns raw key and struct - easing testability

* test for api key prefix length

* added another TODO to middleware

* - api-key prefix rune -> string (rune does not auto-encode when response sent back to client)
- digest -> pointer as we want to allow nil values and omit digest in responses (when nil)

* - updated apikey struct
- updated apikey service to support all common operations
- updated apikey repo
- integration of apikey service into bouncer
- added test for all apikey service functions
- boilerplate code for apikey service integration

* - user access token generation tests
- apiKeyLookup updated to support query params
- added api-key tests for query params
- added api-key tests for apiKeyLookup

* get and remove access token handlers

* get and remove access token handler tests

* - delete user deletes all associated api keys
- tests for this functionality

* removed redundant []byte cast

* automatic api-key eviction set within cache for 1 hour

* fixed bug with loop var using final value

* fixed service comment

* ignore bolt error responses

* case-insensitive query param check

* simplified query var assignment

* - added GetAPIKey func to get by unique id
- updated DeleteAPIKey func to not require user ID
- updated tests

* GenerateRandomKey helper func from github.com/gorilla/securecookie moved to codebase

* json response casing for api-keys fixed

* updating api-key will update the cache

* updated golang LRU cache

* using hashicorps golang-LRU cache for api keys

* simplified jwt check in create user access token

* fixed api-key update logic on cache miss

* Prefix generated api-keys with `ptr_` (#6067)

* prefix api-keys with 'ptr_'

* updated apikey description

* refactor

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>

* helm list test refactor

* fixed user delete test

* reduce test nil pointer errors

* using correct http 201 created status code for token creation; updated tests

* fixed swagger doc user id path param for user access token based endpoints

* added api-key security openapi spec to existing jwt secured endpoints (#6091)

* fixed flaky test

* apikey datecreated and lastused attrs converted to unix timestamp

* feat(user): added access token datatable. (#6124)

* feat(user): added access token datatable.

* feat(tokens): only display lastUsed time when it is not the default date

* Update app/portainer/views/account/accountController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/views/account/accountController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/views/account/accountController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/components/datatables/access-tokens-datatable/accessTokensDatatableController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/services/api/userService.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* feat(improvements): proposed datatable improvements to speed up dev time (#6138)

* modal code update

* updated datatable filenames, updated controller to be default class export

* fix(access-token): code improvement.

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* feat(apikeys): create access token view initial implementation EE-1886 (#6129)

* CopyButton implementation

* Code component implementation

* ToolTip component migration to another folder

* TextTip component implementation - continued

* form Heading component

* Button component updated to be more dynamic

* copybutton - small size

* form control pass tip error

* texttip small text

* CreateAccessToken react feature initial implementation

* create user access token angularjs view implementation

* registration of CreateAccessToken component in AngularJS

* user token generation API request moved to angular service, method passed down instead

* consistent naming of access token operations; clustered similar code together

* any user can add access token

* create access token page routing

* moved code component to the correct location

* removed isadmin check as all functionality applicable to all users

* create access token angular view moved up a level

* fixed PR issues, updated PR

* addressed PR issues/improvements

* explicit hr for horizontal line

* fixed merge conflict storybook build breaking

* - apikey test
- cache test

* addressed testing issues:
- description validations
- remove token description link on table

* fix(api-keys): user role change evicts user keys in cache EE-2113 (#6168)

* user role change evicts user api keys in cache

* EvictUserKeyCache -> InvalidateUserKeyCache

* godoc for InvalidateUserKeyCache func

* additional test line

* disable add access token button after adding token to prevent spam

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
pull/6176/head
zees-dev 2021-11-30 15:31:16 +13:00 committed by GitHub
parent 120584909c
commit 69c17986d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
197 changed files with 3137 additions and 91 deletions

View File

@ -150,6 +150,7 @@
"// @description ",
"// @description **Access policy**: ",
"// @tags ",
"// @security ApiKeyAuth",
"// @security jwt",
"// @accept json",
"// @produce json",

View File

@ -120,6 +120,7 @@ When adding a new route to an existing handler use the following as a template (
// @description
// @description **Access policy**:
// @tags
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

29
api/apikey/apikey.go Normal file
View File

@ -0,0 +1,29 @@
package apikey
import (
"crypto/rand"
"io"
portainer "github.com/portainer/portainer/api"
)
// APIKeyService represents a service for managing API keys.
type APIKeyService interface {
HashRaw(rawKey string) []byte
GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error)
GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey, error)
GetDigestUserAndKey(digest []byte) (portainer.User, portainer.APIKey, error)
UpdateAPIKey(apiKey *portainer.APIKey) error
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
InvalidateUserKeyCache(userId portainer.UserID) bool
}
// generateRandomKey generates a random key of specified length
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
func generateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}

50
api/apikey/apikey_test.go Normal file
View File

@ -0,0 +1,50 @@
package apikey
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_generateRandomKey(t *testing.T) {
is := assert.New(t)
tests := []struct {
name string
wantLenth int
}{
{
name: "Generate a random key of length 16",
wantLenth: 16,
},
{
name: "Generate a random key of length 32",
wantLenth: 32,
},
{
name: "Generate a random key of length 64",
wantLenth: 64,
},
{
name: "Generate a random key of length 128",
wantLenth: 128,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := generateRandomKey(tt.wantLenth)
is.Equal(tt.wantLenth, len(got))
})
}
t.Run("Generated keys are unique", func(t *testing.T) {
keys := make(map[string]bool)
for i := 0; i < 100; i++ {
key := generateRandomKey(8)
_, ok := keys[string(key)]
is.False(ok)
keys[string(key)] = true
}
})
}

69
api/apikey/cache.go Normal file
View File

@ -0,0 +1,69 @@
package apikey
import (
lru "github.com/hashicorp/golang-lru"
portainer "github.com/portainer/portainer/api"
)
const defaultAPIKeyCacheSize = 1024
// entry is a tuple containing the user and API key associated to an API key digest
type entry struct {
user portainer.User
apiKey portainer.APIKey
}
// apiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
// We store the api-key digest (keys) and the associated user and key-data (values) in the cache.
// This is required because HTTP requests will contain only the api-key digest in the x-api-key request header;
// digest value must be mapped to a portainer user (and respective key data) for validation.
// This cache is used to avoid multiple database queries to retrieve these user/key associated to the digest.
type apiKeyCache struct {
// cache type [string]entry cache (key: string(digest), value: user/key entry)
// note: []byte keys are not supported by golang-lru Cache
cache *lru.Cache
}
// NewAPIKeyCache creates a new cache for API keys
func NewAPIKeyCache(cacheSize int) *apiKeyCache {
cache, _ := lru.New(cacheSize)
return &apiKeyCache{cache: cache}
}
// Get returns the user/key associated to an api-key's digest
// This is required because HTTP requests will contain the digest of the API key in header,
// the digest value must be mapped to a portainer user.
func (c *apiKeyCache) Get(digest []byte) (portainer.User, portainer.APIKey, bool) {
val, ok := c.cache.Get(string(digest))
if !ok {
return portainer.User{}, portainer.APIKey{}, false
}
tuple := val.(entry)
return tuple.user, tuple.apiKey, true
}
// Set persists a user/key entry to the cache
func (c *apiKeyCache) Set(digest []byte, user portainer.User, apiKey portainer.APIKey) {
c.cache.Add(string(digest), entry{
user: user,
apiKey: apiKey,
})
}
// Delete evicts a digest's user/key entry key from the cache
func (c *apiKeyCache) Delete(digest []byte) {
c.cache.Remove(string(digest))
}
// InvalidateUserKeyCache loops through all the api-keys associated to a user and removes them from the cache
func (c *apiKeyCache) InvalidateUserKeyCache(userId portainer.UserID) bool {
present := false
for _, k := range c.cache.Keys() {
user, _, _ := c.Get([]byte(k.(string)))
if user.ID == userId {
present = c.cache.Remove(k)
}
}
return present
}

181
api/apikey/cache_test.go Normal file
View File

@ -0,0 +1,181 @@
package apikey
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_apiKeyCacheGet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
// pre-populate cache
keyCache.cache.Add(string("foo"), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string(""), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
tests := []struct {
digest []byte
found bool
}{
{
digest: []byte("foo"),
found: true,
},
{
digest: []byte(""),
found: true,
},
{
digest: []byte("bar"),
found: false,
},
}
for _, test := range tests {
t.Run(string(test.digest), func(t *testing.T) {
_, _, found := keyCache.Get(test.digest)
is.Equal(test.found, found)
})
}
}
func Test_apiKeyCacheSet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
// pre-populate cache
keyCache.Set([]byte("bar"), portainer.User{ID: 2}, portainer.APIKey{})
keyCache.Set([]byte("foo"), portainer.User{ID: 1}, portainer.APIKey{})
// overwrite existing entry
keyCache.Set([]byte("foo"), portainer.User{ID: 3}, portainer.APIKey{})
val, ok := keyCache.cache.Get(string("bar"))
is.True(ok)
tuple := val.(entry)
is.Equal(portainer.User{ID: 2}, tuple.user)
val, ok = keyCache.cache.Get(string("foo"))
is.True(ok)
tuple = val.(entry)
is.Equal(portainer.User{ID: 3}, tuple.user)
}
func Test_apiKeyCacheDelete(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
t.Run("Delete an existing entry", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.Delete([]byte("foo"))
_, ok := keyCache.cache.Get(string("foo"))
is.False(ok)
})
t.Run("Delete a non-existing entry", func(t *testing.T) {
nonPanicFunc := func() { keyCache.Delete([]byte("non-existent-key")) }
is.NotPanics(nonPanicFunc)
})
}
func Test_apiKeyCacheLRU(t *testing.T) {
is := assert.New(t)
tests := []struct {
name string
cacheLen int
key []string
foundKeys []string
evictedKeys []string
}{
{
name: "Cache length is 1, add 2 keys",
cacheLen: 1,
key: []string{"foo", "bar"},
foundKeys: []string{"bar"},
evictedKeys: []string{"foo"},
},
{
name: "Cache length is 1, add 3 keys",
cacheLen: 1,
key: []string{"foo", "bar", "baz"},
foundKeys: []string{"baz"},
evictedKeys: []string{"foo", "bar"},
},
{
name: "Cache length is 2, add 3 keys",
cacheLen: 2,
key: []string{"foo", "bar", "baz"},
foundKeys: []string{"bar", "baz"},
evictedKeys: []string{"foo"},
},
{
name: "Cache length is 2, add 4 keys",
cacheLen: 2,
key: []string{"foo", "bar", "baz", "qux"},
foundKeys: []string{"baz", "qux"},
evictedKeys: []string{"foo", "bar"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
keyCache := NewAPIKeyCache(test.cacheLen)
for _, key := range test.key {
keyCache.Set([]byte(key), portainer.User{ID: 1}, portainer.APIKey{})
}
for _, key := range test.foundKeys {
_, _, found := keyCache.Get([]byte(key))
is.True(found, "Key %s not found", key)
}
for _, key := range test.evictedKeys {
_, _, found := keyCache.Get([]byte(key))
is.False(found, "key %s should have been evicted", key)
}
})
}
}
func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
t.Run("Removes users keys from cache", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)
_, ok = keyCache.cache.Get(string("foo"))
is.False(ok)
})
t.Run("Does not affect other keys", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("bar"), entry{user: portainer.User{ID: 2}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)
ok = keyCache.InvalidateUserKeyCache(1)
is.False(ok)
_, ok = keyCache.cache.Get(string("foo"))
is.False(ok)
_, ok = keyCache.cache.Get(string("bar"))
is.True(ok)
})
}

121
api/apikey/service.go Normal file
View File

@ -0,0 +1,121 @@
package apikey
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
const portainerAPIKeyPrefix = "ptr_"
var ErrInvalidAPIKey = errors.New("Invalid API key")
type apiKeyService struct {
apiKeyRepository portainer.APIKeyRepository
userRepository portainer.UserService
cache *apiKeyCache
}
func NewAPIKeyService(apiKeyRepository portainer.APIKeyRepository, userRepository portainer.UserService) *apiKeyService {
return &apiKeyService{
apiKeyRepository: apiKeyRepository,
userRepository: userRepository,
cache: NewAPIKeyCache(defaultAPIKeyCacheSize),
}
}
// HashRaw computes a hash digest of provided raw API key.
func (a *apiKeyService) HashRaw(rawKey string) []byte {
hashDigest := sha256.Sum256([]byte(rawKey))
return hashDigest[:]
}
// GenerateApiKey generates a raw API key for a user (for one-time display).
// The generated API key is stored in the cache and database.
func (a *apiKeyService) GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error) {
randKey := generateRandomKey(32)
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
hashDigest := a.HashRaw(prefixedAPIKey)
apiKey := &portainer.APIKey{
UserID: user.ID,
Description: description,
Prefix: prefixedAPIKey[:7],
DateCreated: time.Now().Unix(),
Digest: hashDigest,
}
err := a.apiKeyRepository.CreateAPIKey(apiKey)
if err != nil {
return "", nil, errors.Wrap(err, "Unable to create API key")
}
// persist api-key to cache
a.cache.Set(apiKey.Digest, user, *apiKey)
return prefixedAPIKey, apiKey, nil
}
// GetAPIKeys returns all the API keys associated to a user.
func (a *apiKeyService) GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey, error) {
return a.apiKeyRepository.GetAPIKeysByUserID(userID)
}
// GetDigestUserAndKey returns the user and api-key associated to a specified hash digest.
// A cache lookup is performed first; if the user/api-key is not found in the cache, respective database lookups are performed.
func (a *apiKeyService) GetDigestUserAndKey(digest []byte) (portainer.User, portainer.APIKey, error) {
// get api key from cache if possible
cachedUser, cachedKey, ok := a.cache.Get(digest)
if ok {
return cachedUser, cachedKey, nil
}
apiKey, err := a.apiKeyRepository.GetAPIKeyByDigest(digest)
if err != nil {
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve API key")
}
user, err := a.userRepository.User(apiKey.UserID)
if err != nil {
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve digest user")
}
// persist api-key to cache - for quicker future lookups
a.cache.Set(apiKey.Digest, *user, *apiKey)
return *user, *apiKey, nil
}
// UpdateAPIKey updates an API key and in cache and database.
func (a *apiKeyService) UpdateAPIKey(apiKey *portainer.APIKey) error {
user, _, err := a.GetDigestUserAndKey(apiKey.Digest)
if err != nil {
return errors.Wrap(err, "Unable to retrieve API key")
}
a.cache.Set(apiKey.Digest, user, *apiKey)
return a.apiKeyRepository.UpdateAPIKey(apiKey)
}
// DeleteAPIKey deletes an API key and removes the digest/api-key entry from the cache.
func (a *apiKeyService) DeleteAPIKey(apiKeyID portainer.APIKeyID) error {
// get api-key digest to remove from cache
apiKey, err := a.apiKeyRepository.GetAPIKey(apiKeyID)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Unable to retrieve API key: %d", apiKeyID))
}
// delete the user/api-key from cache
a.cache.Delete(apiKey.Digest)
return a.apiKeyRepository.DeleteAPIKey(apiKeyID)
}
func (a *apiKeyService) InvalidateUserKeyCache(userId portainer.UserID) bool {
return a.cache.InvalidateUserKeyCache(userId)
}

289
api/apikey/service_test.go Normal file
View File

@ -0,0 +1,289 @@
package apikey
import (
"crypto/sha256"
"log"
"strings"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/stretchr/testify/assert"
)
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, teardown := bolt.MustNewTestStore(true)
defer teardown()
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, generatedDigest[:])
})
}
func Test_GetAPIKeys(t *testing.T) {
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
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, teardown := bolt.MustNewTestStore(true)
defer teardown()
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, teardown := bolt.MustNewTestStore(true)
defer teardown()
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().CreateUser(&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.Println(apiKey)
log.Println(apiKeyGot)
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, teardown := bolt.MustNewTestStore(true)
defer teardown()
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, teardown := bolt.MustNewTestStore(true)
defer teardown()
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)
})
}

View File

@ -0,0 +1,137 @@
package apikeyrepository
import (
"bytes"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "api_key"
)
// Service represents a service for managing api-key data.
type Service struct {
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
// GetAPIKeysByUserID returns a slice containing all the APIKeys a user has access to.
func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error) {
var result = make([]portainer.APIKey, 0)
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var record portainer.APIKey
err := internal.UnmarshalObject(v, &record)
if err != nil {
return err
}
if record.UserID == userID {
result = append(result, record)
}
}
return nil
})
return result, err
}
// GetAPIKeyByDigest returns the API key for the associated digest.
// Note: there is a 1-to-1 mapping of api-key and digest
func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error) {
var result portainer.APIKey
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var record portainer.APIKey
err := internal.UnmarshalObject(v, &record)
if err != nil {
return err
}
if bytes.Equal(record.Digest, digest) {
result = record
return nil
}
}
return nil
})
return &result, err
}
// CreateAPIKey creates a new APIKey object.
func (service *Service) CreateAPIKey(record *portainer.APIKey) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
record.ID = portainer.APIKeyID(id)
data, err := internal.MarshalObject(record)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(record.ID)), data)
})
}
// GetAPIKey retrieves an existing APIKey object by api key ID.
func (service *Service) GetAPIKey(keyID portainer.APIKeyID) (*portainer.APIKey, error) {
var apiKey *portainer.APIKey
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
item := bucket.Get(internal.Itob(int(keyID)))
if item == nil {
return errors.ErrObjectNotFound
}
err := internal.UnmarshalObject(item, &apiKey)
if err != nil {
return err
}
return nil
})
return apiKey, err
}
func (service *Service) UpdateAPIKey(key *portainer.APIKey) error {
identifier := internal.Itob(int(key.ID))
return internal.UpdateObject(service.connection, BucketName, identifier, key)
}
func (service *Service) DeleteAPIKey(ID portainer.APIKeyID) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
return bucket.Delete(internal.Itob(int(ID)))
})
}

View File

@ -5,6 +5,7 @@ import (
"path"
"time"
"github.com/portainer/portainer/api/bolt/apikeyrepository"
"github.com/portainer/portainer/api/bolt/helmuserrepository"
"github.com/boltdb/bolt"
@ -60,6 +61,7 @@ type Store struct {
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
APIKeyRepositoryService *apikeyrepository.Service
ScheduleService *schedule.Service
SettingsService *settings.Service
SSLSettingsService *ssl.Service

View File

@ -2,6 +2,7 @@ package bolt
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/apikeyrepository"
"github.com/portainer/portainer/api/bolt/customtemplate"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/edgegroup"
@ -155,6 +156,12 @@ func (store *Store) initServices() error {
}
store.UserService = userService
apiKeyService, err := apikeyrepository.NewService(store.connection)
if err != nil {
return err
}
store.APIKeyRepositoryService = apiKeyService
versionService, err := version.NewService(store.connection)
if err != nil {
return err
@ -231,6 +238,11 @@ func (store *Store) Role() portainer.RoleService {
return store.RoleService
}
// APIKeyRepository gives access to the api-key data management layer
func (store *Store) APIKeyRepository() portainer.APIKeyRepository {
return store.APIKeyRepositoryService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService

View File

@ -10,6 +10,7 @@ import (
"github.com/portainer/libhelm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
@ -116,6 +117,10 @@ func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, erro
return libhelm.NewHelmPackageManager(libhelm.HelmConfig{BinaryPath: assetsPath})
}
func initAPIKeyService(datastore portainer.DataStore) apikey.APIKeyService {
return apikey.NewAPIKeyService(datastore.APIKeyRepository(), datastore.User())
}
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
settings, err := dataStore.Settings().Settings()
if err != nil {
@ -457,6 +462,8 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal(err)
}
apiKeyService := initAPIKeyService(dataStore)
jwtService, err := initJWTService(dataStore)
if err != nil {
log.Fatalf("failed initializing JWT service: %v", err)
@ -620,6 +627,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
KubernetesDeployer: kubernetesDeployer,
HelmPackageManager: helmPackageManager,
CryptoService: cryptoService,
APIKeyService: apiKeyService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,

View File

@ -22,6 +22,7 @@ require (
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/golang-lru v0.5.4
github.com/joho/godotenv v1.3.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/json-iterator/go v1.1.11

View File

@ -425,6 +425,8 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=

View File

@ -11,6 +11,7 @@ import (
// @id Logout
// @summary Logout
// @description **Access policy**: authenticated
// @security ApiKeyAuth
// @security jwt
// @tags auth
// @success 204 "Success"

View File

@ -26,6 +26,7 @@ func (p *backupPayload) Validate(r *http.Request) error {
// @description Creates an archive with a system data snapshot that could be used to restore the system.
// @description **Access policy**: admin
// @tags backup
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce octet-stream

View File

@ -22,6 +22,7 @@ import (
// @description Create a custom template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @accept json,multipart/form-data
// @produce json

View File

@ -18,6 +18,7 @@ import (
// @description Remove a template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Template identifier"
// @success 204 "Success"

View File

@ -19,6 +19,7 @@ type fileResponse struct {
// @description Retrieve the content of the Stack file for the specified custom template
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "Template identifier"

View File

@ -18,6 +18,7 @@ import (
// @description Retrieve details about a template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "Template identifier"

View File

@ -17,6 +17,7 @@ import (
// @description List available custom templates.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param type query []int true "Template types" Enums(1,2,3)

View File

@ -62,6 +62,7 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
// @description Update a template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -36,6 +36,7 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
// @summary Create an EdgeGroup
// @description **Access policy**: administrator
// @tags edge_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -15,6 +15,7 @@ import (
// @summary Deletes an EdgeGroup
// @description **Access policy**: administrator
// @tags edge_groups
// @security ApiKeyAuth
// @security jwt
// @param id path int true "EdgeGroup Id"
// @success 204

View File

@ -14,6 +14,7 @@ import (
// @summary Inspects an EdgeGroup
// @description **Access policy**: administrator
// @tags edge_groups
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "EdgeGroup Id"

View File

@ -19,6 +19,7 @@ type decoratedEdgeGroup struct {
// @summary list EdgeGroups
// @description **Access policy**: administrator
// @tags edge_groups
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {array} decoratedEdgeGroup "EdgeGroups"

View File

@ -38,6 +38,7 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
// @summary Updates an EdgeGroup
// @description **Access policy**: administrator
// @tags edge_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -18,6 +18,7 @@ import (
// @summary Create an EdgeJob
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param method query string true "Creation Method" Enums(file, string)

View File

@ -15,6 +15,7 @@ import (
// @summary Delete an EdgeJob
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @param id path string true "EdgeJob Id"
// @success 204

View File

@ -18,6 +18,7 @@ type edgeJobFileResponse struct {
// @summary Fetch a file of an EdgeJob
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"

View File

@ -19,6 +19,7 @@ type edgeJobInspectResponse struct {
// @summary Inspect an EdgeJob
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"

View File

@ -11,6 +11,7 @@ import (
// @summary Fetch EdgeJobs list
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {array} portainer.EdgeJob

View File

@ -15,6 +15,7 @@ import (
// @summary Clear the log for a specifc task on an EdgeJob
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"

View File

@ -14,6 +14,7 @@ import (
// @summary Collect the log for a specifc task on an EdgeJob
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"

View File

@ -17,6 +17,7 @@ type fileResponse struct {
// @summary Fetch the log for a specifc task on an EdgeJob
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"

View File

@ -21,6 +21,7 @@ type taskContainer struct {
// @summary Fetch the list of tasks on an EdgeJob
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeJob Id"

View File

@ -32,6 +32,7 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
// @summary Update an EdgeJob
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -21,6 +21,7 @@ import (
// @summary Create an EdgeStack
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param method query string true "Creation Method" Enums(file,string,repository)

View File

@ -15,6 +15,7 @@ import (
// @summary Delete an EdgeStack
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @param id path string true "EdgeStack Id"
// @success 204

View File

@ -18,6 +18,7 @@ type stackFileResponse struct {
// @summary Fetches the stack file for an EdgeStack
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeStack Id"

View File

@ -14,6 +14,7 @@ import (
// @summary Inspect an EdgeStack
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path string true "EdgeStack Id"

View File

@ -11,6 +11,7 @@ import (
// @summary Fetches the list of EdgeStacks
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {array} portainer.EdgeStack

View File

@ -35,6 +35,7 @@ func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
// @summary Update an EdgeStack
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -19,6 +19,7 @@ type templateFileFormat struct {
// @summary Fetches the list of Edge Templates
// @description **Access policy**: administrator
// @tags edge_templates
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -36,6 +36,7 @@ func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
// @description Create a new environment(endpoint) group.
// @description **Access policy**: administrator
// @tags endpoint_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -16,6 +16,7 @@ import (
// @description Remove an environment(endpoint) group.
// @description **Access policy**: administrator
// @tags endpoint_groups
// @security ApiKeyAuth
// @security jwt
// @param id path int true "EndpointGroup identifier"
// @success 204 "Success"

View File

@ -15,6 +15,7 @@ import (
// @description Add an environment(endpoint) to an environment(endpoint) group
// @description **Access policy**: administrator
// @tags endpoint_groups
// @security ApiKeyAuth
// @security jwt
// @param id path int true "EndpointGroup identifier"
// @param endpointId path int true "Environment(Endpoint) identifier"

View File

@ -14,6 +14,7 @@ import (
// @summary Removes environment(endpoint) from an environment(endpoint) group
// @description **Access policy**: administrator
// @tags endpoint_groups
// @security ApiKeyAuth
// @security jwt
// @param id path int true "EndpointGroup identifier"
// @param endpointId path int true "Environment(Endpoint) identifier"

View File

@ -14,6 +14,7 @@ import (
// @description Retrieve details abont an environment(endpoint) group.
// @description **Access policy**: administrator
// @tags endpoint_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -15,6 +15,7 @@ import (
// @description only return authorized environment(endpoint) groups.
// @description **Access policy**: restricted
// @tags endpoint_groups
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {array} portainer.EndpointGroup "Environment(Endpoint) group"

View File

@ -32,6 +32,7 @@ func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
// @description Update an environment(endpoint) group.
// @description **Access policy**: administrator
// @tags endpoint_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -19,6 +19,7 @@ import (
// @summary De-association an edge environment(endpoint)
// @description De-association an edge environment(endpoint).
// @description **Access policy**: administrator
// @security ApiKeyAuth
// @security jwt
// @tags endpoints
// @produce json

View File

@ -152,6 +152,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
// @description Create a new environment(endpoint) that will be used to manage an environment(endpoint).
// @description **Access policy**: administrator
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @accept multipart/form-data
// @produce json

View File

@ -16,6 +16,7 @@ import (
// @description Remove an environment(endpoint).
// @description **Access policy**: administrator
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Environment(Endpoint) identifier"
// @success 204 "Success"

View File

@ -29,6 +29,7 @@ type dockerhubStatusResponse struct {
// @description get docker pull limits for a docker hub registry in the environment
// @description **Access policy**:
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "endpoint ID"

View File

@ -15,6 +15,7 @@ import (
// @description Retrieve details about an environment(endpoint).
// @description **Access policy**: restricted
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"

View File

@ -20,6 +20,7 @@ import (
// @description only return authorized environments(endpoints).
// @description **Access policy**: restricted
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param start query int false "Start searching from"

View File

@ -16,6 +16,7 @@ import (
// @summary get registry for environment
// @description **Access policy**: authenticated
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "identifier"

View File

@ -19,6 +19,7 @@ import (
// @description **Access policy**: authenticated
// @tags endpoints
// @param namespace query string false "required if kubernetes environment, will show registries by namespace"
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"

View File

@ -25,6 +25,7 @@ func (payload *registryAccessPayload) Validate(r *http.Request) error {
// @summary update registry access for environment
// @description **Access policy**: authenticated
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -39,6 +39,7 @@ func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
// @summary Update settings for an environment(endpoint)
// @description Update settings for an environment(endpoint).
// @description **Access policy**: authenticated
// @security ApiKeyAuth
// @security jwt
// @tags endpoints
// @accept json

View File

@ -16,6 +16,7 @@ import (
// @description Snapshots an environment(endpoint)
// @description **Access policy**: administrator
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Environment(Endpoint) identifier"
// @success 204 "Success"

View File

@ -15,6 +15,7 @@ import (
// @description Snapshot all environments(endpoints)
// @description **Access policy**: administrator
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @success 204 "Success"
// @failure 500 "Server Error"

View File

@ -54,6 +54,7 @@ type endpointStatusInspectResponse struct {
// @description Environment(Endpoint) for edge agent to check status of environment(endpoint)
// @description **Access policy**: restricted only to Edge environments(endpoints)
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Environment(Endpoint) identifier"
// @success 200 {object} endpointStatusInspectResponse "Success"

View File

@ -57,6 +57,7 @@ func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
// @summary Update an environment(endpoint)
// @description Update an environment(endpoint).
// @description **Access policy**: authenticated
// @security ApiKeyAuth
// @security jwt
// @tags endpoints
// @accept json

View File

@ -91,6 +91,10 @@ type Handler struct {
// @BasePath /api
// @schemes http https
// @securitydefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @securitydefinitions.apikey jwt
// @in header
// @name Authorization

View File

@ -26,17 +26,19 @@ type Handler struct {
*mux.Router
requestBouncer requestBouncer
dataStore portainer.DataStore
jwtService portainer.JWTService
kubeConfigService kubernetes.KubeConfigService
kubernetesDeployer portainer.KubernetesDeployer
helmPackageManager libhelm.HelmPackageManager
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, jwtService portainer.JWTService, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
dataStore: dataStore,
jwtService: jwtService,
kubernetesDeployer: kubernetesDeployer,
helmPackageManager: helmPackageManager,
kubeConfigService: kubeConfigService,
@ -91,7 +93,12 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
return nil, &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment on request context", err}
}
bearerToken, err := security.ExtractBearerToken(r)
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
}
bearerToken, err := handler.jwtService.GenerateToken(tokenData)
if err != nil {
return nil, &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err}
}

View File

@ -14,6 +14,7 @@ import (
// @description
// @description **Access policy**: authenticated
// @tags helm
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Environment(Endpoint) identifier"
// @param release path string true "The name of the release/application to uninstall"

View File

@ -11,6 +11,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/exec/exectest"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
"github.com/stretchr/testify/assert"
@ -30,10 +31,13 @@ func Test_helmDelete(t *testing.T) {
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "Error creating a user")
jwtService, err := jwt.NewService("1h", store)
is.NoError(err, "Error initiating jwt service")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeConfigService)
is.NotNil(h, "Handler should not fail")

View File

@ -38,6 +38,7 @@ var errChartNameInvalid = errors.New("invalid chart name. " +
// @description
// @description **Access policy**: authenticated
// @tags helm
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -16,6 +16,7 @@ import (
"github.com/portainer/portainer/api/exec/exectest"
"github.com/portainer/portainer/api/http/security"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
"github.com/stretchr/testify/assert"
)
@ -32,10 +33,13 @@ func Test_helmInstall(t *testing.T) {
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
jwtService, err := jwt.NewService("1h", store)
is.NoError(err, "Error initiating jwt service")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeConfigService)
is.NotNil(h, "Handler should not fail")

View File

@ -14,6 +14,7 @@ import (
// @description
// @description **Access policy**: authenticated
// @tags helm
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -12,6 +12,8 @@ import (
"github.com/portainer/libhelm/release"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/exec/exectest"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
"github.com/stretchr/testify/assert"
@ -20,28 +22,33 @@ import (
)
func Test_helmList(t *testing.T) {
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1})
assert.NoError(t, err, "error creating environment")
is.NoError(err, "error creating environment")
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
assert.NoError(t, err, "error creating a user")
is.NoError(err, "error creating a user")
jwtService, err := jwt.NewService("1h", store)
is.NoError(err, "Error initialising jwt service")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeConfigService)
// Install a single chart. We expect to get these values back
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
h.helmPackageManager.Install(options)
t.Run("helmList", func(t *testing.T) {
is := assert.New(t)
req := httptest.NewRequest(http.MethodGet, "/1/kubernetes/helm", nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)
req.Header.Add("Authorization", "Bearer dummytoken")
rr := httptest.NewRecorder()

View File

@ -16,6 +16,7 @@ import (
// @description **Access policy**: authenticated
// @tags helm
// @param repo query string true "Helm repository URL"
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {object} string "Success"

View File

@ -20,6 +20,7 @@ import (
// @param repo query string true "Helm repository URL"
// @param chart query string true "Chart name"
// @param command path string true "chart/values/readme"
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce text/plain

View File

@ -33,6 +33,7 @@ func (p *addHelmRepoUrlPayload) Validate(_ *http.Request) error {
// @description Create a user helm repository.
// @description **Access policy**: authenticated
// @tags helm
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
@ -93,6 +94,7 @@ func (handler *Handler) userCreateHelmRepo(w http.ResponseWriter, r *http.Reques
// @description Inspect a user helm repositories.
// @description **Access policy**: authenticated
// @tags helm
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "User identifier"

View File

@ -21,6 +21,7 @@ import (
// @description Generates kubeconfig file enabling client communication with k8s api server
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -15,6 +15,7 @@ import (
// @description Get CPU and memory limits of all nodes within k8s cluster
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -22,6 +22,7 @@ func (payload *namespacesToggleSystemPayload) Validate(r *http.Request) error {
// @summary Toggle the system state for a namespace
// @description Toggle the system state for a namespace
// @description **Access policy**: administrator or environment(endpoint) admin
// @security ApiKeyAuth
// @security jwt
// @tags kubernetes
// @accept json

View File

@ -22,6 +22,7 @@ func (payload *checkPayload) Validate(r *http.Request) error {
// @description Test LDAP connectivity using LDAP details
// @description **Access policy**: administrator
// @tags ldap
// @security ApiKeyAuth
// @security jwt
// @accept json
// @param body body checkPayload true "details"

View File

@ -30,6 +30,7 @@ type motdData struct {
// @summary fetches the message of the day
// @description **Access policy**: restricted
// @tags motd
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {object} motdResponse

View File

@ -83,6 +83,7 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
// @description Configures a registry.
// @description **Access policy**: restricted
// @tags registries
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -64,6 +64,7 @@ func (payload *registryCreatePayload) Validate(_ *http.Request) error {
// @description Create a new registry.
// @description **Access policy**: restricted
// @tags registries
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -17,6 +17,7 @@ import (
// @description Remove a registry
// @description **Access policy**: restricted
// @tags registries
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Registry identifier"
// @success 204 "Success"

View File

@ -16,6 +16,7 @@ import (
// @description Retrieve details about a registry.
// @description **Access policy**: restricted
// @tags registries
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "Registry identifier"

View File

@ -16,6 +16,7 @@ import (
// @description will only return authorized registries.
// @description **Access policy**: restricted
// @tags registries
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {array} portainer.Registry "Success"

View File

@ -39,6 +39,7 @@ func (payload *registryUpdatePayload) Validate(r *http.Request) error {
// @description Update a registry
// @description **Access policy**: restricted
// @tags registries
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -58,6 +58,7 @@ func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
// @description Create a new resource control to restrict access to a Docker resource.
// @description **Access policy**: administrator
// @tags resource_controls
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -15,6 +15,7 @@ import (
// @description Remove a resource control.
// @description **Access policy**: administrator
// @tags resource_controls
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Resource control identifier"
// @success 204 "Success"

View File

@ -40,6 +40,7 @@ func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
// @description Update a resource control
// @description **Access policy**: authenticated
// @tags resource_controls
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -12,6 +12,7 @@ import (
// @description List all roles available for use
// @description **Access policy**: administrator
// @tags roles
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {array} portainer.Role "Success"

View File

@ -12,6 +12,7 @@ import (
// @description Retrieve Portainer settings.
// @description **Access policy**: administrator
// @tags settings
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {object} portainer.Settings "Success"

View File

@ -78,6 +78,7 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
// @description Update Portainer settings.
// @description **Access policy**: administrator
// @tags settings
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -12,6 +12,7 @@ import (
// @description Retrieve the ssl settings.
// @description **Access policy**: administrator
// @tags ssl
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {object} portainer.SSLSettings "Success"

View File

@ -28,6 +28,7 @@ func (payload *sslUpdatePayload) Validate(r *http.Request) error {
// @description Update the ssl settings.
// @description **Access policy**: administrator
// @tags ssl
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@ -18,6 +18,7 @@ import (
// @summary Associate an orphaned stack to a new environment(endpoint)
// @description **Access policy**: administrator
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "Stack identifier"

View File

@ -35,6 +35,7 @@ func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
// @description Deploy a new stack into a Docker environment(endpoint) specified via the environment(endpoint) identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @accept json,multipart/form-data
// @produce json

View File

@ -25,6 +25,7 @@ import (
// @description Remove a stack.
// @description **Access policy**: restricted
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Stack identifier"
// @param external query boolean false "Set to true to delete an external stack. Only external Swarm stacks are supported"

View File

@ -23,6 +23,7 @@ type stackFileResponse struct {
// @description Get Stack file content.
// @description **Access policy**: restricted
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "Stack identifier"

View File

@ -19,6 +19,7 @@ import (
// @description Retrieve details about a stack.
// @description **Access policy**: restricted
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "Stack identifier"

Some files were not shown because too many files have changed in this diff Show More