diff --git a/api/connection.go b/api/connection.go index 710b978da..14e2dc1ca 100644 --- a/api/connection.go +++ b/api/connection.go @@ -6,8 +6,10 @@ import ( type ReadTransaction interface { GetObject(bucketName string, key []byte, object any) error + GetRawBytes(bucketName string, key []byte) ([]byte, error) GetAll(bucketName string, obj any, append func(o any) (any, error)) error GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, append func(o any) (any, error)) error + KeyExists(bucketName string, key []byte) (bool, error) } type Transaction interface { diff --git a/api/database/boltdb/db.go b/api/database/boltdb/db.go index cef93b345..a0db7f4e0 100644 --- a/api/database/boltdb/db.go +++ b/api/database/boltdb/db.go @@ -244,6 +244,32 @@ func (connection *DbConnection) GetObject(bucketName string, key []byte, object }) } +func (connection *DbConnection) GetRawBytes(bucketName string, key []byte) ([]byte, error) { + var value []byte + + err := connection.ViewTx(func(tx portainer.Transaction) error { + var err error + value, err = tx.GetRawBytes(bucketName, key) + + return err + }) + + return value, err +} + +func (connection *DbConnection) KeyExists(bucketName string, key []byte) (bool, error) { + var exists bool + + err := connection.ViewTx(func(tx portainer.Transaction) error { + var err error + exists, err = tx.KeyExists(bucketName, key) + + return err + }) + + return exists, err +} + func (connection *DbConnection) getEncryptionKey() []byte { if !connection.isEncrypted { return nil diff --git a/api/database/boltdb/tx.go b/api/database/boltdb/tx.go index 5de5d5333..2e45ac7b9 100644 --- a/api/database/boltdb/tx.go +++ b/api/database/boltdb/tx.go @@ -6,6 +6,7 @@ import ( dserrors "github.com/portainer/portainer/api/dataservices/errors" + "github.com/pkg/errors" "github.com/rs/zerolog/log" bolt "go.etcd.io/bbolt" ) @@ -31,6 +32,33 @@ func (tx *DbTransaction) GetObject(bucketName string, key []byte, object any) er return tx.conn.UnmarshalObject(value, object) } +func (tx *DbTransaction) GetRawBytes(bucketName string, key []byte) ([]byte, error) { + bucket := tx.tx.Bucket([]byte(bucketName)) + + value := bucket.Get(key) + if value == nil { + return nil, fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key)) + } + + if tx.conn.getEncryptionKey() != nil { + var err error + + if value, err = decrypt(value, tx.conn.getEncryptionKey()); err != nil { + return value, errors.Wrap(err, "Failed decrypting object") + } + } + + return value, nil +} + +func (tx *DbTransaction) KeyExists(bucketName string, key []byte) (bool, error) { + bucket := tx.tx.Bucket([]byte(bucketName)) + + value := bucket.Get(key) + + return value != nil, nil +} + func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object any) error { data, err := tx.conn.MarshalObject(object) if err != nil { diff --git a/api/dataservices/base.go b/api/dataservices/base.go index 9b1a42a53..04af70b02 100644 --- a/api/dataservices/base.go +++ b/api/dataservices/base.go @@ -9,6 +9,7 @@ import ( type BaseCRUD[T any, I constraints.Integer] interface { Create(element *T) error Read(ID I) (*T, error) + Exists(ID I) (bool, error) ReadAll() ([]T, error) Update(ID I, element *T) error Delete(ID I) error @@ -42,6 +43,19 @@ func (service BaseDataService[T, I]) Read(ID I) (*T, error) { }) } +func (service BaseDataService[T, I]) Exists(ID I) (bool, error) { + var exists bool + + err := service.Connection.ViewTx(func(tx portainer.Transaction) error { + var err error + exists, err = service.Tx(tx).Exists(ID) + + return err + }) + + return exists, err +} + func (service BaseDataService[T, I]) ReadAll() ([]T, error) { var collection = make([]T, 0) diff --git a/api/dataservices/base_tx.go b/api/dataservices/base_tx.go index db1e702cb..d9915b64c 100644 --- a/api/dataservices/base_tx.go +++ b/api/dataservices/base_tx.go @@ -28,6 +28,12 @@ func (service BaseDataServiceTx[T, I]) Read(ID I) (*T, error) { return &element, nil } +func (service BaseDataServiceTx[T, I]) Exists(ID I) (bool, error) { + identifier := service.Connection.ConvertToKey(int(ID)) + + return service.Tx.KeyExists(service.Bucket, identifier) +} + func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) { var collection = make([]T, 0) diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index dc95e15b0..7ea557910 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -105,14 +105,16 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht for idx := range paginatedEndpoints { hideFields(&paginatedEndpoints[idx]) + paginatedEndpoints[idx].ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion() if paginatedEndpoints[idx].EdgeCheckinInterval == 0 { paginatedEndpoints[idx].EdgeCheckinInterval = settings.EdgeAgentCheckinInterval } + endpointutils.UpdateEdgeEndpointHeartbeat(&paginatedEndpoints[idx], settings) + if !query.excludeSnapshots { - err = handler.SnapshotService.FillSnapshotData(&paginatedEndpoints[idx]) - if err != nil { + if err := handler.SnapshotService.FillSnapshotData(&paginatedEndpoints[idx]); err != nil { return httperror.InternalServerError("Unable to add snapshot data", err) } } @@ -120,6 +122,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount)) w.Header().Set("X-Total-Available", strconv.Itoa(totalAvailableEndpoints)) + return response.JSON(w, paginatedEndpoints) } @@ -130,18 +133,8 @@ func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []porta endpointCount := len(endpoints) - if start < 0 { - start = 0 - } - - if start > endpointCount { - start = endpointCount - } - - end := start + limit - if end > endpointCount { - end = endpointCount - } + start = min(max(start, 0), endpointCount) + end := min(start+limit, endpointCount) return endpoints[start:end] } @@ -151,8 +144,10 @@ func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.Endp for _, group := range groups { if group.ID == groupID { endpointGroup = group + break } } + return endpointGroup } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 00f69e328..eb240692d 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -243,8 +243,7 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, return } - _, err = bouncer.dataStore.User().Read(tokenData.ID) - if bouncer.dataStore.IsErrObjectNotFound(err) { + if ok, err := bouncer.dataStore.User().Exists(tokenData.ID); !ok { httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized) return } else if err != nil { @@ -322,9 +321,8 @@ func (bouncer *RequestBouncer) mwAuthenticateFirst(tokenLookups []tokenLookup, n return } - user, _ := bouncer.dataStore.User().Read(token.ID) - if user == nil { - httperror.WriteError(w, http.StatusUnauthorized, "An authorization token is invalid", httperrors.ErrUnauthorized) + if ok, _ := bouncer.dataStore.User().Exists(token.ID); !ok { + httperror.WriteError(w, http.StatusUnauthorized, "The authorization token is invalid", httperrors.ErrUnauthorized) return } diff --git a/api/internal/testhelpers/datastore.go b/api/internal/testhelpers/datastore.go index 19d4df762..9a16ddfd2 100644 --- a/api/internal/testhelpers/datastore.go +++ b/api/internal/testhelpers/datastore.go @@ -153,6 +153,7 @@ func (s *stubUserService) UsersByRole(role portainer.UserRole) ([]portainer.User func (s *stubUserService) Create(user *portainer.User) error { return nil } func (s *stubUserService) Update(ID portainer.UserID, user *portainer.User) error { return nil } func (s *stubUserService) Delete(ID portainer.UserID) error { return nil } +func (s *stubUserService) Exists(ID portainer.UserID) (bool, error) { return false, nil } // WithUsers testDatastore option that will instruct testDatastore to return provided users func WithUsers(us []portainer.User) datastoreOption { @@ -188,6 +189,9 @@ func (s *stubEdgeJobService) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFun } func (s *stubEdgeJobService) Delete(ID portainer.EdgeJobID) error { return nil } func (s *stubEdgeJobService) GetNextIdentifier() int { return 0 } +func (s *stubEdgeJobService) Exists(ID portainer.EdgeJobID) (bool, error) { + return false, nil +} // WithEdgeJobs option will instruct testDatastore to return provided jobs func WithEdgeJobs(js []portainer.EdgeJob) datastoreOption { @@ -452,6 +456,10 @@ func (s *stubStacksService) GetNextIdentifier() int { return len(s.stacks) } +func (s *stubStacksService) Exists(ID portainer.StackID) (bool, error) { + return false, nil +} + // WithStacks option will instruct testDatastore to return provided stacks func WithStacks(stacks []portainer.Stack) datastoreOption { return func(d *testDatastore) {