feat(waiting-room): show and filter by check in [EE-5186] (#8701)

pull/8819/head
Chaim Lev-Ari 2023-04-27 09:22:05 +07:00 committed by GitHub
parent b5771df6a8
commit 4b9c857d85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 108 additions and 25 deletions

View File

@ -34,7 +34,7 @@ func NewService(connection portainer.Connection) (*Service, error) {
idxEdgeID: make(map[string]portainer.EndpointID), idxEdgeID: make(map[string]portainer.EndpointID),
} }
es, err := s.Endpoints() es, err := s.endpoints()
if err != nil { if err != nil {
return nil, err 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 endpoints []portainer.Endpoint
var err error var err error
@ -99,10 +98,16 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
return err return err
}) })
if err != nil {
return endpoints, 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 nil, err
}
for i, e := range endpoints { for i, e := range endpoints {
t, _ := service.Heartbeat(e.ID) t, _ := service.Heartbeat(e.ID)
endpoints[i].LastCheckInDate = t endpoints[i].LastCheckInDate = t

View File

@ -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) 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 checkinInterval := endpoint.EdgeCheckinInterval
if endpoint.EdgeCheckinInterval == 0 { if endpoint.EdgeCheckinInterval == 0 {
settings, err := handler.DataStore.Settings().Settings() settings, err := handler.DataStore.Settings().Settings()

View File

@ -45,6 +45,7 @@ const (
// @param agentVersions query []string false "will return only environments with on of these agent versions" // @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 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 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" // @param name query string false "will return only environments(endpoints) with this name"
// @success 200 {array} portainer.Endpoint "Endpoints" // @success 200 {array} portainer.Endpoint "Endpoints"
// @failure 500 "Server error" // @failure 500 "Server error"

View File

@ -28,6 +28,7 @@ type EnvironmentsQuery struct {
excludeSnapshots bool excludeSnapshots bool
name string name string
agentVersions []string agentVersions []string
edgeCheckInPassedSeconds int
} }
func parseQuery(r *http.Request) (EnvironmentsQuery, error) { func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
@ -77,6 +78,8 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
excludeSnapshots, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshots", true) excludeSnapshots, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshots", true)
edgeCheckInPassedSeconds, _ := request.RetrieveNumericQueryParameter(r, "edgeCheckInPassedSeconds", true)
return EnvironmentsQuery{ return EnvironmentsQuery{
search: search, search: search,
types: endpointTypes, types: endpointTypes,
@ -90,6 +93,7 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
excludeSnapshots: excludeSnapshots, excludeSnapshots: excludeSnapshots,
name: name, name: name,
agentVersions: agentVersions, agentVersions: agentVersions,
edgeCheckInPassedSeconds: edgeCheckInPassedSeconds,
}, nil }, nil
} }
@ -128,6 +132,22 @@ func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.End
return endpoint.UserTrusted == !query.edgeDeviceUntrusted 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 { if len(query.status) > 0 {
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, query.status, settings) filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, query.status, settings)
} }

View File

@ -1,12 +1,11 @@
package security package security
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey" "github.com/portainer/portainer/api/apikey"
@ -147,13 +146,19 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request,
return errors.New("invalid Edge identifier") 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 return nil
} }
settings, err := bouncer.dataStore.Settings().Settings() settings, err := bouncer.dataStore.Settings().Settings()
if err != nil { 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 { if !settings.TrustOnFirstConnect {

View File

@ -3,8 +3,18 @@ import { useGroups } from '@/react/portainer/environments/environment-groups/que
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups'; import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
import { useTags } from '@/portainer/tags/queries'; import { useTags } from '@/portainer/tags/queries';
import { PortainerSelect } from '@@/form-components/PortainerSelect';
import { useFilterStore } from './filter-store'; 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() { export function Filter() {
const edgeGroupsQuery = useEdgeGroups(); const edgeGroupsQuery = useEdgeGroups();
const groupsQuery = useGroups(); const groupsQuery = useGroups();
@ -45,6 +55,14 @@ export function Filter() {
value: g.ID, value: g.ID,
}))} }))}
/> />
<div className="ml-auto" />
<PortainerSelect
onChange={(f) => filterStore.setCheckIn(f || 0)}
value={filterStore.checkIn}
options={checkInOptions}
bindToBody
/>
</div> </div>
); );
} }

View File

@ -1,3 +1,4 @@
import moment from 'moment';
import { CellProps, Column } from 'react-table'; import { CellProps, Column } from 'react-table';
import { WaitingRoomEnvironment } from '../types'; import { WaitingRoomEnvironment } from '../types';
@ -52,4 +53,24 @@ export const columns: readonly Column<WaitingRoomEnvironment>[] = [
canHide: false, canHide: false,
sortType: 'string', sortType: 'string',
}, },
{
Header: 'Last Check-in',
accessor: 'LastCheckInDate',
Cell: LastCheckinDateCell,
id: 'last-check-in',
disableFilters: true,
Filter: () => null,
canHide: false,
sortType: 'string',
},
] as const; ] as const;
function LastCheckinDateCell({
value,
}: CellProps<WaitingRoomEnvironment, number>) {
if (!value) {
return '-';
}
return moment(value * 1000).fromNow();
}

View File

@ -10,6 +10,8 @@ interface TableFiltersStore {
setEdgeGroups(value: number[]): void; setEdgeGroups(value: number[]): void;
tags: number[]; tags: number[];
setTags(value: number[]): void; setTags(value: number[]): void;
checkIn: number;
setCheckIn(value: number): void;
} }
export const useFilterStore = createStore<TableFiltersStore>()( export const useFilterStore = createStore<TableFiltersStore>()(
@ -27,6 +29,10 @@ export const useFilterStore = createStore<TableFiltersStore>()(
setTags(tags: number[]) { setTags(tags: number[]) {
set({ tags }); set({ tags });
}, },
checkIn: 0,
setCheckIn(checkIn: number) {
set({ checkIn });
},
}), }),
{ {
name: keyBuilder('edge-devices-meta-filters'), name: keyBuilder('edge-devices-meta-filters'),

View File

@ -30,6 +30,7 @@ export function useEnvironments() {
tagIds: filterStore.tags.length ? filterStore.tags : undefined, tagIds: filterStore.tags.length ? filterStore.tags : undefined,
groupIds: filterStore.groups.length ? filterStore.groups : undefined, groupIds: filterStore.groups.length ? filterStore.groups : undefined,
endpointIds: filterByEnvironmentsIds, endpointIds: filterByEnvironmentsIds,
edgeCheckInPassedSeconds: filterStore.checkIn,
}); });
const groupsQuery = useGroups({ const groupsQuery = useGroups({

View File

@ -29,6 +29,7 @@ export interface EnvironmentsQueryParams {
name?: string; name?: string;
agentVersions?: string[]; agentVersions?: string[];
updateInformation?: boolean; updateInformation?: boolean;
edgeCheckInPassedSeconds?: number;
} }
export interface GetEnvironmentsOptions { export interface GetEnvironmentsOptions {