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, }))} /> + +
+ filterStore.setCheckIn(f || 0)} + value={filterStore.checkIn} + options={checkInOptions} + bindToBody + />
); } diff --git a/app/react/edge/edge-devices/WaitingRoomView/Datatable/columns.ts b/app/react/edge/edge-devices/WaitingRoomView/Datatable/columns.ts index 0c2722761..f2cf21f4d 100644 --- a/app/react/edge/edge-devices/WaitingRoomView/Datatable/columns.ts +++ b/app/react/edge/edge-devices/WaitingRoomView/Datatable/columns.ts @@ -1,3 +1,4 @@ +import moment from 'moment'; import { CellProps, Column } from 'react-table'; import { WaitingRoomEnvironment } from '../types'; @@ -52,4 +53,24 @@ export const columns: readonly Column[] = [ canHide: false, sortType: 'string', }, + { + Header: 'Last Check-in', + accessor: 'LastCheckInDate', + Cell: LastCheckinDateCell, + id: 'last-check-in', + disableFilters: true, + Filter: () => null, + canHide: false, + sortType: 'string', + }, ] as const; + +function LastCheckinDateCell({ + value, +}: CellProps) { + if (!value) { + return '-'; + } + + return moment(value * 1000).fromNow(); +} diff --git a/app/react/edge/edge-devices/WaitingRoomView/Datatable/filter-store.ts b/app/react/edge/edge-devices/WaitingRoomView/Datatable/filter-store.ts index feeeae6d7..e692f3aa7 100644 --- a/app/react/edge/edge-devices/WaitingRoomView/Datatable/filter-store.ts +++ b/app/react/edge/edge-devices/WaitingRoomView/Datatable/filter-store.ts @@ -10,6 +10,8 @@ interface TableFiltersStore { setEdgeGroups(value: number[]): void; tags: number[]; setTags(value: number[]): void; + checkIn: number; + setCheckIn(value: number): void; } export const useFilterStore = createStore()( @@ -27,6 +29,10 @@ export const useFilterStore = createStore()( setTags(tags: number[]) { set({ tags }); }, + checkIn: 0, + setCheckIn(checkIn: number) { + set({ checkIn }); + }, }), { name: keyBuilder('edge-devices-meta-filters'), diff --git a/app/react/edge/edge-devices/WaitingRoomView/Datatable/useEnvironments.ts b/app/react/edge/edge-devices/WaitingRoomView/Datatable/useEnvironments.ts index 6d899fc2b..1ee4fca02 100644 --- a/app/react/edge/edge-devices/WaitingRoomView/Datatable/useEnvironments.ts +++ b/app/react/edge/edge-devices/WaitingRoomView/Datatable/useEnvironments.ts @@ -30,6 +30,7 @@ export function useEnvironments() { tagIds: filterStore.tags.length ? filterStore.tags : undefined, groupIds: filterStore.groups.length ? filterStore.groups : undefined, endpointIds: filterByEnvironmentsIds, + edgeCheckInPassedSeconds: filterStore.checkIn, }); const groupsQuery = useGroups({ diff --git a/app/react/portainer/environments/environment.service/index.ts b/app/react/portainer/environments/environment.service/index.ts index 4c6763a55..03d5a25e9 100644 --- a/app/react/portainer/environments/environment.service/index.ts +++ b/app/react/portainer/environments/environment.service/index.ts @@ -29,6 +29,7 @@ export interface EnvironmentsQueryParams { name?: string; agentVersions?: string[]; updateInformation?: boolean; + edgeCheckInPassedSeconds?: number; } export interface GetEnvironmentsOptions {