diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b9cbbb92..fc6cde8c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,16 +15,16 @@ jobs: EOF - name: Check out repository code - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Install Golang - uses: actions/setup-go@v5.0.1 + uses: actions/setup-go@v5.0.2 with: go-version-file: go.mod check-latest: true cache: true - - uses: actions/setup-node@v4.0.2 + - uses: actions/setup-node@v4.0.3 with: node-version: '18' @@ -39,7 +39,7 @@ jobs: run: go mod download - name: golangci-lint - uses: golangci/golangci-lint-action@v6.0.1 + uses: golangci/golangci-lint-action@v6.1.0 with: version: v1.57.2 args: --timeout 7m diff --git a/.github/workflows/master-build.yaml b/.github/workflows/master-build.yaml index b70daa04..7305bd8a 100644 --- a/.github/workflows/master-build.yaml +++ b/.github/workflows/master-build.yaml @@ -19,10 +19,10 @@ jobs: EOF - name: Check out repository code - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Log in to GHCR.io - uses: docker/login-action@v3.2.0 + uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -60,9 +60,9 @@ jobs: EOF - name: Check out repository code - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Log in to GHCR.io - uses: docker/login-action@v3.2.0 + uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d23bfb18..3a8e110d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,12 @@ ## Contributor Guidelines and Governance -Please see [CONTRIBUTING](https://github.com/spiffe/spiffe/blob/main/CONTRIBUTING.md) and [GOVERNANCE](https://github.com/spiffe/spiffe/blob/main/GOVERNANCE.md) from the SPIFFE project. +Please see [CONTRIBUTING](https://github.com/spiffe/spiffe/blob/main/CONTRIBUTING.md) and [GOVERNANCE](https://github.com/spiffe/spiffe/blob/main/GOVERNANCE.md) from the SPIFFE project for community guidelines. + +> [!IMPORTANT] +> Before opening a new issue, search for any existing issues [here](https://github.com/spiffe/tornjak/issues) to avoid duplication. + +If you're new to this project, we recommend you join us on [Slack](https://spiffe.slack.com/archives/C024JTTK58T) for discussion of potential new features. ## Pre-built images @@ -26,20 +31,54 @@ In order to build, we require the following installations: ## Building Executables and Images -Building Tornjak manually can be done with the Makefile. Notable make targets follow: +Building Tornjak manually can be done with the Makefile. Below is a list of local executable builds: - `make bin/tornjak-backend`: makes the Go executable of the Tornjak backend - `make bin/tornjak-manager`: makes the Go executable of the Tornjak manager - `make frontend-local-build`: makes the optimized ReactJS app locally for the Tornjak frontend. Uses environment variable configuration as in tornjak-frontend/.env + +And below is a list of container image builds: - `make image-tornjak-backend`: containerizes Go executable of the Tornjak backend - `make image-tornjak-manager`:containerizes Go executable of the Tornjak manager - `make image-tornjak-frontend`: containerizes React JS app for the Tornjak frontend -- `make image-tornjak`: containerizes Tornjak backend with Tornjak frontend For usage instructions of the containers, please see our [USAGE document](./USAGE.md) to get started. ## Development -We welcome all development attempst and contributions from the community. The easiest place to start is by reviewing our code architecture diagrams available in our [api documentation](./docs/tornjak-ui-api-documentation.md#11-overview). +We welcome all development attempts and contributions from the community. The easiest place to start is by reviewing our code architecture diagrams available in our [api documentation](./docs/tornjak-ui-api-documentation.md#11-overview). + +## Opening a pull request + +1. Fork the tornjak repo +2. Ensure your branch is based on the latest commit in `dev` +3. Commit changes to your fork. Make sure your commit messages contain a `Signed-off-by: ` line (see `git-commit --signoff`) to certify the [DCO](/DCO) +4. Test your PR locally and ensure all tests in Github actions pass +5. Open a [pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) + against the upstream `dev` branch + +> [!IMPORTANT] +> Please make sure you open all PRs against the `dev` branch + +> [!IMPORTANT] +> For any new feature design, or feature level changes, please create an issue first, then submit a PR with design details before code implementation. + +## After your pull request is submitted + +At least one maintainer must approve the pull request. + +Once your pull request is submitted, it's your responsibility to: + +* Respond to reviewer's feedback +* Keep it merge-ready at all times until it has been approved and actually merged + +Following approval, the pull request will be merged by the last maintainer to approve the request. + +#### Third-party code + +When third-party code must be included, all licenses must be preserved. This includes modified +third-party code and excerpts, as well. + +Thank you for contributing to Tornjak! ## Local testing diff --git a/Dockerfile.backend-container b/Dockerfile.backend-container index be552152..e76fac0c 100644 --- a/Dockerfile.backend-container +++ b/Dockerfile.backend-container @@ -18,10 +18,10 @@ RUN if [ "$TARGETARCH" = "arm64" ]; then CC=aarch64-alpine-linux-musl; fi && \ go build --tags 'sqlite_json' -mod=vendor -ldflags '-s -w -linkmode external -extldflags "-static"' -o bin/tornjak-backend ./cmd/agent/main.go FROM alpine AS runtime -RUN mkdir -p /opt/spire +RUN mkdir -p /opt/tornjak -WORKDIR /opt/spire -ENTRYPOINT ["/opt/spire/run_backend.sh"] +WORKDIR /opt/tornjak +ENTRYPOINT ["/opt/tornjak/run_backend.sh"] # Add init COPY scripts/run_backend.sh run_backend.sh diff --git a/Dockerfile.backend-container.ubi b/Dockerfile.backend-container.ubi index 1901bcec..eeb4102a 100644 --- a/Dockerfile.backend-container.ubi +++ b/Dockerfile.backend-container.ubi @@ -18,10 +18,10 @@ RUN if [ "$TARGETARCH" = "arm64" ]; then CC=aarch64-alpine-linux-musl; fi && \ go build --tags 'sqlite_json' -mod=vendor -ldflags '-s -w -linkmode external -extldflags "-static"' -o bin/tornjak-backend ./cmd/agent/main.go FROM registry.access.redhat.com/ubi8-micro:latest AS runtime -RUN mkdir -p /opt/spire +RUN mkdir -p /opt/tornjak -WORKDIR /opt/spire -ENTRYPOINT ["/opt/spire/run_backend.sh"] +WORKDIR /opt/tornjak +ENTRYPOINT ["/opt/tornjak/run_backend.sh"] # Add init COPY scripts/run_backend.sh run_backend.sh diff --git a/USAGE.md b/USAGE.md index 96efdcfa..af317562 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,14 +1,14 @@ # Usage -We publish four container images currently: +We publish and support three container images currently: - [Tornjak Backend](https://github.com/spiffe/tornjak/pkgs/container/tornjak-backend): This image can be deployed as a sidecar with any SPIRE server. - [Tornjak Manager](https://github.com/spiffe/tornjak/pkgs/container/tornjak-manager): A container that runs this image exposes a port to register multiple Tornjak backends and forward typical commands to multiple Tornjak backends from one API. - [Tornjak Frontend](https://github.com/spiffe/tornjak/pkgs/container/tornjak-frontend): This image is typically deployed after the Tornjak Backend or Manager are deployed, as it requires a URL to connect directly to the Tornjak backend API. -- [Tornjak](https://github.com/spiffe/tornjak/pkgs/container/tornjak): This image containing both Tornjak Backend and Frontend components can deployed as a sidecar alongside a SPIRE Server container -NOTE: Previously, we had images placing the Tornjak backend and SPIRE server in the same container, but these are currently deprecated. The above is a comprehensive list of images +> [!NOTE] +> Previously, we had images placing the Tornjak backend and SPIRE server in the same container, but these are currently deprecated. The above is a comprehensive list of supported images -Pre-built images can be found at the above links. To decide which tag to use, typically choose a release from [this page](https://github.com/spiffe/tornjak/releases) and choose the corresponding tag. For example, if you are interested in release `tornjak-1.0.2`, then choose image tag `v1.0.2`. +Pre-built images can be found at the above links. To decide which tag to use, typically choose a release from [this page](https://github.com/spiffe/tornjak/releases) and choose the corresponding tag. For example, if you are interested in release `v1.7.0`, then choose image tag `v1.7.0`. ### Compatibility Table @@ -17,11 +17,11 @@ Please see below for compatibility charts of SPIRE server versions with Tornjak: | Tornjak version | SPIRE Server version | | :--------------------- | :------------------- | | v1.1.x, v1.2.x, v1.3.x | v1.1.x, v1.2.x, v1.3.x, v1.4.x | -| v1.4.x, v1.5.x, v1.6.x | v1.5.x, v1.6.x, v1.7.x, v1.8.x, v1.9.x| +| v1.4.x, v1.5.x, v1.6.x, v1.7.x | v1.5.x, v1.6.x, v1.7.x, v1.8.x, v1.9.x| -## Tornjak Backend +## [Tornjak Backend](https://github.com/spiffe/tornjak/pkgs/container/tornjak-backend) -This is meant to be deployed where it can access a SPIRE server. To run, the container has three arguments: +The backend is designed to be deployed where it can access a SPIRE server. To run, the container has three arguments: | Flag | Description | Default | Arguments | Required | |:-----------------------|:------------------------------------------------------------|:--------|:----------|:---------| @@ -49,7 +49,9 @@ This creates a service listening on container port 50000, forwarded to localhost ## Tornjak Frontend -The frontend is meant to connect to either the Tornjak backend or the Tornjak manager. To run the container, we must set some environment variables: +The Tornjak frontend container exposes a browser application and must be able to connect to either the Tornjak backend or the Tornjak manager. + +The container requires certain environment variables be set. Below is a comprehensive list of all environment variables: | Variable | Description | Default | Example Argument | Required | |:----------------------------|-------------|--|--|--| @@ -64,26 +66,11 @@ The frontend is meant to connect to either the Tornjak backend or the Tornjak ma | `REACT_APP_SPIRE_HEALTH_CHECK_ENABLE` | Enable SPIRE health check component | `false` | `true` | false | ``` -docker run -p 3000:8080 -e REACT_APP_API_SERVER_URI='http://localhost:50000' -e REACT_APP_TORNJAK_MANAGER=true -e PORT_FE-8080 -e REACT_APP_SPIRE_HEALTH_CHECK=true ghcr.io/spiffe/tornjak-frontend:latest +docker run -p 3000:8080 -e REACT_APP_API_SERVER_URI='http://localhost:50000' -e REACT_APP_TORNJAK_MANAGER=true -e PORT_FE=8080 -e REACT_APP_SPIRE_HEALTH_CHECK=true ghcr.io/spiffe/tornjak-frontend:latest ``` The above command is an example of how to run the frontend. This creates a UI available at http://localhost:3000 forwarded from container port `8080`. It is listening to a Tornjak manager component available at http://localhost:50000, and knows to run in manager mode with the `REACT_APP_TORNJAK_MANAGER` flag. The last environment variables namely, `REACT_APP_SPIRE_HEALTH_CHECK_ENABLE` is used to enable the SPIRE health check component. -## Tornjak - -This container may be used as an alternative to having a frontend and backend container separately. The backend is configured exactly as the [Tornjak backend] with container arguments, and the frontend is configured exactly as the [Tornjak frontend] with container environment variables. - -An example command: - -``` -docker run -p 10000:10000 -p 3000:8080 -e REACT_APP_API_SERVER_URI='http://localhost:10000' -e PORT_FE-8080 -e PORT_BE-10000 ghcr.io/spiffe/tornjak:latest --spire-config --tornjak-config -``` - -The above command creates a UI available at `http://localhost:3000` forwarded from container port `8080`. It is listening to the Tornjak backend at `http://localhost:10000`, as given by the `REACT_APP_API_SERVER_URI` value. At the same time, the container is exposing port `10000` for the backend, which reads the SPIRE config and Tornjak config at `` and `` respectively. - -NOTE: The value of `REACT_APP_API_SERVER_URI` must be a URI that is separately available to any browser that accesses the frontend. Therefore, in production environments, it is necessary that backend service endpoint be public. - - ## Further steps It is recommended to try a full deployment of the Tornjak frontend, backend, and SPIRE Server in minikube. Please see our [tutorial document](docs/quickstart/README.md) for step-by-step instructions. diff --git a/api/agent/api.go b/api/agent/api.go index 251e5bf9..f3bcf74d 100644 --- a/api/agent/api.go +++ b/api/agent/api.go @@ -8,8 +8,10 @@ import ( "google.golang.org/grpc/credentials/insecure" agent "github.com/spiffe/spire-api-sdk/proto/spire/api/server/agent/v1" + bundle "github.com/spiffe/spire-api-sdk/proto/spire/api/server/bundle/v1" debugServer "github.com/spiffe/spire-api-sdk/proto/spire/api/server/debug/v1" entry "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" + trustdomain "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" types "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "google.golang.org/grpc/health/grpc_health_v1" @@ -215,6 +217,198 @@ func (s *Server) GetTornjakServerInfo(inp GetTornjakServerInfoRequest) (*GetTorn return (*GetTornjakServerInfoResponse)(&s.SpireServerInfo), nil } +// Bundle APIs +type GetBundleRequest bundle.GetBundleRequest +type GetBundleResponse types.Bundle + +func (s *Server) GetBundle(inp GetBundleRequest) (*GetBundleResponse, error) { //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + inpReq := bundle.GetBundleRequest(inp) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + var conn *grpc.ClientConn + conn, err := grpc.Dial(s.SpireServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + defer conn.Close() + client := bundle.NewBundleClient(conn) + + bundle, err := client.GetBundle(context.Background(), &inpReq) + if err != nil { + return nil, err + } + + return (*GetBundleResponse)(bundle), nil +} + +type ListFederatedBundlesRequest bundle.ListFederatedBundlesRequest +type ListFederatedBundlesResponse bundle.ListFederatedBundlesResponse + +func (s *Server) ListFederatedBundles(inp ListFederatedBundlesRequest) (*ListFederatedBundlesResponse, error) { //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + inpReq := bundle.ListFederatedBundlesRequest(inp) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + var conn *grpc.ClientConn + conn, err := grpc.Dial(s.SpireServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + defer conn.Close() + client := bundle.NewBundleClient(conn) + + bundle, err := client.ListFederatedBundles(context.Background(), &inpReq) + if err != nil { + return nil, err + } + + return (*ListFederatedBundlesResponse)(bundle), nil +} + +type CreateFederatedBundleRequest bundle.BatchCreateFederatedBundleRequest +type CreateFederatedBundleResponse bundle.BatchCreateFederatedBundleResponse + +func (s *Server) CreateFederatedBundle(inp CreateFederatedBundleRequest) (*CreateFederatedBundleResponse, error) { //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + inpReq := bundle.BatchCreateFederatedBundleRequest(inp) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + var conn *grpc.ClientConn + conn, err := grpc.Dial(s.SpireServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + defer conn.Close() + client := bundle.NewBundleClient(conn) + + bundle, err := client.BatchCreateFederatedBundle(context.Background(), &inpReq) + if err != nil { + return nil, err + } + + return (*CreateFederatedBundleResponse)(bundle), nil +} + +type UpdateFederatedBundleRequest bundle.BatchUpdateFederatedBundleRequest +type UpdateFederatedBundleResponse bundle.BatchUpdateFederatedBundleResponse + +func (s *Server) UpdateFederatedBundle(inp UpdateFederatedBundleRequest) (*UpdateFederatedBundleResponse, error) { //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + inpReq := bundle.BatchUpdateFederatedBundleRequest(inp) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + var conn *grpc.ClientConn + conn, err := grpc.Dial(s.SpireServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + defer conn.Close() + client := bundle.NewBundleClient(conn) + + bundle, err := client.BatchUpdateFederatedBundle(context.Background(), &inpReq) + if err != nil { + return nil, err + } + + return (*UpdateFederatedBundleResponse)(bundle), nil +} + +type DeleteFederatedBundleRequest bundle.BatchDeleteFederatedBundleRequest +type DeleteFederatedBundleResponse bundle.BatchDeleteFederatedBundleResponse + +func (s *Server) DeleteFederatedBundle(inp DeleteFederatedBundleRequest) (*DeleteFederatedBundleResponse, error) { //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + inpReq := bundle.BatchDeleteFederatedBundleRequest(inp) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + var conn *grpc.ClientConn + conn, err := grpc.Dial(s.SpireServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + defer conn.Close() + client := bundle.NewBundleClient(conn) + + bundle, err := client.BatchDeleteFederatedBundle(context.Background(), &inpReq) + if err != nil { + return nil, err + } + + return (*DeleteFederatedBundleResponse)(bundle), nil +} + +// Federation APIs +type ListFederationRelationshipsRequest trustdomain.ListFederationRelationshipsRequest +type ListFederationRelationshipsResponse trustdomain.ListFederationRelationshipsResponse + +func (s *Server) ListFederationRelationships(inp ListFederationRelationshipsRequest) (*ListFederationRelationshipsResponse, error) { //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + inpReq := trustdomain.ListFederationRelationshipsRequest(inp) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + var conn *grpc.ClientConn + conn, err := grpc.Dial(s.SpireServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + defer conn.Close() + client := trustdomain.NewTrustDomainClient(conn) + + bundle, err := client.ListFederationRelationships(context.Background(), &inpReq) + if err != nil { + return nil, err + } + + return (*ListFederationRelationshipsResponse)(bundle), nil +} + +type CreateFederationRelationshipRequest trustdomain.BatchCreateFederationRelationshipRequest +type CreateFederationRelationshipResponse trustdomain.BatchCreateFederationRelationshipResponse + +func (s *Server) CreateFederationRelationship(inp CreateFederationRelationshipRequest) (*CreateFederationRelationshipResponse, error) { //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + inpReq := trustdomain.BatchCreateFederationRelationshipRequest(inp) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + var conn *grpc.ClientConn + conn, err := grpc.Dial(s.SpireServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + defer conn.Close() + client := trustdomain.NewTrustDomainClient(conn) + + bundle, err := client.BatchCreateFederationRelationship(context.Background(), &inpReq) + if err != nil { + return nil, err + } + + return (*CreateFederationRelationshipResponse)(bundle), nil +} + +type UpdateFederationRelationshipRequest trustdomain.BatchUpdateFederationRelationshipRequest +type UpdateFederationRelationshipResponse trustdomain.BatchUpdateFederationRelationshipResponse + +func (s *Server) UpdateFederationRelationship(inp UpdateFederationRelationshipRequest) (*UpdateFederationRelationshipResponse, error) { //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + inpReq := trustdomain.BatchUpdateFederationRelationshipRequest(inp) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + var conn *grpc.ClientConn + conn, err := grpc.Dial(s.SpireServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + defer conn.Close() + client := trustdomain.NewTrustDomainClient(conn) + + bundle, err := client.BatchUpdateFederationRelationship(context.Background(), &inpReq) + if err != nil { + return nil, err + } + + return (*UpdateFederationRelationshipResponse)(bundle), nil +} + +type DeleteFederationRelationshipRequest trustdomain.BatchDeleteFederationRelationshipRequest +type DeleteFederationRelationshipResponse trustdomain.BatchDeleteFederationRelationshipResponse + +func (s *Server) DeleteFederationRelationship(inp DeleteFederationRelationshipRequest) (*DeleteFederationRelationshipResponse, error) { //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + inpReq := trustdomain.BatchDeleteFederationRelationshipRequest(inp) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + var conn *grpc.ClientConn + conn, err := grpc.Dial(s.SpireServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + defer conn.Close() + client := trustdomain.NewTrustDomainClient(conn) + + bundle, err := client.BatchDeleteFederationRelationship(context.Background(), &inpReq) + if err != nil { + return nil, err + } + + return (*DeleteFederationRelationshipResponse)(bundle), nil +} + + /* Agent diff --git a/api/agent/server.go b/api/agent/server.go index 3d647f3f..cbb9a835 100644 --- a/api/agent/server.go +++ b/api/agent/server.go @@ -1,6 +1,7 @@ package api import ( + "crypto/tls" "encoding/json" "fmt" "io" @@ -11,7 +12,6 @@ import ( "path/filepath" "strings" "time" - "crypto/tls" backoff "github.com/cenkalti/backoff/v4" "github.com/gorilla/mux" @@ -20,9 +20,12 @@ import ( "github.com/hashicorp/hcl/hcl/token" "github.com/pkg/errors" - agentdb "github.com/spiffe/tornjak/pkg/agent/db" + "google.golang.org/protobuf/encoding/protojson" + trustdomain "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1" + "github.com/spiffe/tornjak/pkg/agent/authentication/authenticator" "github.com/spiffe/tornjak/pkg/agent/authorization" + agentdb "github.com/spiffe/tornjak/pkg/agent/db" ) type Server struct { @@ -36,9 +39,9 @@ type Server struct { TornjakConfig *TornjakConfig // Plugins - Db agentdb.AgentDB + Db agentdb.AgentDB Authenticator authenticator.Authenticator - Authorizer authorization.Authorizer + Authorizer authorization.Authorizer } // config type, as defined by SPIRE @@ -406,10 +409,379 @@ func (s *Server) entryDelete(w http.ResponseWriter, r *http.Request) { } } +// Bundle APIs +func (s *Server) bundleGet(w http.ResponseWriter, r *http.Request) { + var input GetBundleRequest + buf := new(strings.Builder) + + n, err := io.Copy(buf, r.Body) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + data := buf.String() + + if n == 0 { + input = GetBundleRequest{} + } else { + err := json.Unmarshal([]byte(data), &input) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + } + + ret, err := s.GetBundle(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + je := json.NewEncoder(w) + err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +func (s *Server) federatedBundleList(w http.ResponseWriter, r *http.Request) { + var input ListFederatedBundlesRequest + buf := new(strings.Builder) + + n, err := io.Copy(buf, r.Body) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + data := buf.String() + + if n == 0 { + input = ListFederatedBundlesRequest{} + } else { + err := json.Unmarshal([]byte(data), &input) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + } + + ret, err := s.ListFederatedBundles(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + je := json.NewEncoder(w) + err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +func (s *Server) federatedBundleCreate(w http.ResponseWriter, r *http.Request) { + var input CreateFederatedBundleRequest + buf := new(strings.Builder) + + n, err := io.Copy(buf, r.Body) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + data := buf.String() + + if n == 0 { + input = CreateFederatedBundleRequest{} + } else { + err := json.Unmarshal([]byte(data), &input) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + } + + ret, err := s.CreateFederatedBundle(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + je := json.NewEncoder(w) + err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +func (s *Server) federatedBundleUpdate(w http.ResponseWriter, r *http.Request) { + var input UpdateFederatedBundleRequest + buf := new(strings.Builder) + + n, err := io.Copy(buf, r.Body) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + data := buf.String() + + if n == 0 { + input = UpdateFederatedBundleRequest{} + } else { + err := json.Unmarshal([]byte(data), &input) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + } + + ret, err := s.UpdateFederatedBundle(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + je := json.NewEncoder(w) + err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +func (s *Server) federatedBundleDelete(w http.ResponseWriter, r *http.Request) { + var input DeleteFederatedBundleRequest + buf := new(strings.Builder) + + n, err := io.Copy(buf, r.Body) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + data := buf.String() + + if n == 0 { + input = DeleteFederatedBundleRequest{} + } else { + err := json.Unmarshal([]byte(data), &input) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + } + + ret, err := s.DeleteFederatedBundle(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + je := json.NewEncoder(w) + err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +// Federation APIs +func (s *Server) federationList(w http.ResponseWriter, r *http.Request) { + var input ListFederationRelationshipsRequest + buf := new(strings.Builder) + + n, err := io.Copy(buf, r.Body) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + data := buf.String() + + if n == 0 { + input = ListFederationRelationshipsRequest{} + } else { + err := json.Unmarshal([]byte(data), &input) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + } + + ret, err := s.ListFederationRelationships(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + je := json.NewEncoder(w) + err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +func (s *Server) federationCreate(w http.ResponseWriter, r *http.Request) { + var input CreateFederationRelationshipRequest + var rawInput trustdomain.BatchCreateFederationRelationshipRequest + buf := new(strings.Builder) + + n, err := io.Copy(buf, r.Body) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + data := buf.String() + + if n == 0 { + input = CreateFederationRelationshipRequest{} + } else { + // required to use protojson because of oneof field + err := protojson.Unmarshal([]byte(data), &rawInput) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + input = CreateFederationRelationshipRequest(rawInput) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + } + + ret, err := s.CreateFederationRelationship(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + je := json.NewEncoder(w) + err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +func (s *Server) federationUpdate(w http.ResponseWriter, r *http.Request) { + var input UpdateFederationRelationshipRequest + var rawInput trustdomain.BatchUpdateFederationRelationshipRequest + buf := new(strings.Builder) + + n, err := io.Copy(buf, r.Body) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + data := buf.String() + + if n == 0 { + input = UpdateFederationRelationshipRequest{} + } else { + err := protojson.Unmarshal([]byte(data), &rawInput) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + input = UpdateFederationRelationshipRequest(rawInput) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + + } + + ret, err := s.UpdateFederationRelationship(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + je := json.NewEncoder(w) + err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + +func (s *Server) federationDelete(w http.ResponseWriter, r *http.Request) { + var input DeleteFederationRelationshipRequest + buf := new(strings.Builder) + + n, err := io.Copy(buf, r.Body) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + data := buf.String() + + if n == 0 { + input = DeleteFederationRelationshipRequest{} + } else { + err := json.Unmarshal([]byte(data), &input) + if err != nil { + emsg := fmt.Sprintf("Error parsing data: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } + } + + ret, err := s.DeleteFederationRelationship(input) //nolint:govet //Ignoring mutex (not being used) - sync.Mutex by value is unused for linter govet + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusInternalServerError) + return + } + + cors(w, r) + je := json.NewEncoder(w) + err = je.Encode(ret) + if err != nil { + emsg := fmt.Sprintf("Error: %v", err.Error()) + retError(w, emsg, http.StatusBadRequest) + return + } +} + + func cors(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, access-control-allow-origin, access-control-allow-headers, access-control-allow-credentials, Authorization, access-control-allow-methods") w.Header().Set("Access-Control-Expose-Headers", "*, Authorization") w.WriteHeader(http.StatusOK) @@ -418,7 +790,7 @@ func cors(w http.ResponseWriter, _ *http.Request) { func retError(w http.ResponseWriter, emsg string, status int) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, access-control-allow-origin, access-control-allow-headers, access-control-allow-credentials, Authorization, access-control-allow-methods") w.Header().Set("Access-Control-Expose-Headers", "*, Authorization") http.Error(w, emsg, status) @@ -431,7 +803,7 @@ func (s *Server) verificationMiddleware(next http.Handler) http.Handler { cors(w, r) return } - + userInfo := s.Authenticator.AuthenticateRequest(r) err := s.Authorizer.AuthorizeRequest(r, userInfo) @@ -558,7 +930,6 @@ func (s *Server) health(w http.ResponseWriter, r *http.Request) { } } - func (s *Server) GetRouter() http.Handler { rtr := mux.NewRouter() @@ -598,6 +969,38 @@ func (s *Server) GetRouter() http.Handler { apiRtr.HandleFunc("/api/tornjak/clusters/edit", s.clusterEdit) apiRtr.HandleFunc("/api/tornjak/clusters/delete", s.clusterDelete) + // Spire APIs with versioning + apiRtr.HandleFunc("/api/v1/spire/serverinfo", s.debugServer).Methods(http.MethodGet, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/spire/healthcheck", s.healthcheck).Methods(http.MethodGet, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/spire/agents", s.agentList).Methods(http.MethodGet, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/spire/agents/ban", s.agentBan).Methods(http.MethodPost, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/spire/agents", s.agentDelete).Methods(http.MethodDelete, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/spire/agents/jointoken", s.agentCreateJoinToken).Methods(http.MethodPost, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/spire/entries", s.entryList).Methods(http.MethodGet, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/spire/entries", s.entryCreate).Methods(http.MethodPost) + apiRtr.HandleFunc("/api/v1/spire/entries", s.entryDelete).Methods(http.MethodDelete) + apiRtr.HandleFunc("/api/v1/spire/bundle", s.bundleGet).Methods(http.MethodGet, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/spire/federations/bundles", s.federatedBundleList).Methods(http.MethodGet, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/spire/federations/bundles", s.federatedBundleCreate).Methods(http.MethodPost) + apiRtr.HandleFunc("/api/v1/spire/federations/bundles", s.federatedBundleUpdate).Methods(http.MethodPatch) + apiRtr.HandleFunc("/api/v1/spire/federations/bundles", s.federatedBundleDelete).Methods(http.MethodDelete) + apiRtr.HandleFunc("/api/v1/spire/federations", s.federationList).Methods(http.MethodGet, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/spire/federations", s.federationCreate).Methods(http.MethodPost) + apiRtr.HandleFunc("/api/v1/spire/federations", s.federationUpdate).Methods(http.MethodPatch) + apiRtr.HandleFunc("/api/v1/spire/federations", s.federationDelete).Methods(http.MethodDelete) + + // Tornjak specific + apiRtr.HandleFunc("/api/v1/tornjak/serverinfo", s.tornjakGetServerInfo).Methods(http.MethodGet, http.MethodOptions) + // Agents Selectors + apiRtr.HandleFunc("/api/v1/tornjak/selectors", s.tornjakPluginDefine).Methods(http.MethodPost, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/tornjak/selectors", s.tornjakSelectorsList).Methods(http.MethodGet) + apiRtr.HandleFunc("/api/v1/tornjak/agents", s.tornjakAgentsList).Methods(http.MethodGet, http.MethodOptions) + // Clusters + apiRtr.HandleFunc("/api/v1/tornjak/clusters", s.clusterList).Methods(http.MethodGet, http.MethodOptions) + apiRtr.HandleFunc("/api/v1/tornjak/clusters", s.clusterCreate).Methods(http.MethodPost) + apiRtr.HandleFunc("/api/v1/tornjak/clusters", s.clusterEdit).Methods(http.MethodPatch) + apiRtr.HandleFunc("/api/v1/tornjak/clusters", s.clusterDelete).Methods(http.MethodDelete) + // Middleware apiRtr.Use(s.verificationMiddleware) @@ -609,7 +1012,7 @@ func (s *Server) GetRouter() http.Handler { } func (s *Server) redirectHTTP(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" && r.Method != "HEAD" { + if r.Method != http.MethodGet && r.Method != "HEAD" { http.Error(w, "Use HTTPS", http.StatusBadRequest) return } @@ -636,7 +1039,7 @@ func (s *Server) HandleRequests() { // TODO: replace with workerGroup for thread safety errChannel := make(chan error, 2) - + serverConfig := s.TornjakConfig.Server if serverConfig.HTTPConfig == nil { err = fmt.Errorf("HTTP Config error: no port configured") @@ -658,7 +1061,6 @@ func (s *Server) HandleRequests() { httpsConfig := serverConfig.HTTPSConfig var tlsConfig *tls.Config - if serverConfig.HTTPSConfig.ListenPort == 0 { // Fail because this is required field in this section err = fmt.Errorf("HTTPS Config error: no port configured. Starting insecure HTTP connection at %d...", serverConfig.HTTPConfig.ListenPort) @@ -834,6 +1236,7 @@ func NewAuthorizer(authorizerPlugin *ast.ObjectItem) (authorization.Authorizer, // decode into role list and apiMapping roleList := make(map[string]string) apiMapping := make(map[string][]string) + apiV1Mapping := make(map[string]map[string][]string) for _, role := range config.RoleList { roleList[role.Name] = role.Desc // print warning for empty string @@ -843,9 +1246,22 @@ func NewAuthorizer(authorizerPlugin *ast.ObjectItem) (authorization.Authorizer, } for _, api := range config.APIRoleMappings { apiMapping[api.Name] = api.AllowedRoles + fmt.Printf("API name: %s, Allowed Roles: %s \n", api.Name, api.AllowedRoles) + } + for _, apiV1 := range config.APIv1RoleMappings{ + arr := strings.Split(apiV1.Name, " ") + apiV1.Method = arr[0] + apiV1.Path = arr[1] + fmt.Printf("API V1 method: %s, API V1 path: %s, API V1 allowed roles: %s \n", apiV1.Method, apiV1.Path, apiV1.AllowedRoles) + if _, ok := apiV1Mapping[apiV1.Path]; ok { + apiV1Mapping[apiV1.Path][apiV1.Method] = apiV1.AllowedRoles + } else { + apiV1Mapping[apiV1.Path] = map[string][]string{apiV1.Method: apiV1.AllowedRoles} + } } + fmt.Printf("API V1 Mapping: %+v\n", apiV1Mapping) - authorizer, err := authorization.NewRBACAuthorizer(config.Name, roleList, apiMapping) + authorizer, err := authorization.NewRBACAuthorizer(config.Name, roleList, apiMapping, apiV1Mapping) if err != nil { return nil, errors.Errorf("Couldn't configure Authorizer: %v", err) } diff --git a/api/agent/types.go b/api/agent/types.go index b5e39911..8e996b00 100644 --- a/api/agent/types.go +++ b/api/agent/types.go @@ -1,130 +1,138 @@ -package api - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "os" - - "github.com/hashicorp/hcl/hcl/ast" -) - -// TornjakServerInfo provides insight into the configuration of the SPIRE server -// where the Tornjak Agent resides -type TornjakSpireServerInfo struct { - // Plugins is a map from plugin types to respective names of plugins configured - Plugins map[string][]string `json:"plugins"` - // TrustDomain specifies the trust domain of the SPIRE server configured with tornjak - TrustDomain string `json:"trustDomain"` - // Verbose config contains unstructure information on the config on the agent - VerboseConfig string `json:"verboseConfig"` -} - -// pared down version of full Server Config type spire/cmd/spire-server/cli/run -// we curently need only extract the trust domain -type SpireServerConfig struct { - TrustDomain string `hcl:"trust_domain"` -} - -type SPIREConfig struct { - Server *SpireServerConfig `hcl:"server"` - Plugins ast.Node `hcl:"plugins"` -} - -type TornjakConfig struct { - Server *serverConfig `hcl:"server"` - Plugins *ast.Node `hcl:"plugins"` -} - -/* Server configuration*/ - -type serverConfig struct { - SPIRESocket string `hcl:"spire_socket_path"` - HTTPConfig *HTTPConfig `hcl:"http"` - HTTPSConfig *HTTPSConfig `hcl:"https"` -} - -type HTTPConfig struct { - ListenPort int `hcl:"port"` -} - -type HTTPSConfig struct { - ListenPort int `hcl:"port"` - Cert string `hcl:"cert"` - Key string `hcl:"key"` - ClientCA string `hcl:"client_ca"` -} - -func (h HTTPSConfig) Parse() (*tls.Config, error) { - serverCertPath := h.Cert - serverKeyPath := h.Key - clientCAPath := h.ClientCA - - mtls := (clientCAPath != "") - - if _, err := os.Stat(serverCertPath); os.IsNotExist(err) { - return nil, fmt.Errorf("server cert path '%s': %w", serverCertPath, err) - } - if _, err := os.Stat(serverKeyPath); os.IsNotExist(err) { - return nil, fmt.Errorf("server key path '%s': %w", serverKeyPath, err) - } - - // Create a CA certificate pool and add cert.pem to it - serverCert, err := os.ReadFile(serverCertPath) - if err != nil { - return nil, fmt.Errorf("server ca pool error: %w", err) - } - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(serverCert) - - if mtls { - // add mTLS CA path to cert pool as well - if _, err := os.Stat(clientCAPath); os.IsNotExist(err) { - return nil, fmt.Errorf("server file does not exist %s", clientCAPath) - } - clientCA, err := os.ReadFile(clientCAPath) - if err != nil { - return nil, fmt.Errorf("server: could not read file %s: %w", clientCAPath, err) - } - caCertPool.AppendCertsFromPEM(clientCA) - } - - // Create the TLS Config with the CA pool and enable Client certificate validation - tlsConfig := &tls.Config{ - ClientCAs: caCertPool, - } - - if mtls { - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - } - //tlsConfig.BuildNameToCertificate() - - return tlsConfig, nil -} - -/* Plugin types */ -type pluginDataStoreSQL struct { - Drivername string `hcl:"drivername"` - Filename string `hcl:"filename"` -} - -type pluginAuthenticatorKeycloak struct { - IssuerURL string `hcl:"issuer"` - Audience string `hcl:"audience"` -} - -type AuthRole struct { - Name string `hcl:",key"` - Desc string `hcl:"desc"` -} - -type APIRoleMapping struct { - Name string `hcl:",key"` - AllowedRoles []string `hcl:"allowed_roles"` -} - -type pluginAuthorizerRBAC struct { - Name string `hcl:"name"` - RoleList []*AuthRole `hcl:"role,block"` - APIRoleMappings []*APIRoleMapping `hcl:"API,block"` -} +package api + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" + + "github.com/hashicorp/hcl/hcl/ast" +) + +// TornjakServerInfo provides insight into the configuration of the SPIRE server +// where the Tornjak Agent resides +type TornjakSpireServerInfo struct { + // Plugins is a map from plugin types to respective names of plugins configured + Plugins map[string][]string `json:"plugins"` + // TrustDomain specifies the trust domain of the SPIRE server configured with tornjak + TrustDomain string `json:"trustDomain"` + // Verbose config contains unstructure information on the config on the agent + VerboseConfig string `json:"verboseConfig"` +} + +// pared down version of full Server Config type spire/cmd/spire-server/cli/run +// we curently need only extract the trust domain +type SpireServerConfig struct { + TrustDomain string `hcl:"trust_domain"` +} + +type SPIREConfig struct { + Server *SpireServerConfig `hcl:"server"` + Plugins ast.Node `hcl:"plugins"` +} + +type TornjakConfig struct { + Server *serverConfig `hcl:"server"` + Plugins *ast.Node `hcl:"plugins"` +} + +/* Server configuration*/ + +type serverConfig struct { + SPIRESocket string `hcl:"spire_socket_path"` + HTTPConfig *HTTPConfig `hcl:"http"` + HTTPSConfig *HTTPSConfig `hcl:"https"` +} + +type HTTPConfig struct { + ListenPort int `hcl:"port"` +} + +type HTTPSConfig struct { + ListenPort int `hcl:"port"` + Cert string `hcl:"cert"` + Key string `hcl:"key"` + ClientCA string `hcl:"client_ca"` +} + +func (h HTTPSConfig) Parse() (*tls.Config, error) { + serverCertPath := h.Cert + serverKeyPath := h.Key + clientCAPath := h.ClientCA + + mtls := (clientCAPath != "") + + if _, err := os.Stat(serverCertPath); os.IsNotExist(err) { + return nil, fmt.Errorf("server cert path '%s': %w", serverCertPath, err) + } + if _, err := os.Stat(serverKeyPath); os.IsNotExist(err) { + return nil, fmt.Errorf("server key path '%s': %w", serverKeyPath, err) + } + + // Create a CA certificate pool and add cert.pem to it + serverCert, err := os.ReadFile(serverCertPath) + if err != nil { + return nil, fmt.Errorf("server ca pool error: %w", err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(serverCert) + + if mtls { + // add mTLS CA path to cert pool as well + if _, err := os.Stat(clientCAPath); os.IsNotExist(err) { + return nil, fmt.Errorf("server file does not exist %s", clientCAPath) + } + clientCA, err := os.ReadFile(clientCAPath) + if err != nil { + return nil, fmt.Errorf("server: could not read file %s: %w", clientCAPath, err) + } + caCertPool.AppendCertsFromPEM(clientCA) + } + + // Create the TLS Config with the CA pool and enable Client certificate validation + tlsConfig := &tls.Config{ + ClientCAs: caCertPool, + } + + if mtls { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } + //tlsConfig.BuildNameToCertificate() + + return tlsConfig, nil +} + +/* Plugin types */ +type pluginDataStoreSQL struct { + Drivername string `hcl:"drivername"` + Filename string `hcl:"filename"` +} + +type pluginAuthenticatorKeycloak struct { + IssuerURL string `hcl:"issuer"` + Audience string `hcl:"audience"` +} + +type AuthRole struct { + Name string `hcl:",key"` + Desc string `hcl:"desc"` +} + +type APIRoleMapping struct { + Name string `hcl:",key"` + AllowedRoles []string `hcl:"allowed_roles"` +} + +type APIv1RoleMapping struct { + Name string `hcl:",key"` + Method string `hcl:"-"` + Path string `hcl:"-"` + AllowedRoles []string `hcl:"allowed_roles"` +} + +type pluginAuthorizerRBAC struct { + Name string `hcl:"name"` + RoleList []*AuthRole `hcl:"role,block"` + APIRoleMappings []*APIRoleMapping `hcl:"API,block"` + APIv1RoleMappings []*APIv1RoleMapping `hcl:"APIv1,block"` +} diff --git a/cmd/agent/main.go b/cmd/agent/main.go index f62f2ff0..7b5471d2 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -15,7 +15,7 @@ import ( type cliOptions struct { genericOptions struct { - spireFile string + spireFile string tornjakFile string expandEnv bool } @@ -124,7 +124,6 @@ func GetServerInfo(configData string) (agentapi.TornjakSpireServerInfo, error) { return agentapi.TornjakSpireServerInfo{}, errors.Errorf("Could not parse SPIRE Config: %v", err) } - if config.Plugins == nil { return agentapi.TornjakSpireServerInfo{}, errors.New("config plugins map should not be nil") } diff --git a/docs/conf/agent/full.conf b/docs/conf/agent/full.conf index e1814daa..3d8bae30 100644 --- a/docs/conf/agent/full.conf +++ b/docs/conf/agent/full.conf @@ -32,7 +32,7 @@ plugins { DataStore "sql" { plugin_data { drivername = "sqlite3" - filename = "/run/spire/data/tornjak.sqlite3" # location of database + filename = "tornjak.sqlite3" # location of database } } @@ -89,6 +89,34 @@ plugins { API "/api/tornjak/clusters/create" { allowed_roles = ["admin"] } API "/api/tornjak/clusters/edit" { allowed_roles = ["admin"] } API "/api/tornjak/clusters/delete" { allowed_roles = ["admin"] } + + # v1 API + APIv1 "GET /api/v1/spire/serverinfo" { allowed_roles = ["admin", "viewer"] } + APIv1 "GET /api/v1/spire/healthcheck" { allowed_roles = ["admin", "viewer"] } + APIv1 "GET /api/v1/spire/agents" { allowed_roles = ["admin", "viewer"] } + APIv1 "DELETE /api/v1/spire/agents" { allowed_roles = ["admin"] } + APIv1 "POST /api/v1/spire/agents/ban" { allowed_roles = ["admin"] } + APIv1 "POST /api/v1/spire/agents/jointoken" { allowed_roles = ["admin"] } + APIv1 "GET /api/v1/spire/entries" { allowed_roles = ["admin", "viewer"] } + APIv1 "POST /api/v1/spire/entries" { allowed_roles = ["admin"] } + APIv1 "DELETE /api/v1/spire/entries" { allowed_roles = ["admin"] } + + # SPIRE Federation API calls + APIv1 "GET /api/v1/spire/bundle" { allowed_roles = ["admin", "viewer"] } + APIv1 "GET /api/v1/spire/federations/bundles" { allowed_roles = ["admin", "viewer"] } + APIv1 "POST /api/v1/spire/federations/bundles" { allowed_roles = ["admin"] } + APIv1 "PATCH /api/v1/spire/federations/bundles" { allowed_roles = ["admin"] } + APIv1 "DELETE /api/v1/spire/federations/bundles" { allowed_roles = ["admin"] } + + # Tornjak API calls + APIv1 "GET /api/v1/tornjak/serverinfo" { allowed_roles = ["admin", "viewer"] } + APIv1 "GET /api/v1/tornjak/agents" { allowed_roles = ["admin", "viewer"] } + APIv1 "POST /api/v1/tornjak/selectors" { allowed_roles = ["admin"] } + APIv1 "GET /api/v1/tornjak/selectors" { allowed_roles = ["admin", "viewer"] } + APIv1 "GET /api/v1/tornjak/clusters" { allowed_roles = ["admin", "viewer"] } + APIv1 "POST /api/v1/tornjak/clusters" { allowed_roles = ["admin"] } + APIv1 "PATCH /api/v1/tornjak/clusters" { allowed_roles = ["admin"] } + APIv1 "DELETE /api/v1/tornjak/clusters" { allowed_roles = ["admin"] } } } diff --git a/docs/quickstart/README.md b/docs/quickstart/README.md index 9676e0f1..b8852d01 100644 --- a/docs/quickstart/README.md +++ b/docs/quickstart/README.md @@ -6,12 +6,12 @@ Before we dive into the deployment process, let’s familiarize ourselves with T [SPIRE](https://github.com/spiffe/spire) (the SPIFFE Runtime Environment) is an open-source software tool that provides a way to issue and manage identities in the form of SPIFFE IDs within a distributed system. These identities are used to establish trust between software services and are based on the SPIFFE (Secure Production Identity Framework For Everyone) standards, which define a universal identity control plane for distributed systems. SPIRE provides access to the SPIFFE Workload API, which authenticates active software systems and allocates SPIFFE IDs and corresponding SVIDs to them. This process enables mutual trust establishment between two distinct workloads. -Tornjak is a control plane and GUI for SPIRE, aimed at managing SPIRE deployments across multiple clusters. It provides a management plane that simplifies and centralizes the administration of SPIRE, offering an intuitive interface for defining, distributing, and visualizing SPIFFE identities across a heterogeneous environment. +Tornjak is a control plane and GUI for SPIRE, aimed at managing SPIRE deployments across multiple clusters. It provides a management plane that simplifies and centralizes the administration of SPIRE, offering an intuitive interface for defining, distributing, and visualizing SPIFFE identities across a heterogeneous environment. -This tutorial will get you up and running with a local deployment of SPIRE and Tornjak in three simple steps: +This tutorial will get you up and running with a local deployment of SPIRE and Tornjak in three simple steps: - Setting up the deployment files - Deployment -- Connecting to Tornjak. +- Connecting to Tornjak. Contents - [Step 0: Prerequisite](#step-0-prerequisite) @@ -21,7 +21,7 @@ Contents - [Cleanup](#cleanup) - [Troubleshooting Commmon Issues](#Troubleshooting) -## Step 0: Prerequisite +## Step 0: Prerequisite Before you begin this tutorial, make sure you have the following: - Minikube: Version 1.12.0 or later. [Download Minikube.](https://minikube.sigs.k8s.io/docs/start/) @@ -29,13 +29,13 @@ Before you begin this tutorial, make sure you have the following: Note: While we have tested this tutorial with the versions below, newer versions should also work. Ensure you're using the most recent stable releases to avoid compatibility issues. - Minikube Version 1.12.0, Version 1.31.2 - - Docker Version 20.10.23, Version 24.0.6 + - Docker Version 20.10.23, Version 24.0.6 ## Step 1: Setup deployment files ### Setting up k8s -For this tutorial, we will use minikube. If you have an existing kubernetes cluster, feel free to use that. +For this tutorial, we will use minikube. If you have an existing kubernetes cluster, feel free to use that. ```console minikube start @@ -74,11 +74,11 @@ cd tornjak cd docs/quickstart ``` -Notice, the files in this directory are largely the same files as provided by the [SPIRE quickstart for Kubernetes](https://spiffe.io/docs/latest/try/getting-started-k8s/). However, there are some minor key differences. Take note of the tornjak-configmap.yaml file, which includes configuration details for the Tornjak backend. +Notice, the files in this directory are largely the same files as provided by the [SPIRE quickstart for Kubernetes](https://spiffe.io/docs/latest/try/getting-started-k8s/). However, there are some minor key differences. Take note of the tornjak-configmap.yaml file, which includes configuration details for the Tornjak backend. To view the configuration you can issue the following: ```console -cat tornjak-configmap.yaml +cat tornjak-configmap.yaml ``` Contents of the configuration for the Tornjak backend should look like: @@ -114,24 +114,24 @@ data: } ``` -More information on this config file format can be found in [our config documentation](../config-tornjak-server.md). +More information on this config file format can be found in [our config documentation](../config-tornjak-server.md). -Additionally, we have sample server-statefulset files in the directory `server-statefulset-examples`. We will copy one of them in depending on which deployment scheme you would like. +Additionally, we have sample server-statefulset files in the directory `server-statefulset-examples`. We will copy one of them in depending on which deployment scheme you would like. ### Choosing the Statefulset Deployment -Depending on your use case, you can deploy Tornjak in different configurations. Note we have deprecated support of the use case where parts of Tornjak run on the same container as SPIRE. +Depending on your use case, you can deploy Tornjak in different configurations. Note we have deprecated support of the use case where parts of Tornjak run on the same container as SPIRE. -Currently, we support the following deployment scheme: +Currently, we support the following deployment scheme: -1. Only the Tornjak backend (to make Tornjak API calls) is run as a separate container on the same pod that exposes only one port (to communicate with the Tornjak backend). This deployment type is fully-supported, has a smaller sidecar image without the frontend components, and ensures that the frontend and backend share no memory. +1. Only the Tornjak backend (to make Tornjak API calls) is run as a separate container on the same pod that exposes only one port (to communicate with the Tornjak backend). This deployment type is fully-supported, has a smaller sidecar image without the frontend components, and ensures that the frontend and backend share no memory. -Using the option below, easily copy in the right server-statefulset file. +Using the option below, easily copy in the right server-statefulset file. -
🔴 [Click] For the deployment of only the Tornjak backend (API) +
🔴 [Click] For the deployment of only the Tornjak backend (API) -There is an additional requirement to mount the SPIRE server socket and make it accessible to the Tornjak backend container. +There is an additional requirement to mount the SPIRE server socket and make it accessible to the Tornjak backend container. The relevant file is called `backend-sidecar-server-statefulset.yaml` within the examples directory. Please copy to the relevant file as follows: @@ -139,10 +139,10 @@ The relevant file is called `backend-sidecar-server-statefulset.yaml` within the cp server-statefulset-examples/backend-sidecar-server-statefulset.yaml server-statefulset.yaml ``` -The statefulset will look something like this, where we have commented leading with a 👈 on the changed or new lines: +The statefulset will look something like this, where we have commented leading with a 👈 on the changed or new lines: ```console -cat server-statefulset.yaml +cat server-statefulset.yaml ``` ``` @@ -243,11 +243,11 @@ spec: Note that there are three key differences in this StatefulSet file from that in the SPIRE quickstart: -1. There is a new container in the pod named tornjak-backend. +1. There is a new container in the pod named tornjak-backend. 3. We create a volume named `tornjak-config` that reads from the ConfigMap `tornjak-agent`. -4. We create a volume named `test-socket` so that the containers may communicate. +4. We create a volume named `test-socket` so that the containers may communicate. -This is all done specifically to pass the Tornjak config file as an argument to the container and to allow communication between Tornjak and SPIRE. +This is all done specifically to pass the Tornjak config file as an argument to the container and to allow communication between Tornjak and SPIRE.
@@ -255,7 +255,7 @@ This is all done specifically to pass the Tornjak config file as an argument to Now that we have the correct deployment files, please follow the below steps to deploy Tornjak and SPIRE! -NOTE: In a Windows OS environment, you will need to replace the backslashes ( \\ ) below with backticks ( \` ) to copy and paste into a Windows terminal. This doesnt apply for Mac. +NOTE: In a Windows OS environment, you will need to replace the backslashes ( \\ ) below with backticks ( \` ) to copy and paste into a Windows terminal. This doesnt apply for Mac. ```console kubectl apply -f spire-namespace.yaml \ -f server-account.yaml \ @@ -286,7 +286,7 @@ service/tornjak-backend-mtls created service/tornjak-frontend created ``` -Before continuing, check that the spire-server is ready: +Before continuing, check that the spire-server is ready: ```console kubectl get statefulset --namespace spire @@ -301,7 +301,7 @@ NOTE: You may initially see a `0/1` for READY status. Just wait a few minutes an ### Deploying the agent and creating test entries -The following steps will configure and deploy the SPIRE agent. +The following steps will configure and deploy the SPIRE agent. NOTE: In a windows environment, you will need to replace the backslashes ( \\ ) below with backticks ( \` ) to copy and paste into a windows terminal ```console kubectl apply \ @@ -329,7 +329,7 @@ spire-agent 1 1 1 1 1 ``` -Then, we can create a registration entry for the node. +Then, we can create a registration entry for the node. NOTE: In a windows environment, you will need to replace the backslashes ( \\ ) below with backticks ( \` ) to copy and paste into a windows terminal ```console @@ -375,7 +375,7 @@ Selector : k8s:ns:default Selector : k8s:sa:default ``` -Finally, here we deploy a workload container: +Finally, here we deploy a workload container: ```console kubectl apply -f client-deployment.yaml @@ -419,7 +419,7 @@ Should yield two lines depending on which deployment you used: Image: ``` -where `` is `ghcr.io/spiffe/tornjak:latest` if you deployed the Tornjak with the UI and is `ghcr.io/spiffe/tornjak-backend:latest` if you deployed only the Tornjak backend. +where `` is `ghcr.io/spiffe/tornjak:latest` if you deployed the Tornjak with the UI and is `ghcr.io/spiffe/tornjak-backend:latest` if you deployed only the Tornjak backend. ## Step 3: Configuring Access to Tornjak @@ -438,7 +438,7 @@ Forwarding from 127.0.0.1:10000 -> 10000 Forwarding from [::1]:10000 -> 10000 ``` -While this runs, open a browser to +While this runs, open a browser to ``` http://localhost:10000/api/tornjak/serverinfo @@ -450,16 +450,16 @@ This output represents the backend response. Now you should be able to make Torn ### Step 3b: Connecting to the Tornjak frontend to access the Tornjak UI -Make sure that the backend is accessible from your browser at `http://localhost:10000`, as above, or the frontend will not work. +Make sure that the backend is accessible from your browser at `http://localhost:10000`, as above, or the frontend will not work. If you chose to deploy Tornjak with the UI, connecting to the UI is very simple. Otherwise, you can always run the UI locally and connect. See the two choices below: -
🔴 [Click] Run the Tornjak frontend locally +
🔴 [Click] Run the Tornjak frontend locally -You will need to deploy the separate frontend separately to access the exposed Tornjak backend. We have prebuilt the frontend in a container, so we can simply run it via a single docker command in a separate terminal, which will take a couple minutes to run: +You will need to deploy the separate frontend separately to access the exposed Tornjak backend. We have prebuilt the frontend in a container, so we can simply run it via a single docker command in a separate terminal, which will take a couple minutes to run: ```console -docker run -p 3000:3000 -e REACT_APP_API_SERVER_URI='http://localhost:10000' ghcr.io/spiffe/tornjak-frontend:latest +docker run -p 3000:3000 -e REACT_APP_API_SERVER_URI='http://localhost:10000' ghcr.io/spiffe/tornjak-frontend:latest ``` After the image is downloaded, you will eventually see the following output: @@ -469,7 +469,7 @@ After the image is downloaded, you will eventually see the following output: > react-scripts --openssl-legacy-provider start ℹ 「wds」: Project is running at http://172.17.0.3/ -ℹ 「wds」: webpack output is served from +ℹ 「wds」: webpack output is served from ℹ 「wds」: Content not from webpack is served from /usr/src/app/public ℹ 「wds」: 404s will fallback to / Starting the development server... @@ -485,7 +485,7 @@ Note that the development build is not optimized. To create a production build, use npm run build. ``` -Note, it will likely take a few minutes for the applicaiton to compile successfully. +Note, it will likely take a few minutes for the applicaiton to compile successfully.
@@ -516,7 +516,7 @@ kubectl delete clusterrole spire-server-trust-role spire-agent-cluster-role kubectl delete clusterrolebinding spire-server-trust-role-binding spire-agent-cluster-role-binding ``` -## Troubleshooting +## Troubleshooting
Troubleshoot 1: Minikube fails to start with a Docker CLI context error When running the `minikube start` command, you might encounter an error like the one below: @@ -541,9 +541,9 @@ Solution: - Make sure Docker is installed on your system. If it's not installed, you can install Docker by following the instructions on the official Docker [installation guide.](https://docs.docker.com/get-docker/) 2. Start Docker: -- On macOS and Windows: Docker Desktop has a graphical interface to manage the Docker service. Open Docker Desktop to start Docker. Alternativly, run the command '''open -a Docker'''' +- On macOS and Windows: Docker Desktop has a graphical interface to manage the Docker service. Open Docker Desktop to start Docker. Alternativly, run the command '''open -a Docker'''' 3. Retry Starting Minikube: -- After ensuring that Docker is running, you can start Minikube again using: +- After ensuring that Docker is running, you can start Minikube again using: ```console minikube start ``` diff --git a/examples/tls_mtls/README.md b/examples/tls_mtls/README.md index bbcedc0d..a647e37a 100644 --- a/examples/tls_mtls/README.md +++ b/examples/tls_mtls/README.md @@ -86,7 +86,7 @@ cat server-statefulset-tls.yaml volumeMounts: ... - name: tls-volume - mountPath: /opt/spire/server + mountPath: /opt/tornjak/server ... volumes: ... @@ -123,9 +123,9 @@ Then we mount the secret to the Tornjak container via volume mount, as in the pr volumeMounts: ... - name: tls-volume - mountPath: /opt/spire/server + mountPath: /opt/tornjak/server - name: client-cas - mountPath: /opt/spire/clients + mountPath: /opt/tornjak/clients ... volumes: ... diff --git a/examples/tls_mtls/server-statefulset-mtls.yaml b/examples/tls_mtls/server-statefulset-mtls.yaml index 5784e012..981fa4e4 100644 --- a/examples/tls_mtls/server-statefulset-mtls.yaml +++ b/examples/tls_mtls/server-statefulset-mtls.yaml @@ -71,9 +71,9 @@ spec: - name: socket mountPath: /tmp/spire-server/private - name: tls-volume # 👈 TLS SECRET VOLUME MOUNT - mountPath: /opt/spire/server # 👈 TLS SECRET VOLUME MOUNT + mountPath: /opt/tornjak/server # 👈 TLS SECRET VOLUME MOUNT - name: client-ca # 👈 mTLS CA SECRET VOLUME MOUNT - mountPath: /opt/spire/clients # 👈 mTLS CA SECRET VOLUME MOUNT + mountPath: /opt/tornjak/clients # 👈 mTLS CA SECRET VOLUME MOUNT volumes: - name: spire-config configMap: diff --git a/examples/tls_mtls/server-statefulset-tls.yaml b/examples/tls_mtls/server-statefulset-tls.yaml index b03b0b39..3a9120c9 100644 --- a/examples/tls_mtls/server-statefulset-tls.yaml +++ b/examples/tls_mtls/server-statefulset-tls.yaml @@ -71,7 +71,7 @@ spec: - name: socket mountPath: /tmp/spire-server/private - name: secret-volume # 👈 TLS SECRET VOLUME MOUNT - mountPath: /opt/spire/server # 👈 TLS SECRET VOLUME MOUNT + mountPath: /opt/tornjak/server # 👈 TLS SECRET VOLUME MOUNT volumes: - name: spire-config configMap: diff --git a/frontend/.env b/frontend/.env index b40176c3..761d8972 100644 --- a/frontend/.env +++ b/frontend/.env @@ -5,6 +5,7 @@ NODE_PATH=src/ SKIP_PREFLIGHT_CHECK=true REACT_APP_DEBUG_TORNJAK=true REACT_APP_TORNJAK_MANAGER=false +REACT_APP_API_VERSION=v1 ##### Backend Server uri REACT_APP_API_SERVER_URI=http://localhost:10000/ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 65b68ef7..b045ffba 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,7 +23,7 @@ "@types/file-saver": "^2.0.5", "@types/react-dom": "^17.0.9", "@types/react-router-dom": "^5.3.1", - "axios": "^1.6.0", + "axios": "^1.7.7", "bootstrap": "^5.3.0", "carbon-components": "^10.36.0", "carbon-components-react": "^8.31.3", @@ -69,7 +69,7 @@ "jest-transform-stub": "^2.0.0", "moxios": "^0.4.0", "nodemon": "^2.0.13", - "puppeteer": "^20.1.2", + "puppeteer": "^22.11.2", "react-inject-env": "^2.1.0", "react-test-renderer": "^18.2.0", "redux-mock-store": "^1.5.4" @@ -4316,32 +4316,25 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", - "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.3.tgz", + "integrity": "sha512-bJ0UBsk0ESOs6RFcLXOt99a3yTDcOKlzfjad+rhFwdaG1Lu/Wzq58GHYCDTlZ9z6mldf4g+NTb+TXEfe0PpnsQ==", "dev": true, "dependencies": { "debug": "4.3.4", "extract-zip": "2.0.1", "progress": "2.0.3", - "proxy-agent": "6.3.0", - "tar-fs": "3.0.4", + "proxy-agent": "6.4.0", + "semver": "7.6.0", + "tar-fs": "3.0.5", "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" + "yargs": "17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" }, "engines": { - "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=18" } }, "node_modules/@puppeteer/browsers/node_modules/cliui": { @@ -4358,10 +4351,43 @@ "node": ">=12" } }, + "node_modules/@puppeteer/browsers/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@puppeteer/browsers/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -5137,15 +5163,6 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -5780,9 +5797,9 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -5799,9 +5816,9 @@ "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.6", @@ -5819,14 +5836,14 @@ "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { @@ -5851,26 +5868,26 @@ "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -5878,22 +5895,22 @@ } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -5902,11 +5919,11 @@ } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, @@ -5964,10 +5981,10 @@ "acorn-walk": "^8.0.2" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "peerDependencies": { "acorn": "^8" } @@ -6410,11 +6427,11 @@ } }, "node_modules/axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -6428,9 +6445,9 @@ } }, "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", "dev": true }, "node_modules/babel-jest": { @@ -6724,6 +6741,52 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", + "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", + "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", + "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "dev": true, + "optional": true, + "dependencies": { + "streamx": "^2.18.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6744,9 +6807,9 @@ ] }, "node_modules/basic-ftp": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.4.tgz", - "integrity": "sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "dev": true, "engines": { "node": ">=10.0.0" @@ -6794,9 +6857,9 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -6806,7 +6869,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -6890,11 +6953,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -7008,13 +7071,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7299,12 +7367,14 @@ } }, "node_modules/chromium-bidi": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", - "integrity": "sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.23.tgz", + "integrity": "sha512-1o/gLU9wDqbN5nL2MtfjykjOuighGXc3/hnWueO1haiEoFgX8h5vbvcA4tgdQfjw1mkZ1OEF4x/+HVeqEX6NoA==", "dev": true, "dependencies": { - "mitt": "3.0.0" + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" @@ -8645,9 +8715,9 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" }, "node_modules/data-uri-to-buffer": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.1.tgz", - "integrity": "sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, "engines": { "node": ">= 14" @@ -8787,16 +8857,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -8921,9 +8994,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/devtools-protocol": { - "version": "0.0.1147663", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz", - "integrity": "sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==", + "version": "0.0.1299070", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1299070.tgz", + "integrity": "sha512-+qtL3eX50qsJ7c+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7//Xe8yhMmQ+elj3uAqSg==", "dev": true }, "node_modules/didyoumean": { @@ -9057,9 +9130,9 @@ } }, "node_modules/dompurify": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz", - "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==" + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" }, "node_modules/domutils": { "version": "3.1.0", @@ -9186,9 +9259,9 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -9203,9 +9276,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -9248,6 +9321,15 @@ "node": ">= 6" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/enzyme": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", @@ -9391,6 +9473,25 @@ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", "dev": true }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -10308,36 +10409,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -10362,9 +10463,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/extract-zip": { "version": "2.0.1", @@ -10596,9 +10697,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -10607,12 +10708,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -10879,17 +10980,6 @@ "node": ">=8" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -10951,14 +11041,6 @@ "node": ">=6" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -11011,17 +11093,17 @@ "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" }, "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=14.14" } }, "node_modules/fs-monkey": { @@ -11097,15 +11179,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11150,15 +11236,15 @@ } }, "node_modules/get-uri": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.2.tgz", - "integrity": "sha512-5KLucCJobh8vBY1K07EFV4+cPZH3mrV9YeAruUseCQKHB58SGjjT2l9/eA9LD082IiuMjSlFJEcdJ27TXvbZNw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", "dev": true, "dependencies": { "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.0", + "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4", - "fs-extra": "^8.1.0" + "fs-extra": "^11.2.0" }, "engines": { "node": ">= 14" @@ -11365,11 +11451,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11921,10 +12007,23 @@ "loose-envify": "^1.0.0" } }, - "node_modules/ip": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", - "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, "node_modules/ipaddr.js": { @@ -14527,6 +14626,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/jsdom": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", @@ -14642,10 +14747,12 @@ } }, "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -15027,9 +15134,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -15053,11 +15163,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -15211,15 +15321,9 @@ } }, "node_modules/mitt": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", - "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==", - "dev": true - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "dev": true }, "node_modules/moment": { @@ -15850,9 +15954,9 @@ } }, "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "dependencies": { "debug": "^4.3.4" @@ -15862,9 +15966,9 @@ } }, "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -15875,9 +15979,9 @@ } }, "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -15888,13 +15992,12 @@ } }, "node_modules/pac-resolver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz", - "integrity": "sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, "dependencies": { "degenerator": "^5.0.0", - "ip": "^1.1.8", "netmask": "^2.0.2" }, "engines": { @@ -17444,28 +17547,28 @@ } }, "node_modules/proxy-agent": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", - "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", "dev": true, "dependencies": { "agent-base": "^7.0.2", "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.0", + "pac-proxy-agent": "^7.0.1", "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.2" }, "engines": { "node": ">= 14" } }, "node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "dependencies": { "debug": "^4.3.4" @@ -17475,9 +17578,9 @@ } }, "node_modules/proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -17488,9 +17591,9 @@ } }, "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -17544,72 +17647,53 @@ } }, "node_modules/puppeteer": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-20.9.0.tgz", - "integrity": "sha512-kAglT4VZ9fWEGg3oLc4/de+JcONuEJhlh3J6f5R1TLkrY/EHHIHxWXDOzXvaxQCtedmyVXBwg8M+P8YCO/wZjw==", - "deprecated": "< 21.3.7 is no longer supported", + "version": "22.11.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.11.2.tgz", + "integrity": "sha512-8fjdQSgW0sq7471ftca24J7sXK+jXZ7OW7Gx+NEBFNyXrcTiBfukEI46gNq6hiMhbLEDT30NeylK/1ZoPdlKSA==", "dev": true, "hasInstallScript": true, "dependencies": { - "@puppeteer/browsers": "1.4.6", - "cosmiconfig": "8.2.0", - "puppeteer-core": "20.9.0" + "@puppeteer/browsers": "2.2.3", + "cosmiconfig": "9.0.0", + "devtools-protocol": "0.0.1299070", + "puppeteer-core": "22.11.2" + }, + "bin": { + "puppeteer": "lib/esm/puppeteer/node/cli.js" }, "engines": { - "node": ">=16.3.0" + "node": ">=18" } }, "node_modules/puppeteer-core": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.9.0.tgz", - "integrity": "sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg==", + "version": "22.11.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.11.2.tgz", + "integrity": "sha512-vQo+YDuePyvj+92Z9cdtxi/HalKf+k/R4tE80nGtQqJRNqU81eHaHkbVfnLszdaLlvwFF5tipnnSCzqWlEddtw==", "dev": true, "dependencies": { - "@puppeteer/browsers": "1.4.6", - "chromium-bidi": "0.4.16", - "cross-fetch": "4.0.0", - "debug": "4.3.4", - "devtools-protocol": "0.0.1147663", - "ws": "8.13.0" + "@puppeteer/browsers": "2.2.3", + "chromium-bidi": "0.5.23", + "debug": "4.3.5", + "devtools-protocol": "0.0.1299070", + "ws": "8.17.1" }, "engines": { - "node": ">=16.3.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=18" } }, - "node_modules/puppeteer-core/node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true, - "engines": { - "node": ">=10.0.0" + "ms": "2.1.2" }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { + "supports-color": { "optional": true } } @@ -17621,21 +17705,29 @@ "dev": true }, "node_modules/puppeteer/node_modules/cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "dependencies": { - "import-fresh": "^3.2.1", + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0" + "parse-json": "^5.2.0" }, "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/puppeteer/node_modules/js-yaml": { @@ -17651,11 +17743,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -18269,17 +18361,6 @@ "node": ">=12" } }, - "node_modules/react-scripts/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/react-scripts/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -18305,14 +18386,6 @@ "node": ">=10" } }, - "node_modules/react-scripts/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/react-scripts/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -19316,9 +19389,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -19351,6 +19424,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -19435,29 +19516,30 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -19531,13 +19613,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -19616,26 +19702,26 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dev": true, "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", - "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", "dev": true, "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.1", "debug": "^4.3.4", "socks": "^2.7.1" }, @@ -19644,9 +19730,9 @@ } }, "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "dependencies": { "debug": "^4.3.4" @@ -19655,12 +19741,6 @@ "node": ">= 14" } }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true - }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -19904,13 +19984,17 @@ } }, "node_modules/streamx": { - "version": "2.15.6", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz", - "integrity": "sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", "dev": true, "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -20399,20 +20483,23 @@ } }, "node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", "dev": true, "dependencies": { - "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, "node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, "dependencies": { "b4a": "^1.6.4", @@ -20539,6 +20626,15 @@ "node": ">=8" } }, + "node_modules/text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -21012,12 +21108,11 @@ } }, "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "engines": { - "node": ">= 4.0.0" + "node": ">= 10.0.0" } }, "node_modules/unpipe": { @@ -21088,6 +21183,12 @@ "requires-port": "^1.0.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true + }, "node_modules/use-resize-observer": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-6.1.0.tgz", @@ -21193,9 +21294,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -21226,33 +21327,32 @@ } }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -21808,17 +21908,6 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, - "node_modules/workbox-build/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/workbox-build/node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -21843,14 +21932,6 @@ "node": ">=4" } }, - "node_modules/workbox-build/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/workbox-build/node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -22127,9 +22208,9 @@ } }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, @@ -22225,6 +22306,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 04938e82..a18bce5e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,7 +18,7 @@ "@types/file-saver": "^2.0.5", "@types/react-dom": "^17.0.9", "@types/react-router-dom": "^5.3.1", - "axios": "^1.6.0", + "axios": "^1.7.7", "bootstrap": "^5.3.0", "carbon-components": "^10.36.0", "carbon-components-react": "^8.31.3", @@ -98,7 +98,7 @@ "jest-transform-stub": "^2.0.0", "moxios": "^0.4.0", "nodemon": "^2.0.13", - "puppeteer": "^20.1.2", + "puppeteer": "^22.11.2", "react-inject-env": "^2.1.0", "react-test-renderer": "^18.2.0", "redux-mock-store": "^1.5.4" diff --git a/frontend/src/components/agent-create-join-token.tsx b/frontend/src/components/agent-create-join-token.tsx index 45b02de7..65859249 100644 --- a/frontend/src/components/agent-create-join-token.tsx +++ b/frontend/src/components/agent-create-join-token.tsx @@ -7,6 +7,7 @@ import { serverSelectedFunc } from 'redux/actions'; import { RootState } from 'redux/reducers'; import { ToastContainer } from "react-toastify" import { showResponseToast, showToast } from './error-api'; +import apiEndpoints from './apiConfig'; type CreateJoinTokenProp = { globalServerSelected: string, @@ -102,7 +103,7 @@ class CreateJoinToken extends Component { getApiEntryCreateEndpoint(): string { if (!IsManager) { - return GetApiServerUri('/api/tornjak/clusters/create') + return GetApiServerUri(apiEndpoints.tornjakClustersApi) } else if (IsManager && this.state.selectedServer !== "") { return GetApiServerUri('/manager-api/tornjak/clusters/create') + "/" + this.state.selectedServer } else { diff --git a/frontend/src/components/cluster-edit.tsx b/frontend/src/components/cluster-edit.tsx index d04c1f96..942037d2 100644 --- a/frontend/src/components/cluster-edit.tsx +++ b/frontend/src/components/cluster-edit.tsx @@ -27,6 +27,7 @@ import { DebugServerInfo, } from './types'; import { showResponseToast, showToast } from './error-api'; +import apiEndpoints from './apiConfig'; type ClusterEditProp = { // tornjak server debug info of the selected server @@ -259,7 +260,7 @@ class ClusterEdit extends Component { getApiEntryCreateEndpoint(): string { if (!IsManager) { - return GetApiServerUri('/api/tornjak/clusters/edit') + return GetApiServerUri(apiEndpoints.tornjakClustersApi) } if (IsManager && this.state.selectedServer !== "") { return GetApiServerUri('/manager-api/tornjak/clusters/edit') + "/" + this.state.selectedServer @@ -342,7 +343,7 @@ class ClusterEdit extends Component { return } - axios.post(endpoint, cjtData) + axios.patch(endpoint, cjtData) .then( res => this.setState({ message: "Request:" + JSON.stringify(cjtData, null, ' ') + "\n\nSuccess:" + JSON.stringify(res.data, null, ' '), diff --git a/frontend/src/components/entry-create.tsx b/frontend/src/components/entry-create.tsx index 472f6951..5870eb3b 100644 --- a/frontend/src/components/entry-create.tsx +++ b/frontend/src/components/entry-create.tsx @@ -47,6 +47,7 @@ import EntryExpiryFeatures from './entry-expiry-features'; import CreateEntryJson from './entry-create-json'; import { ToastContainer } from "react-toastify" import { showResponseToast, showToast } from './error-api'; +import apiEndpoints from './apiConfig'; // import PropTypes from "prop-types"; // needed for testing will be removed on last pr type CreateEntryProp = { @@ -573,7 +574,7 @@ class CreateEntry extends Component { getApiEntryCreateEndpoint(): string { if (!IsManager) { - return GetApiServerUri('/api/entry/create') + return GetApiServerUri(apiEndpoints.spireEntriesApi) } if (IsManager && this.state.selectedServer !== "") { return GetApiServerUri('/manager-api/entry/create') + "/" + this.state.selectedServer diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index 080f7b6f..7e9aca4a 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -8,17 +8,26 @@ import tornjak_logo from "res/tornjak_logo.png"; import TornjakHelper from 'components/tornjak-helper'; import KeycloakService from "auth/KeycloakAuth"; import { RootState } from 'redux/reducers'; +import TornjakApi from './tornjak-api-helpers'; import { clickedDashboardTableFunc, isAuthenticatedUpdateFunc, accessTokenUpdateFunc, UserRolesUpdateFunc, + serverInfoUpdateFunc, + tornjakServerInfoUpdateFunc, + spireDebugServerInfoUpdateFunc, + tornjakMessageFunc } from 'redux/actions'; +import { Tag } from 'carbon-components-react'; import { - AccessToken + AccessToken, + ServerInfo, + DebugServerInfo, + TornjakServerInfo } from './types'; import HeaderToolBar from './navbar-header-toolbar'; -import {env} from '../env'; +import { env } from '../env'; const Auth_Server_Uri = env.REACT_APP_AUTH_SERVER_URI; @@ -39,15 +48,31 @@ type NavigationBarProp = { clickedDashboardTableFunc: (globalClickedDashboardTable: string) => void, // the clicked dashboard table globalClickedDashboardTable: string, + // the server trust domain and nodeAttestorPlugin as a ServerInfoType + globalServerInfo: ServerInfo, + // tornjak server debug info of the selected server + globalDebugServerInfo: DebugServerInfo, + // tornjak server info of the selected server + globalTornjakServerInfo: TornjakServerInfo, + // dispatches a payload for the server trust domain and nodeAttestorPlugin as a ServerInfoType and has a return type of void + serverInfoUpdateFunc: (globalServerInfo: ServerInfo) => void, + // dispatches a payload for the tornjak server info of the selected server and has a return type of void + tornjakServerInfoUpdateFunc: (globalTornjakServerInfo: TornjakServerInfo) => void, + // dispatches a payload for the debug server info of the selected server and has a return type of void + spireDebugServerInfoUpdateFunc: (globalDebugServerInfo: DebugServerInfo) => void, + // dispatches a payload for an Error Message/ Success Message of an executed function as a string and has a return type of void + tornjakMessageFunc: (globalErrorMessage: string) => void, } type NavigationBarState = {} class NavigationBar extends Component { TornjakHelper: TornjakHelper; + TornjakApi: TornjakApi; constructor(props: NavigationBarProp) { super(props); this.TornjakHelper = new TornjakHelper({}); + this.TornjakApi = new TornjakApi(props); this.state = {}; } @@ -64,6 +89,9 @@ class NavigationBar extends Component { } } } + this.TornjakApi.populateLocalTornjakServerInfo(this.props.tornjakServerInfoUpdateFunc, this.props.tornjakMessageFunc); + this.TornjakApi.populateLocalTornjakDebugServerInfo(this.props.spireDebugServerInfoUpdateFunc, this.props.tornjakMessageFunc); + this.TornjakApi.populateServerInfo(this.props.globalTornjakServerInfo, this.props.serverInfoUpdateFunc); } render() { @@ -133,6 +161,14 @@ class NavigationBar extends Component { Tornjak + {/* Temporarily using trust domain as server unique identifier */} +
+ + Server ID: + {this.props.globalServerInfo.trustDomain} + {this.props.globalDebugServerInfo.svid_chain[0].id.path} + +
); } @@ -149,11 +185,23 @@ const mapStateToProps = (state: RootState) => ({ globalIsAuthenticated: state.auth.globalIsAuthenticated, globalAccessToken: state.auth.globalAccessToken, globalUserRoles: state.auth.globalUserRoles, + globalServerInfo: state.servers.globalServerInfo, + globalDebugServerInfo: state.servers.globalDebugServerInfo, + globalTornjakServerInfo: state.servers.globalTornjakServerInfo, }) export default connect( mapStateToProps, - { clickedDashboardTableFunc, isAuthenticatedUpdateFunc, accessTokenUpdateFunc, UserRolesUpdateFunc } + { + clickedDashboardTableFunc, + isAuthenticatedUpdateFunc, + accessTokenUpdateFunc, + UserRolesUpdateFunc, + serverInfoUpdateFunc, + tornjakServerInfoUpdateFunc, + spireDebugServerInfoUpdateFunc, + tornjakMessageFunc + } )(NavigationBar) export { NavigationBar } \ No newline at end of file diff --git a/frontend/src/components/style.css b/frontend/src/components/style.css index 32064ed2..c996de5b 100644 --- a/frontend/src/components/style.css +++ b/frontend/src/components/style.css @@ -425,4 +425,12 @@ .delete-cluster-button { width: 180px; float: right; -} \ No newline at end of file +} +.spire-server-unique-identifier { + margin-top: 20px; + margin-right: 20px; + z-index: 1000; /* High z-index to ensure it's on top */ + position: relative; + display: inline; + float: right; /* Float to the right */ +} diff --git a/frontend/src/components/tornjak-api-helpers.tsx b/frontend/src/components/tornjak-api-helpers.tsx index df847ada..5a1d4f64 100644 --- a/frontend/src/components/tornjak-api-helpers.tsx +++ b/frontend/src/components/tornjak-api-helpers.tsx @@ -18,6 +18,7 @@ import { showResponseToast } from './error-api'; // import { logError } from './helpers'; // import { displayResponseError } from './error-api'; import { env } from '../env'; +import apiEndpoints from './apiConfig'; const Auth_Server_Uri = env.REACT_APP_AUTH_SERVER_URI; type TornjakApiProp = {} @@ -61,7 +62,7 @@ class TornjakApi extends Component { spireHealthCheckingFunc: { (globalSpireHealthChecking: boolean): void; }, ) => { spireHealthCheckingFunc(false); - axios.get(GetApiServerUri("/api/healthcheck"), { crossdomain: true }) + axios.get(GetApiServerUri(apiEndpoints.spireHealthCheckApi), { crossdomain: true }) .then(response => { console.log("SPIRE HEALTH:", response.data.status); if (response.data.status === 1) { @@ -77,56 +78,17 @@ class TornjakApi extends Component { }) } - registerSelectors = (serverName: string, wLoadAttdata: { spiffeid: string; plugin: string; }, - refreshSelectorsState: { (serverName: string, agentworkloadSelectorInfoFunc: (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]) => void): void; }, - agentworkloadSelectorInfoFunc: (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]) => void) => { - axios.post(GetApiServerUri('/manager-api/tornjak/selectors/register/') + serverName, wLoadAttdata) - .then(res => { - refreshSelectorsState(serverName, agentworkloadSelectorInfoFunc); - } - ) - .catch((error) => { - showResponseToast(error, { caption: "Could not register selectors." }) - }) - } - + // registerLocalSelectors registers the selected selectors in local mode registerLocalSelectors = (wLoadAttdata: { spiffeid: string; plugin: string; }, refreshLocalSelectorsState: { (agentworkloadSelectorInfoFunc: (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]) => void): void; }, agentworkloadSelectorInfoFunc: (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]) => void) => { - axios.post(GetApiServerUri('/api/tornjak/selectors/register'), wLoadAttdata) + axios.post(GetApiServerUri(apiEndpoints.tornjakSelectorsApi), wLoadAttdata) .then(res => refreshLocalSelectorsState(agentworkloadSelectorInfoFunc)) .catch((error) => { showResponseToast(error, { caption: "Could not register local selectors." }) }) } - // refreshSelectorsState returns the list agent's with their workload plugin info for the selected server in manager mode - // [ - // "agent1workloadselectorinfo": [ - // { - // "id": "agentid", - // "spiffeid": "agentspiffeeid", - // "selectors": "agentworkloadselectors" - // } - // ], - // "agent2workloadselectorinfo": [ - // { - // "id": "agentid", - // "spiffeid": "agentspiffeeid", - // "selectors": "agentworkloadselectors" - // } - // ] - // ] - refreshSelectorsState = (serverName: string, - agentworkloadSelectorInfoFunc: { - (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]): void; - (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]): void; - }) => { - axios.get(GetApiServerUri("/manager-api/tornjak/selectors/list/") + serverName, { crossdomain: true }) - .then(response => { - agentworkloadSelectorInfoFunc(response.data["agents"]); - }) - .catch((error) => showResponseToast(error, { caption: "Could not refresh selector state." })) - } + // refreshLocalSelectorsState returns the list agent's with their workload plugin info for the local server // [ // "agent1workloadselectorinfo": [ @@ -147,7 +109,7 @@ class TornjakApi extends Component { refreshLocalSelectorsState = (agentworkloadSelectorInfoFunc: { (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]): void; }) => { - axios.get(GetApiServerUri("/api/tornjak/selectors/list"), { crossdomain: true }) + axios.get(GetApiServerUri(apiEndpoints.tornjakSelectorsApi), { crossdomain: true }) .then(response => { console.log(response.data) agentworkloadSelectorInfoFunc(response.data["agents"]) @@ -155,56 +117,19 @@ class TornjakApi extends Component { .catch((error) => showResponseToast(error, { caption: "Could not refresh local selector states." })) } - // populateTornjakAgentInfo returns tornjak info of requested agents including cluster name and selector - populateTornjakAgentInfo = (serverName: string, - agentworkloadSelectorInfoFunc: (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]) => void, - inputData: string) => { - axios.post(GetApiServerUri("/manager-api/tornjak/agents/list/") + serverName, inputData, - { - crossdomain: true, - }) - .then(response => { - agentworkloadSelectorInfoFunc(response.data["agents"]); - }) - .catch((error) => showResponseToast(error, { caption: "Could not populate tornjak agent info." })) - } - // populateLocalTornjakAgentInfo returns tornjak info of requested agents including cluster name and selector populateLocalTornjakAgentInfo = ( agentworkloadSelectorInfoFunc: (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]) => void, - inputData: string + inputData: string // Assuming this input data is used as query parameters ) => { - axios.post(GetApiServerUri("/api/tornjak/agents/list"), inputData, { crossdomain: true }) + axios.get(GetApiServerUri(apiEndpoints.tornjakAgentsApi), { + params: { input: inputData }, // Add inputData as query parameter + crossdomain: true + }) .then(response => { - agentworkloadSelectorInfoFunc(response.data["agents"]) + agentworkloadSelectorInfoFunc(response.data["agents"]); }) - .catch((error) => showResponseToast(error, { caption: "Could not populate local tornjak agent info." })) - } - - // populateTornjakServerInfo returns the tornjak server info of the selected server in manager mode - populateTornjakServerInfo = ( - serverName: string, - tornjakServerInfoUpdateFunc: { (globalTornjakServerInfo: TornjakServerInfo): void }, - tornjakMessageFunc: { (globalErrorMessage: string): void } - ) => { - axios.get(GetApiServerUri('/manager-api/tornjak/serverinfo/') + serverName, { crossdomain: true }) - .then(response => { - tornjakServerInfoUpdateFunc(response.data) - tornjakMessageFunc(response.statusText) - }).catch(error => { - showResponseToast(error, { caption: "Could not populate tornjak server info." }) - tornjakServerInfoUpdateFunc({ - plugins: { - DataStore: [], - KeyManager: [], - NodeAttestor: [], - NodeResolver: [], - Notifier: [] - }, - trustDomain: "", - verboseConfig: "" - }); - }); + .catch((error) => showResponseToast(error, { caption: "Could not populate local tornjak agent info." })); } // populateLocalTornjakDebugServerInfo returns the debug server info of the server in local mode @@ -212,7 +137,7 @@ class TornjakApi extends Component { spireDebugServerInfoUpdateFunc: { (globalDebugServerInfo: DebugServerInfo): void }, tornjakMessageFunc: { (globalErrorMessage: string): void } ) => { - axios.get(GetApiServerUri('/api/debugserver'), { crossdomain: true }) + axios.get(GetApiServerUri(apiEndpoints.spireServerInfoApi), { crossdomain: true }) .then(response => { spireDebugServerInfoUpdateFunc(response.data) tornjakMessageFunc(response.statusText) @@ -227,7 +152,7 @@ class TornjakApi extends Component { tornjakServerInfoUpdateFunc: { (globalTornjakServerInfo: TornjakServerInfo): void }, tornjakMessageFunc: { (globalErrorMessage: string): void } ) => { - axios.get(GetApiServerUri('/api/tornjak/serverinfo'), { crossdomain: true }) + axios.get(GetApiServerUri(apiEndpoints.tornjakServerInfoApi), { crossdomain: true }) .then(response => { if (response.status === 200) { tornjakServerInfoUpdateFunc(response.data) @@ -266,25 +191,10 @@ class TornjakApi extends Component { serverInfoUpdateFunc(reqInfo); } - populateEntriesUpdate = (serverName: string, - entriesListUpdateFunc: { (globalEntriesList: EntriesList[]): void; }, - tornjakMessageFunc: { (globalErrorMessage: string): void; }) => { - axios.get(GetApiServerUri('/manager-api/entry/list/') + serverName, { crossdomain: true }) - .then(response => { - if (!response.data["entries"]) { - entriesListUpdateFunc([]); - } else { entriesListUpdateFunc(response.data["entries"]); } - tornjakMessageFunc(response.statusText); - }).catch(error => { - entriesListUpdateFunc([]); - tornjakMessageFunc("Error retrieving " + serverName + " : " + error + (typeof (error.response) !== "undefined" ? ":" + error.response.data : "")); - showResponseToast(error, { caption: "Could not populate entries." }) - }) - } - + // populateLocalEntriesUpdate - returns the list of entries with their info in Local mode for the server populateLocalEntriesUpdate = (entriesListUpdateFunc: { (globalEntriesList: EntriesList[]): void; }, tornjakMessageFunc: { (globalErrorMessage: string): void; }) => { - axios.get(GetApiServerUri('/api/entry/list'), { crossdomain: true }) + axios.get(GetApiServerUri(apiEndpoints.spireEntriesApi), { crossdomain: true }) .then(response => { if (!response.data["entries"]) { entriesListUpdateFunc([]) @@ -299,30 +209,12 @@ class TornjakApi extends Component { }) } - // populateAgentsUpdate returns the list of agents with their info in manager mode for the selected server - populateAgentsUpdate = (serverName: string, - agentsListUpdateFunc: { (globalAgentsList: AgentsList[]): void; }, - tornjakMessageFunc: { (globalErrorMessage: string): void; }) => { - axios.get(GetApiServerUri('/manager-api/agent/list/') + serverName, { crossdomain: true }) - .then(response => { - if (!response.data["agents"]) { - agentsListUpdateFunc([]); - } else { agentsListUpdateFunc(response.data["agents"]); } - tornjakMessageFunc(response.statusText); - }).catch(error => { - showResponseToast(error, { caption: "Could not populate agents." }) - agentsListUpdateFunc([]); - tornjakMessageFunc("Error retrieving " + serverName + " : " + error.message); - }); - - } - // populateLocalAgentsUpdate - returns the list of agents with their info in Local mode for the server populateLocalAgentsUpdate = (agentsListUpdateFunc: { (globalAgentsList: AgentsList[]): void; }, tornjakMessageFunc: { (globalErrorMessage: string): void; }) => { - axios.get(GetApiServerUri('/api/agent/list'), { crossdomain: true }) + axios.get(GetApiServerUri(apiEndpoints.spireAgentsApi), { crossdomain: true }) .then(response => { if (!response.data["agents"]) { agentsListUpdateFunc([]); @@ -336,27 +228,12 @@ class TornjakApi extends Component { }) } - // populateClustersUpdate returns the list of clusters with their info in manager mode for the selected server - populateClustersUpdate = (serverName: string, - clustersListUpdateFunc: { (globalClustersList: ClustersList[]): void; }, - tornjakMessageFunc: { (globalErrorMessage: string): void; }) => { - axios.get(GetApiServerUri('/manager-api/tornjak/clusters/list/') + serverName, { crossdomain: true }) - .then(response => { - clustersListUpdateFunc(response.data["clusters"]); - tornjakMessageFunc(response.statusText); - }).catch(error => { - showResponseToast(error, { caption: "Could not populate clusters." }) - clustersListUpdateFunc([]); - tornjakMessageFunc("Error retrieving " + serverName + " : " + error.message); - }); - } - // populateLocalClustersUpdate - returns the list of clusters with their info in Local mode for the server populateLocalClustersUpdate = ( clustersListUpdateFunc: { (globalClustersList: ClustersList[]): void }, tornjakMessageFunc: { (globalErrorMessage: string): void } ) => { - axios.get(GetApiServerUri('/api/tornjak/clusters/list'), { crossdomain: true }) + axios.get(GetApiServerUri(apiEndpoints.tornjakClustersApi), { crossdomain: true }) .then(response => { clustersListUpdateFunc(response.data["clusters"]) tornjakMessageFunc(response.statusText) @@ -368,14 +245,59 @@ class TornjakApi extends Component { }) } - // clusterDelete - returns success message after successful deletion of a cluster in manager mode - async clusterDelete(serverName: string, inputData: { cluster: { name: string; }; }, clustersListUpdateFunc: { (globalClustersList: ClustersList[]): void }, globalClustersList: any[]) { - const response = await axios.post(GetApiServerUri("/manager-api/tornjak/clusters/delete/") + serverName, inputData, + // localClusterDelete - returns success message after successful deletion of a cluster in Local mode for the server + + async localClusterDelete( + inputData: { cluster: { name: string } }, + clustersListUpdateFunc: { (globalClustersList: ClustersList[]): void }, + globalClustersList: any[] + ) { + try { + const response = await axios.delete(GetApiServerUri(apiEndpoints.tornjakClustersApi), { + data: inputData, + headers: { + 'Content-Type': 'application/json' + }, + crossdomain: true, + }); + clustersListUpdateFunc(globalClustersList.filter(el => el.name !== inputData.cluster.name)); + return response.data; + } catch (error) { + return error; + } + } + + // localEntryDelete - returns success message after successful deletion of a entry in Local mode for the server + async localEntryDelete( + inputData: { ids: string[] }, + entriesListUpdateFunc: { (globalEntriesList: EntriesList[]): void }, + globalEntriesList: EntriesList[] + ) { + try { + const response = await axios.delete(GetApiServerUri(apiEndpoints.spireEntriesApi), { + data: { ids: inputData.ids }, + headers: { + 'Content-Type': 'application/json' + }, + crossdomain: true, + }); + entriesListUpdateFunc(globalEntriesList.filter(el => !inputData.ids.includes(el.id))); + return response.data; + } catch (error) { + return error; + } + } + + // manager apis + + // entryDelete - returns success message after successful deletion of a entry in manager mode + async entryDelete(serverName: string, inputData: { ids: string[] }, entriesListUpdateFunc: { (globalEntriesList: EntriesList[]): void }, globalEntriesList: any[]) { + const response = await axios.post(GetApiServerUri("/manager-api/tornjak/entry/delete/") + serverName, inputData, { crossdomain: true, }) .then(function (response) { - clustersListUpdateFunc(globalClustersList.filter(el => + entriesListUpdateFunc(globalEntriesList.filter(el => el.name !== inputData)) return response.data; }) @@ -385,9 +307,9 @@ class TornjakApi extends Component { return response.data; } - // localClusterDelete - returns success message after successful deletion of a cluster in Local mode for the server - async localClusterDelete(inputData: { cluster: { name: string; }; }, clustersListUpdateFunc: { (globalClustersList: ClustersList[]): void }, globalClustersList: any[]) { - const response = await axios.post(GetApiServerUri("/api/tornjak/clusters/delete"), inputData, + // clusterDelete - returns success message after successful deletion of a cluster in manager mode + async clusterDelete(serverName: string, inputData: { cluster: { name: string; }; }, clustersListUpdateFunc: { (globalClustersList: ClustersList[]): void }, globalClustersList: any[]) { + const response = await axios.post(GetApiServerUri("/manager-api/tornjak/clusters/delete/") + serverName, inputData, { crossdomain: true, }) @@ -399,7 +321,139 @@ class TornjakApi extends Component { .catch(function (error) { return error.message; }) - return response; + return response.data; + } + + // populateClustersUpdate returns the list of clusters with their info in manager mode for the selected server + populateClustersUpdate = (serverName: string, + clustersListUpdateFunc: { (globalClustersList: ClustersList[]): void; }, + tornjakMessageFunc: { (globalErrorMessage: string): void; }) => { + axios.get(GetApiServerUri('/manager-api/tornjak/clusters/list/') + serverName, { crossdomain: true }) + .then(response => { + clustersListUpdateFunc(response.data["clusters"]); + tornjakMessageFunc(response.statusText); + }).catch(error => { + showResponseToast(error, { caption: "Could not populate clusters." }) + clustersListUpdateFunc([]); + tornjakMessageFunc("Error retrieving " + serverName + " : " + error.message); + }); + } + + // populateAgentsUpdate returns the list of agents with their info in manager mode for the selected server + populateAgentsUpdate = (serverName: string, + agentsListUpdateFunc: { (globalAgentsList: AgentsList[]): void; }, + tornjakMessageFunc: { (globalErrorMessage: string): void; }) => { + axios.get(GetApiServerUri('/manager-api/agent/list/') + serverName, { crossdomain: true }) + .then(response => { + if (!response.data["agents"]) { + agentsListUpdateFunc([]); + } else { agentsListUpdateFunc(response.data["agents"]); } + tornjakMessageFunc(response.statusText); + }).catch(error => { + showResponseToast(error, { caption: "Could not populate agents." }) + agentsListUpdateFunc([]); + tornjakMessageFunc("Error retrieving " + serverName + " : " + error.message); + }); + } + + // populateEntriesUpdate - returns the list of entries with their info in manager mode for the server + populateEntriesUpdate = (serverName: string, + entriesListUpdateFunc: { (globalEntriesList: EntriesList[]): void; }, + tornjakMessageFunc: { (globalErrorMessage: string): void; }) => { + axios.get(GetApiServerUri('/manager-api/entry/list/') + serverName, { crossdomain: true }) + .then(response => { + if (!response.data["entries"]) { + entriesListUpdateFunc([]); + } else { entriesListUpdateFunc(response.data["entries"]); } + tornjakMessageFunc(response.statusText); + }).catch(error => { + entriesListUpdateFunc([]); + tornjakMessageFunc("Error retrieving " + serverName + " : " + error + (typeof (error.response) !== "undefined" ? ":" + error.response.data : "")); + showResponseToast(error, { caption: "Could not populate entries." }) + }) + } + + // populateTornjakServerInfo returns the tornjak server info of the selected server in manager mode + populateTornjakServerInfo = ( + serverName: string, + tornjakServerInfoUpdateFunc: { (globalTornjakServerInfo: TornjakServerInfo): void }, + tornjakMessageFunc: { (globalErrorMessage: string): void } + ) => { + axios.get(GetApiServerUri('/manager-api/tornjak/serverinfo/') + serverName, { crossdomain: true }) + .then(response => { + tornjakServerInfoUpdateFunc(response.data) + tornjakMessageFunc(response.statusText) + }).catch(error => { + showResponseToast(error, { caption: "Could not populate tornjak server info." }) + tornjakServerInfoUpdateFunc({ + plugins: { + DataStore: [], + KeyManager: [], + NodeAttestor: [], + NodeResolver: [], + Notifier: [] + }, + trustDomain: "", + verboseConfig: "" + }); + }); + } + + // populateTornjakAgentInfo returns tornjak info of requested agents including cluster name and selector + populateTornjakAgentInfo = (serverName: string, + agentworkloadSelectorInfoFunc: (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]) => void, + inputData: string) => { + axios.post(GetApiServerUri("/manager-api/tornjak/agents/list/") + serverName, inputData, + { + crossdomain: true, + }) + .then(response => { + agentworkloadSelectorInfoFunc(response.data["agents"]); + }) + .catch((error) => showResponseToast(error, { caption: "Could not populate tornjak agent info." })) + } + + // refreshSelectorsState returns the list agent's with their workload plugin info for the selected server in manager mode + // [ + // "agent1workloadselectorinfo": [ + // { + // "id": "agentid", + // "spiffeid": "agentspiffeeid", + // "selectors": "agentworkloadselectors" + // } + // ], + // "agent2workloadselectorinfo": [ + // { + // "id": "agentid", + // "spiffeid": "agentspiffeeid", + // "selectors": "agentworkloadselectors" + // } + // ] + // ] + refreshSelectorsState = (serverName: string, + agentworkloadSelectorInfoFunc: { + (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]): void; + (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]): void; + }) => { + axios.get(GetApiServerUri("/manager-api/tornjak/selectors/list/") + serverName, { crossdomain: true }) + .then(response => { + agentworkloadSelectorInfoFunc(response.data["agents"]); + }) + .catch((error) => showResponseToast(error, { caption: "Could not refresh selector state." })) + } + + // registerSelectors registers the selected selectors in manager mode + registerSelectors = (serverName: string, wLoadAttdata: { spiffeid: string; plugin: string; }, + refreshSelectorsState: { (serverName: string, agentworkloadSelectorInfoFunc: (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]) => void): void; }, + agentworkloadSelectorInfoFunc: (globalAgentsWorkLoadAttestorInfo: AgentsWorkLoadAttestorInfo[]) => void) => { + axios.post(GetApiServerUri('/manager-api/tornjak/selectors/register/') + serverName, wLoadAttdata) + .then(res => { + refreshSelectorsState(serverName, agentworkloadSelectorInfoFunc); + } + ) + .catch((error) => { + showResponseToast(error, { caption: "Could not register selectors." }) + }) } } diff --git a/frontend/src/env.ts b/frontend/src/env.ts index 3e12aa81..2e414d2d 100644 --- a/frontend/src/env.ts +++ b/frontend/src/env.ts @@ -10,5 +10,6 @@ declare global { REACT_APP_TORNJAK_MANAGER: string, REACT_APP_KEYCLOAK_REALM: string, REACT_APP_OIDC_CLIENT_ID: string, + REACT_APP_API_VERSION: string, } export const env: EnvType = { ...process.env, ...window.env } \ No newline at end of file diff --git a/frontend/src/tables/agents-list-table.tsx b/frontend/src/tables/agents-list-table.tsx index f193154b..e3fb7ad1 100644 --- a/frontend/src/tables/agents-list-table.tsx +++ b/frontend/src/tables/agents-list-table.tsx @@ -11,6 +11,7 @@ import { AgentsList, AgentsWorkLoadAttestorInfo } from "components/types"; import { DenormalizedRow } from "carbon-components-react"; import { RootState } from "redux/reducers"; import { showResponseToast } from "components/error-api"; +import apiEndpoints from 'components/apiConfig'; // AgentListTable takes in // listTableData: agents data to be rendered on table @@ -99,7 +100,7 @@ class AgentsListTable extends React.Component { @@ -127,33 +134,34 @@ class AgentsListTable extends React.Component showResponseToast(error, {caption: "Could not delete agent."})) + .catch((error) => showResponseToast(error, { caption: "Could not delete agent." })) } banAgent(selectedRows: readonly DenormalizedRow[]) { - var id: {path: string; trust_domain: string}[] = [], i = 0, endpoint = "", prefix = "spiffe://" + var id: { path: string; trust_domain: string }[] = [], i = 0, endpoint = "", prefix = "spiffe://" if (IsManager) { endpoint = GetApiServerUri('/manager-api/agent/ban') + "/" + this.props.globalServerSelected } else { - endpoint = GetApiServerUri('/api/agent/ban') + endpoint = GetApiServerUri(apiEndpoints.spireAgentsBanApi) } if (selectedRows === undefined || !selectedRows) return "" for (i = 0; i < selectedRows.length; i++) { - id[i] = {path: "", trust_domain: ""} + id[i] = { path: "", trust_domain: "" } id[i].trust_domain = selectedRows[i].cells[1].value id[i].path = selectedRows[i].cells[2].value.substr(selectedRows[i].cells[1].value.concat(prefix).length) - axios.post(endpoint, {id: {trust_domain: id[i].trust_domain, path: id[i].path}}) + axios.post(endpoint, { id: { trust_domain: id[i].trust_domain, path: id[i].path } }) .then(res => { alert("Ban SUCCESS") this.componentDidMount() }) - .catch((error) => showResponseToast(error, {caption: "Could not ban agent."})) + .catch((error) => showResponseToast(error, { caption: "Could not ban agent." })) } } render() { diff --git a/frontend/src/tables/clusters-list-table.tsx b/frontend/src/tables/clusters-list-table.tsx index 8d86c32b..276fe130 100644 --- a/frontend/src/tables/clusters-list-table.tsx +++ b/frontend/src/tables/clusters-list-table.tsx @@ -11,6 +11,7 @@ import { ClustersList } from "components/types"; import { DenormalizedRow } from "carbon-components-react"; import { RootState } from "redux/reducers"; import { showResponseToast } from "components/error-api"; +import TornjakApi from 'components/tornjak-api-helpers'; // ClusterListTable takes in // listTableData: clusters data to be rendered on table @@ -44,8 +45,10 @@ type ClustersListTableState = { } class ClustersListTable extends React.Component { + TornjakApi: TornjakApi; constructor(props: ClustersListTableProp) { super(props); + this.TornjakApi = new TornjakApi(props); this.state = { listData: props.data, listTableData: [], @@ -88,34 +91,29 @@ class ClustersListTable extends React.Component { - if (this.props.globalClustersList === undefined) return - for (let i = 0; i < responses.length; i++) { - this.props.clustersListUpdateFunc(this.props.globalClustersList.filter(el =>el.name !== cluster[i].name)) + cluster[i] = { name: selectedRows[i].cells[1].value }; + if (IsManager) { + successMessage = this.TornjakApi.clusterDelete(this.props.globalServerSelected, { cluster: cluster[i] }, this.props.clustersListUpdateFunc, this.props.globalClustersList); + } else { + successMessage = this.TornjakApi.localClusterDelete({ cluster: cluster[i] }, this.props.clustersListUpdateFunc, this.props.globalClustersList); + } + successMessage.then(function (result) { + if (result === "SUCCESS") { + window.alert(`CLUSTER "${cluster[i].name}" DELETED SUCCESSFULLY!`); + window.location.reload(); + } else { + window.alert(`Error deleting cluster "${cluster[i].name}": ` + result); } + return; }) - .catch((error) => showResponseToast(error, {caption: "Could not delete cluster."})) + } } + render() { const { listTableData } = this.state; const headerData = [ diff --git a/frontend/src/tables/entries-list-table.tsx b/frontend/src/tables/entries-list-table.tsx index 220cd5cf..1dd33e83 100644 --- a/frontend/src/tables/entries-list-table.tsx +++ b/frontend/src/tables/entries-list-table.tsx @@ -12,6 +12,8 @@ import { RootState } from "redux/reducers"; import { DenormalizedRow } from "carbon-components-react"; import { saveAs } from "file-saver"; import { showResponseToast } from "components/error-api"; +import apiEndpoints from 'components/apiConfig'; +import TornjakApi from 'components/tornjak-api-helpers'; // EntriesListTable takes in // listTableData: entries data to be rendered on table @@ -38,8 +40,10 @@ type EntriesListTableState = { } class EntriesListTable extends React.Component { + TornjakApi: TornjakApi; constructor(props: EntriesListTableProp) { super(props); + this.TornjakApi = new TornjakApi(props); this.state = { listData: props.data, listTableData: [{ "id": "0" }], @@ -83,33 +87,36 @@ class EntriesListTable extends React.Component { - if (this.props.globalEntriesList === undefined) { - return - } - for (let i = 0; i < responses.length; i++) { - this.props.entriesListUpdateFunc(this.props.globalEntriesList.filter(el => el.id !== responses[i].data.results[0].id)) + if (!selectedRows || selectedRows.length === 0) return ""; + + // Collect the IDs of the selected entries + const idsToDelete = selectedRows.map(row => row.cells[1].value); + + const deletePromise = IsManager + ? this.TornjakApi.entryDelete(this.props.globalServerSelected, { ids: idsToDelete }, this.props.entriesListUpdateFunc, this.props.globalEntriesList) + : this.TornjakApi.localEntryDelete({ ids: idsToDelete }, this.props.entriesListUpdateFunc, this.props.globalEntriesList); + + deletePromise + .then(response => { + const results = response.results; // Ensure you're accessing the 'results' array in the response + + if (Array.isArray(results)) { + const successIds = results.map(result => result.id); + const failedIds = idsToDelete.filter(id => !successIds.includes(id)); + + if (failedIds.length === 0) { + window.alert(`Entries deleted successfully!`); + window.location.reload(); // Reload the page or update the UI as needed + } else { + window.alert(`Error deleting entries with IDs: ${failedIds.join(', ')}`); + } + } else { + window.alert("Unexpected response format. Could not delete entries."); } }) - .catch((error) => showResponseToast(error, {caption: "Could not delete entry."})) + .catch(error => { + window.alert(`Error deleting entries: ${error.message}`); + }); } downloadEntries(selectedRows: readonly DenormalizedRow[]) { @@ -117,14 +124,14 @@ class EntriesListTable extends React.Component