diff --git a/api/dataservices/endpoint/endpoint.go b/api/dataservices/endpoint/endpoint.go index 998d43967..51fa57f6c 100644 --- a/api/dataservices/endpoint/endpoint.go +++ b/api/dataservices/endpoint/endpoint.go @@ -34,7 +34,7 @@ func NewService(connection portainer.Connection) (*Service, error) { idxEdgeID: make(map[string]portainer.EndpointID), } - es, err := s.Endpoints() + es, err := s.endpoints() if err != nil { return nil, err } @@ -89,8 +89,7 @@ func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error { }) } -// Endpoints return an array containing all the environments(endpoints). -func (service *Service) Endpoints() ([]portainer.Endpoint, error) { +func (service *Service) endpoints() ([]portainer.Endpoint, error) { var endpoints []portainer.Endpoint var err error @@ -99,8 +98,14 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) { return err }) + return endpoints, err +} + +// Endpoints return an array containing all the environments(endpoints). +func (service *Service) Endpoints() ([]portainer.Endpoint, error) { + endpoints, err := service.endpoints() if err != nil { - return endpoints, err + return nil, err } for i, e := range endpoints { diff --git a/api/http/handler/endpointedge/endpoint_edgestatus_inspect.go b/api/http/handler/endpointedge/endpoint_edgestatus_inspect.go index 23d747365..0c541ae62 100644 --- a/api/http/handler/endpointedge/endpoint_edgestatus_inspect.go +++ b/api/http/handler/endpointedge/endpoint_edgestatus_inspect.go @@ -117,6 +117,11 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http return httperror.InternalServerError("Unable to Unable to persist environment changes inside the database", err) } + err = handler.requestBouncer.TrustedEdgeEnvironmentAccess(endpoint) + if err != nil { + return httperror.Forbidden("Permission denied to access environment", err) + } + checkinInterval := endpoint.EdgeCheckinInterval if endpoint.EdgeCheckinInterval == 0 { settings, err := handler.DataStore.Settings().Settings() diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index 8ab1a35cf..fd3cf804b 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -45,6 +45,7 @@ const ( // @param agentVersions query []string false "will return only environments with on of these agent versions" // @param edgeAsync query bool false "if exists true show only edge async agents, false show only standard edge agents. if missing, will show both types (relevant only for edge agents)" // @param edgeDeviceUntrusted query bool false "if true, show only untrusted edge agents, if false show only trusted edge agents (relevant only for edge agents)" +// @param edgeCheckInPassedSeconds query number false "if bigger then zero, show only edge agents that checked-in in the last provided seconds (relevant only for edge agents)" // @param name query string false "will return only environments(endpoints) with this name" // @success 200 {array} portainer.Endpoint "Endpoints" // @failure 500 "Server error" diff --git a/api/http/handler/endpoints/filter.go b/api/http/handler/endpoints/filter.go index 248ff2f44..7a05867e6 100644 --- a/api/http/handler/endpoints/filter.go +++ b/api/http/handler/endpoints/filter.go @@ -23,11 +23,12 @@ type EnvironmentsQuery struct { groupIds []portainer.EndpointGroupID status []portainer.EndpointStatus // if edgeAsync not nil, will filter edge endpoints based on this value - edgeAsync *bool - edgeDeviceUntrusted bool - excludeSnapshots bool - name string - agentVersions []string + edgeAsync *bool + edgeDeviceUntrusted bool + excludeSnapshots bool + name string + agentVersions []string + edgeCheckInPassedSeconds int } func parseQuery(r *http.Request) (EnvironmentsQuery, error) { @@ -77,19 +78,22 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) { excludeSnapshots, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshots", true) + edgeCheckInPassedSeconds, _ := request.RetrieveNumericQueryParameter(r, "edgeCheckInPassedSeconds", true) + return EnvironmentsQuery{ - search: search, - types: endpointTypes, - tagIds: tagIDs, - endpointIds: endpointIDs, - tagsPartialMatch: tagsPartialMatch, - groupIds: groupIDs, - status: status, - edgeAsync: edgeAsync, - edgeDeviceUntrusted: edgeDeviceUntrusted, - excludeSnapshots: excludeSnapshots, - name: name, - agentVersions: agentVersions, + search: search, + types: endpointTypes, + tagIds: tagIDs, + endpointIds: endpointIDs, + tagsPartialMatch: tagsPartialMatch, + groupIds: groupIDs, + status: status, + edgeAsync: edgeAsync, + edgeDeviceUntrusted: edgeDeviceUntrusted, + excludeSnapshots: excludeSnapshots, + name: name, + agentVersions: agentVersions, + edgeCheckInPassedSeconds: edgeCheckInPassedSeconds, }, nil } @@ -128,6 +132,22 @@ func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.End return endpoint.UserTrusted == !query.edgeDeviceUntrusted }) + if query.edgeCheckInPassedSeconds > 0 { + filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool { + // ignore non-edge endpoints + if !endpointutils.IsEdgeEndpoint(&endpoint) { + return true + } + + // filter out endpoints that have never checked in + if endpoint.LastCheckInDate == 0 { + return false + } + + return time.Now().Unix()-endpoint.LastCheckInDate < int64(query.edgeCheckInPassedSeconds) + }) + } + if len(query.status) > 0 { filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, query.status, settings) } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index ed80b229d..9855c8407 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -1,12 +1,11 @@ package security import ( - "errors" - "fmt" "net/http" "strings" "time" + "github.com/pkg/errors" httperror "github.com/portainer/libhttp/error" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/apikey" @@ -147,13 +146,19 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request, return errors.New("invalid Edge identifier") } - if endpoint.LastCheckInDate > 0 || endpoint.UserTrusted { + return nil +} + +// TrustedEdgeEnvironmentAccess defines a security check for Edge environments, checks if +// the request is coming from a trusted Edge environment +func (bouncer *RequestBouncer) TrustedEdgeEnvironmentAccess(endpoint *portainer.Endpoint) error { + if endpoint.UserTrusted { return nil } settings, err := bouncer.dataStore.Settings().Settings() if err != nil { - return fmt.Errorf("could not retrieve the settings: %w", err) + return errors.WithMessage(err, "could not retrieve the settings") } if !settings.TrustOnFirstConnect { diff --git a/app/react/edge/edge-devices/WaitingRoomView/Datatable/Filter.tsx b/app/react/edge/edge-devices/WaitingRoomView/Datatable/Filter.tsx index 1ba91a723..2655858ca 100644 --- a/app/react/edge/edge-devices/WaitingRoomView/Datatable/Filter.tsx +++ b/app/react/edge/edge-devices/WaitingRoomView/Datatable/Filter.tsx @@ -3,8 +3,18 @@ import { useGroups } from '@/react/portainer/environments/environment-groups/que import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups'; import { useTags } from '@/portainer/tags/queries'; +import { PortainerSelect } from '@@/form-components/PortainerSelect'; + import { useFilterStore } from './filter-store'; +const checkInOptions = [ + { value: 0, label: 'Show all time' }, + { value: 60 * 60, label: 'Show past hour' }, + { value: 60 * 60 * 24, label: 'Show past day' }, + { value: 60 * 60 * 24 * 7, label: 'Show past week' }, + { value: 60 * 60 * 24 * 14, label: 'Show past 14 days' }, +]; + export function Filter() { const edgeGroupsQuery = useEdgeGroups(); const groupsQuery = useGroups(); @@ -45,6 +55,14 @@ export function Filter() { value: g.ID, }))} /> + +
+