mirror of https://github.com/portainer/portainer
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
parent
120584909c
commit
69c17986d9
|
@ -150,6 +150,7 @@
|
|||
"// @description ",
|
||||
"// @description **Access policy**: ",
|
||||
"// @tags ",
|
||||
"// @security ApiKeyAuth",
|
||||
"// @security jwt",
|
||||
"// @accept json",
|
||||
"// @produce json",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)))
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
// @id Logout
|
||||
// @summary Logout
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags auth
|
||||
// @success 204 "Success"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
// @description
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue